sjh37 / EntityFramework-Reverse-POCO-Code-First-Generator

EntityFramework Reverse POCO Code First Generator - Beautifully generated code that is fully customisable. This generator creates code as if you reverse engineered a database and lovingly created the code by hand. It is free to academics (you need a .edu or a .ac email address), not free for commercial use. Obtain your licence from
https://www.reversepoco.co.uk/
Other
700 stars 230 forks source link

Using stored procedure OUT parameters with multiple result sets #769

Closed sjh37 closed 1 year ago

sjh37 commented 1 year ago
CREATE PROCEDURE CheckIfApplicationIsComplete
    @ApplicationId INT, @IsApplicationComplete BIT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;

    IF (@ApplicationId < 10)
    BEGIN
        SET @IsApplicationComplete = 0;
        SELECT 'Application' [Key], 'Not complete' [Value];
    END
    ELSE
    BEGIN
        SET @IsApplicationComplete = 1;
        SELECT 'Application' [Key], 'Complete' [Value];
    END
END;
GO

SET NOCOUNT ON;
DECLARE @ApplicationId INT;
DECLARE @IsApplicationComplete BIT;
SET @ApplicationId = 5;
EXEC dbo.CheckIfApplicationIsComplete @ApplicationId, @IsApplicationComplete OUTPUT
PRINT @IsApplicationComplete -- Check Messages tab for result

SET @ApplicationId = 50;
EXEC dbo.CheckIfApplicationIsComplete @ApplicationId, @IsApplicationComplete OUTPUT
PRINT @IsApplicationComplete -- Check Messages tab for result
sjh37 commented 1 year ago

Investigate this helper file from github.com/dotnet/efcore/issues/8127

Consume it like this

_context.LoadStoredProc("get_user_by_lastupdateddate]")
    .WithSqlParam("@lastupdatedfrom", lastUpdatedFrom)
    .WithSqlParam("@startwithuserid", startWithUserId)
    .ExecuteStoredProc((handler) =>
    {
        usersData.AddRange(handler.ReadToList<Users>());
        handler.NextResult();

        usersData.AddRange(handler.ReadToList<Users2>());
        handler.NextResult();

        usersData.AddRange(handler.ReadToList<Users3>());
        handler.NextResult();
    });
public static class SqlHelper
{
    /// <summary>
    /// Creates an initial DbCommand object based on a stored procedure name
    /// </summary>
    /// <param name="context">target database context</param>
    /// <param name="storedProcName">target procedure name</param>
    /// <param name="prependDefaultSchema">Prepend the default schema name to <paramref name="storedProcName"/> if explicitly defined in <paramref name="context"/></param>
    /// <returns></returns>
    public static DbCommand LoadStoredProc(this DbContext context, string storedProcName, bool prependDefaultSchema = true)
    {
        var cmd = context.Database.GetDbConnection().CreateCommand();
        if (prependDefaultSchema)
        {
            var schemaName = context.Model.Relational().DefaultSchema;
            if (schemaName != null)
            {
                storedProcName = $"{schemaName}.{storedProcName}";
            }

        }
        cmd.CommandText = storedProcName;
        cmd.CommandType = System.Data.CommandType.StoredProcedure;
        return cmd;
    }

    /// <summary>
    /// Creates a DbParameter object and adds it to a DbCommand
    /// </summary>
    /// <param name="cmd"></param>
    /// <param name="paramName"></param>
    /// <param name="paramValue"></param>
    /// <returns></returns>
    public static DbCommand WithSqlParam(this DbCommand cmd, string paramName, object paramValue, Action<DbParameter> configureParam = null)
    {
        if (string.IsNullOrEmpty(cmd.CommandText) && cmd.CommandType != System.Data.CommandType.StoredProcedure)
            throw new InvalidOperationException("Call LoadStoredProc before using this method");

        var param = cmd.CreateParameter();
        param.ParameterName = paramName;
        param.Value = (paramValue != null ? paramValue : DBNull.Value);
        configureParam?.Invoke(param);
        cmd.Parameters.Add(param);
        return cmd;
    }

    /// <summary>
    /// Creates a DbParameter object and adds it to a DbCommand
    /// </summary>
    /// <param name="cmd"></param>
    /// <param name="paramName"></param>
    /// <param name="paramValue"></param>
    /// <returns></returns>
    public static DbCommand WithSqlParam(this DbCommand cmd, string paramName, Action<DbParameter> configureParam = null)
    {
        if (string.IsNullOrEmpty(cmd.CommandText) && cmd.CommandType != System.Data.CommandType.StoredProcedure)
            throw new InvalidOperationException("Call LoadStoredProc before using this method");

        var param = cmd.CreateParameter();
        param.ParameterName = paramName;
        configureParam?.Invoke(param);
        cmd.Parameters.Add(param);
        return cmd;
    }

    /// <summary>
    /// Creates a DbParameter object based on the SqlParameter and adds it to a DbCommand.
    /// This enabled the ability to provide custom types for SQL-parameters.
    /// </summary>
    /// <param name="cmd"></param>
    /// <param name="paramName"></param>
    /// <param name="paramValue"></param>
    /// <returns></returns>
    public static DbCommand WithSqlParam(this DbCommand cmd, string paramName, SqlParameter parameter)
    {
        if (string.IsNullOrEmpty(cmd.CommandText) && cmd.CommandType != System.Data.CommandType.StoredProcedure)
            throw new InvalidOperationException("Call LoadStoredProc before using this method");

        //var param = cmd.CreateParameter();
        //param.ParameterName = paramName;
        //configureParam?.Invoke(param);
        cmd.Parameters.Add(parameter);

        return cmd;
    }

    public class SprocResults
    {

        //  private DbCommand _command;
        private DbDataReader _reader;

        public SprocResults(DbDataReader reader)
        {
            // _command = command;
            _reader = reader;
        }

        public IList<T> ReadToList<T>()
        {
            return MapToList<T>(_reader);
        }

        public T? ReadToValue<T>() where T : struct
        {
            return MapToValue<T>(_reader);
        }

        public Task<bool> NextResultAsync()
        {
            return _reader.NextResultAsync();
        }

        public Task<bool> NextResultAsync(CancellationToken ct)
        {
            return _reader.NextResultAsync(ct);
        }

        public bool NextResult()
        {
            return _reader.NextResult();
        }

        /// <summary>
        /// Retrieves the column values from the stored procedure and maps them to <typeparamref name="T"/>'s properties
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="dr"></param>
        /// <returns>IList<<typeparamref name="T"/>></returns>
        private IList<T> MapToList<T>(DbDataReader dr)
        {
            var objList = new List<T>();
            var props = typeof(T).GetRuntimeProperties().ToList();

            var colMapping = dr.GetColumnSchema()
                .Where(x => props.Any(y => y.Name.ToLower() == x.ColumnName.ToLower()))
                .ToDictionary(key => key.ColumnName.ToLower());

            if (dr.HasRows)
            {
                while (dr.Read())
                {
                    T obj = Activator.CreateInstance<T>();
                    foreach (var prop in props)
                    {
                        if (colMapping.ContainsKey(prop.Name.ToLower()))
                        {
                            var column = colMapping[prop.Name.ToLower()];

                            if (column?.ColumnOrdinal != null)
                            {
                                var val = dr.GetValue(column.ColumnOrdinal.Value);
                                prop.SetValue(obj, val == DBNull.Value ? null : val);
                            }

                        }
                    }
                    objList.Add(obj);
                }
            }
            return objList;
        }

        /// <summary>
        /// Attempts to read the first value of the first row of the resultset.
        /// </summary>
        private T? MapToValue<T>(DbDataReader dr) where T : struct
        {
            if (dr.HasRows)
            {
                if (dr.Read())
                {
                    return dr.IsDBNull(0) ? new T?() : new T?(dr.GetFieldValue<T>(0));
                }
            }
            return new T?();
        }
    }

    /// <summary>
    /// Executes a DbDataReader and returns a list of mapped column values to the properties of <typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="command"></param>
    /// <returns></returns>
    public static void ExecuteStoredProc(this DbCommand command, Action<SprocResults> handleResults, System.Data.CommandBehavior commandBehaviour = System.Data.CommandBehavior.Default, bool manageConnection = true)
    {
        if (handleResults == null)
        {
            throw new ArgumentNullException(nameof(handleResults));
        }

        using (command)
        {
            if (manageConnection && command.Connection.State == System.Data.ConnectionState.Closed)
                command.Connection.Open();
            try
            {
                using (var reader = command.ExecuteReader(commandBehaviour))
                {
                    var sprocResults = new SprocResults(reader);
                    handleResults(sprocResults);
                }
            }
            finally
            {
                if (manageConnection)
                {
                    command.Connection.Close();
                }
            }
        }
    }

    /// <summary>
    /// Executes a DbDataReader asynchronously and returns a list of mapped column values to the properties of <typeparamref name="T"/>.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="command"></param>
    /// <returns></returns>
    public async static Task ExecuteStoredProcAsync(this DbCommand command, Action<SprocResults> handleResults, System.Data.CommandBehavior commandBehaviour = System.Data.CommandBehavior.Default, CancellationToken ct = default(CancellationToken), bool manageConnection = true)
    {
        if (handleResults == null)
        {
            throw new ArgumentNullException(nameof(handleResults));
        }

        using (command)
        {
            if (manageConnection && command.Connection.State == System.Data.ConnectionState.Closed)
                await command.Connection.OpenAsync(ct).ConfigureAwait(false);
            try
            {
                using (var reader = await command.ExecuteReaderAsync(commandBehaviour, ct).ConfigureAwait(false))
                {
                    var sprocResults = new SprocResults(reader);
                    handleResults(sprocResults);
                }
            }
            finally
            {
                if (manageConnection)
                {
                    command.Connection.Close();
                }
            }
        }
    }
}
sjh37 commented 1 year ago

The solution was to move

if (IsSqlParameterNull(isApplicationCompleteParam))
    isApplicationComplete = null;
else
    isApplicationComplete = (bool) isApplicationCompleteParam.Value;

to the bottom of the function like so:

public CheckIfApplicationIsCompleteReturnModel CheckIfApplicationIsComplete(int? applicationId, out bool? isApplicationComplete)
{
    var applicationIdParam = new SqlParameter { ParameterName = "@ApplicationId", SqlDbType = SqlDbType.Int, Direction = ParameterDirection.Input, Value = applicationId.GetValueOrDefault(), Precision = 10, Scale = 0 };
    if (!applicationId.HasValue)
        applicationIdParam.Value = DBNull.Value;

    var isApplicationCompleteParam = new SqlParameter { ParameterName = "@IsApplicationComplete", SqlDbType = SqlDbType.Bit, Direction = ParameterDirection.Output };

    var procResultData = new CheckIfApplicationIsCompleteReturnModel();
    var cmd = Database.Connection.CreateCommand();
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.CommandText = "[dbo].[CheckIfApplicationIsComplete]";
    cmd.Parameters.Add(applicationIdParam);
    cmd.Parameters.Add(isApplicationCompleteParam);

    try
    {
        DbInterception.Dispatch.Connection.Open(Database.Connection, new DbInterceptionContext());
        var reader = cmd.ExecuteReader();
        var objectContext = ((IObjectContextAdapter) this).ObjectContext;

        procResultData.ResultSet1 = objectContext.Translate<CheckIfApplicationIsCompleteReturnModel.ResultSetModel1>(reader).ToList();
        reader.NextResult();

        procResultData.ResultSet2 = objectContext.Translate<CheckIfApplicationIsCompleteReturnModel.ResultSetModel2>(reader).ToList();
        reader.Close();
    }
    finally
    {
        DbInterception.Dispatch.Connection.Close(Database.Connection, new DbInterceptionContext());
    }

    if (IsSqlParameterNull(isApplicationCompleteParam))
        isApplicationComplete = null;
    else
        isApplicationComplete = (bool) isApplicationCompleteParam.Value;

    return procResultData;
}
sjh37 commented 1 year ago

This issue has been resolved and will be in the next release.