efcore icon indicating copy to clipboard operation
efcore copied to clipboard

FromSql: Support multiple resultsets

Open Eilon opened this issue 8 years ago • 60 comments

From @rpundlik on March 22, 2017 12:9

While retrieving the results using stored procedure how can I retrieve and store multiple result set in view model in .net core

For e.g. from stored procedure I am returning records for below queries

Select * LMS_Survey
Select * from LMS_SurveyQuestion
Select * from LMS_SurveyQuestionOptionChoice

and below is view model

public class LMS_SurveyTraineeViewModel
{
    public LMS_SurveyDetailsViewModel SurveyDetailsViewModel { get; set; }
    public LMS_SurveyQuestionsViewModel SurveyQuestionsViewModel { get; set; }
    public LMS_SurveyQuestionOptionChoiceViewModel SurveyQuestionOptionChoiceViewModel { get; set; }
}

This is how I am executing the stored procedure

public List<LMS_SurveyTraineeViewModel> GetTraineeSurvey(int surveyID)
        {
            try
            {
                List<LMS_SurveyTraineeViewModel> modelList = new List<LMS_SurveyTraineeViewModel>();

                modelList = dbcontext.Set<LMS_SurveyTraineeViewModel>().FromSql("LMSSP_GetTraineeSurvey @surveyID = {0},@LanguageID = {1}", surveyID, AppTenant.SelectedLanguageID).ToList();

                return modelList;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

How can stored the multiple result set using stored procedure in view model ?

Right now I need to call the three separate stored procedures to get data and merging them into one view model

Thanks for the help !

Copied from original issue: aspnet/Mvc#6011

Eilon avatar Apr 11 '17 16:04 Eilon

I think this question is more about retrieving multiple result sets from a sproc, and less so about MVC view models.

Eilon avatar Apr 11 '17 16:04 Eilon

@Eilon it looks there was an issue raised in the past #6026 but it was closed for some reason. Is there any current plan to include this feature in the 2.0 milestone.

Ricky-G avatar Apr 12 '17 10:04 Ricky-G

@RickyG-Akl , the friendly folks in this repo will give you an answer 😄

Eilon avatar Apr 12 '17 16:04 Eilon

@rpundlik @RickyG-Akl There isn't currently any support for this when using EF and FromSql. The workaround is to drop down to ADO.NET and use the DbConnection directly to run the query and handle the multiple resultsets. Putting this issue on the backlog so that we can consider adding support in the future.

ajcvickers avatar Apr 12 '17 19:04 ajcvickers

@ajcvickers In EF6 we have ObjectContext.Translate api, which we could use in this kind of scenario. There it was possible to materialize SqlDataReader into the EF entity. However this is missing in EF Core 2.0. The issue has been raised in #4675. We use this feature extensively and without this we won't be able to migrate from EF6 to EF Core.

ajayvikas avatar Aug 29 '17 05:08 ajayvikas

@ppn2, I am curious about how you are using Translate(). Can you confirm if you need to have readers with multiple results?

divega avatar Aug 29 '17 06:08 divega

Generally when the entities are simple for example order, orderdetails etc kind, we don't need to use Translate. The include() method works just fine. There are many cases where we need to return a complex object graph from the stored procedure. Generally this stored procedure will have complex business rules and will return multiple resultsets (in our case about 15-20 resultsets). In these cases we run the ExecuteReader command and load SqlDataReader and using translate apis to dematerialize the entities one by one.

ajayvikas avatar Sep 01 '17 18:09 ajayvikas

@ajcvickers, @divega By looking at past responses and the source code, we came up with the following procedure which is equivalent to the EF6 ObjectContext.Translate (We could extend it to include the merge options, similar to EF6 Translate provides). We have tested this code and seems to be working. We do not have deep knowledge of he source code, so our confidence level is not very high. Is it possible for somebody with deeper knowledge to review this code? This will really help us to move forward with the migration to EF Core 2.0.

        public static List<T> Translate<T>(this DbSet<T> set, DbDataReader reader) where T : class
        {
            var entityList = new List<T>();
            if (reader == null || reader.HasRows == false) return entityList;
            var entityType = set.GetService<IModel>().FindEntityType(typeof(T));
            var valueBufferParameter = Expression.Parameter(typeof(ValueBuffer));
            var entityMaterializerSource = set.GetService<IEntityMaterializerSource>();
            var valueBufferFactory = set.GetService<IRelationalValueBufferFactoryFactory>().Create(new[] { typeof(T) }, null);
            var stateManager = set.GetService<IStateManager>() as StateManager;
            Func<ValueBuffer, T> materializer = Expression.Lambda<Func<ValueBuffer, T>>(
                    entityMaterializerSource.CreateMaterializeExpression(entityType, valueBufferParameter), valueBufferParameter)
                .Compile();
            stateManager.BeginTrackingQuery();
            while (reader.Read())
            {
                ValueBuffer valueBuffer = valueBufferFactory.Create(reader);
                var entity = materializer.Invoke(valueBuffer);
                var entry = stateManager.StartTrackingFromQuery(entityType, entity, valueBuffer, null);
                entityList.Add((T)entry.Entity);
            }
            return entityList;
        }

The way to use this code will be

            using (var ctx = new ApplicationDbContext())
            {
                using (var cnn = ctx.Database.GetDbConnection())
                {
                    var cmm = cnn.CreateCommand();
                    cmm.CommandType = System.Data.CommandType.Text;
                    cmm.CommandText = "SELECT AccountId, AccountBalance, AccountName FROM Accounts; SELECT CustomerId, AccountId, CustomerName FROM Customers";
                    cmm.Connection = cnn;
                    cnn.Open();
                    using (var reader = cmm.ExecuteReader())
                    {
                        var accounts = ctx.Accounts.Translate(reader);
                        reader.NextResult();
                        var customers = ctx.Customers.Translate(reader);
                    }
                }
            }

ajayvikas avatar Sep 20 '17 20:09 ajayvikas

@sjh37 FYI!

ErikEJ avatar Sep 21 '17 05:09 ErikEJ

Multiple result sets from a sproc are supported via the Entity Framework Reverse Poco Generator. This generates code for EF6, which you could copy and put into your EF.Core project. Download at https://marketplace.visualstudio.com/items?itemName=SimonHughes.EntityFrameworkReversePOCOGenerator

sjh37 avatar Sep 21 '17 13:09 sjh37

@sjh37 Notice that ObjectContext is not present in EF Core

ErikEJ avatar Sep 21 '17 15:09 ErikEJ

@ppn2 We looked through your code in triage and we think it should be okay for simple cases. Some things to keep in mind:

  • The code makes use of "internal" APIs, which means that it could be broken any time a new EF version is released. Use internal surface at your own risk
  • Some places it may not work include
    • Places where the column order may not be what is expected; for example, FromSql has special code to handle column ordering
    • Inheritance between entity types and TPH mapping
    • There is no identity resolution, so if an entity with a given ID is already tracked, then this looks like it will fail. (You might look into QueryBuffer for this.)

Overall, if you are in full control of the types, the queries, and the state of the context, then this should work, but as a general solution it will fall down in some places.

ajcvickers avatar Sep 22 '17 22:09 ajcvickers

@sjh37 . On your comment that we can use the multiple resultsets from stored proceudure in an EF core project, did you mean that as part of that hack :

  1. We can add the EF6 nuget to the EF core project and generate the context
  2. USe the generated context from reverse poco instead of the Context from Core project?

srini1978 avatar Jan 02 '18 05:01 srini1978

@srini1978 I thought it would of been a simple task to generate the code using the generator, then copy n paste the stored proc and result set class to your ef core db context. However I didn't realise at the time that ObjectContext is not present in EF Core.

sjh37 avatar Jan 02 '18 11:01 sjh37

Is there any update on this? can I use EF Core and FromSql to get multiple result set using stored procedure in model?

anildbest83 avatar Aug 10 '18 12:08 anildbest83

@anildbest83 This issue is in the Backlog milestone. We haven't done any work on it yet and that it is currently not planned for for the 2.2 or 3.0 release. We will re-assess the backlog following the 3.0 release and consider this item at that time. However, keep in mind that there are many other high priority features with which it will be competing for resources.

ajcvickers avatar Aug 10 '18 16:08 ajcvickers

This is a big deal for us because we have very complex load SPs that do a lot of calculating/coalescing to figure out what records need to be included. This work can only be done once and we can return from 10-30 tables in one load SP. I attempted the code written by @ppn2 above but it did not work in the latest EF version (not overly surprised considering the Internals warning). I attempted to take a different approach to load and I was able to get it to work by simply taking data (ADO.NET) from the database and manually creating records and then attaching them to the context. This worked fine and would potentially work for us since we code gen a lot so we would know what tables are where with what columns so code gening this load logic wouldn't be terrible even though it is not ideal as EF should have this capability built in. I have no idea what the performance cost is for this as it wires up all relationship on attach. I do batch AttachRange hoping that would be better but I don't know. Once the context was loaded all entity access would then be done using the Local cache only.

Obviously this is not a generic solution that will work for any entity type automatically but at least on the surface seems like a possibility. Wanted to share to hopfully up priority on this issue as well as get any feedback on this approach.

spudcud avatar Sep 11 '18 13:09 spudcud

Somehow I managed to tackle my situation using the code written by @ppn2, thanks

anildbest83 avatar Sep 12 '18 04:09 anildbest83

@anildbest83 What did you do to get it to work? Can you post a snippet?

spudcud avatar Sep 12 '18 15:09 spudcud

Something like this

Used SqlHerper.cs class (attached)

SqlHerpler.txt

and cosnume it like this

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

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

                      usersSql.AddRange( handler.ReadToList<Users3>());
                      handler.NextResult();
                });`

anildbest83 avatar Sep 13 '18 07:09 anildbest83

I was wondering if anyone had managed to wrangle a decent workaround for this in EF Core 2.1. I've been attempting to dig into the internals of EF Core to figure out how to materialize and track an entity using a DbDataReader, but I get lost pretty quickly with what's going on.

As @spudcud pointed out, it looks like the workaround provided by @ppn2 no longer works for EF Core 2.1. I also attempted to hook up a workaround from #4675 by @jnm2, but see an identical problem there, too.

The crux of it is that I receive an exception System.InvalidOperationException: 'No mapping to a relational type can be found for the CLR type 'MyType'.' when attempting to call Create(new[] { typeof(TEntity) }, null) on the resolved IRelationalValueBufferFactoryFactory service resolved from my DbSet<TEntity>. I assume there used to be mappings for the DbContext types in that service (or its dependencies), but that may no longer be the case.

I guess two real questions holding me up are:

  1. Is there a way to resolve a service or create a type IRelationalValueBufferFactory from CLR type to be used for the Create function on the IRelationalValueBufferFactoryFactory implementation?
  2. Once that IRelationalValueBufferFactory is created/resolved, how can I avoid using the obsolete IRelationalValueBufferFactory.Create(valueTypes, indexMap) function in favor of the IRelationalValueBufferFactory.Create(types) function?

We have a rather large solution that I am working to convert, and this is one of the last hurdles preventing me from completely migrating to EF Core. Any guidance is greatly appreciated. Thanks!

mscappini avatar Nov 02 '18 20:11 mscappini

Crickets chirping...

Same here, @mscappini. It never occurred to me that a mature product like EF Core 2.x would not have any means by which to return a stored procedure returned recordset back to the caller. Moving on to ADO.NET, I guess, per @ajcvickers's guidance.

BTW, why is this issue titled "Support multiple resultsets"? Best I can tell, EF doesn't even really support single resultsets. (I am disregarding solutions such as this that look painful and awkward.)

I very much look forward to the version of EF that not only supports stored procedures, but can import them into projects automatically, as edmx did in the past. Until then, I guess I either live with ancient versions of EF or use some other tool, because I use stored procedures extensively. Am I that unusual?

WellspringCS avatar Nov 14 '18 01:11 WellspringCS

@WellspringCS This issue is titled "Support multiple resultsets" because that's what it's about. Returning single result sets is supported for query types and entity types..

ajcvickers avatar Nov 14 '18 16:11 ajcvickers

Following this thread because we we're looking for a way to have EF.Core handling SPs returning multiple resultsets, and ended going with Dapper, which supports it nicely, on a ASP.Net Core project.

albertosilva avatar Nov 14 '18 17:11 albertosilva

Apologies if I came off rudely. I was (and am) frustrated, but I never forget that these tools are free. I'm thankful for so much of what Microsoft does.

I said what I said because after a day or two of conversion work, I came down to code like this...


        public virtual int spCopyQuestionColumns(Nullable<System.Guid> fromQuestionGuid, Nullable<System.Guid> toQuestionGuid)
        {
            var fromQuestionGuidParameter = fromQuestionGuid.HasValue ?
                new SqlParameter("FromQuestionGuid", fromQuestionGuid) :
                new SqlParameter("FromQuestionGuid", typeof(System.Guid));

            var toQuestionGuidParameter = toQuestionGuid.HasValue ?
                new SqlParameter("ToQuestionGuid", toQuestionGuid) :
                new SqlParameter("ToQuestionGuid", typeof(System.Guid));

            return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction("spCopyQuestionColumns", fromQuestionGuidParameter, toQuestionGuidParameter);
        }

Tons of it... dozens of routines. All used to be autogenerated by EF edmx. A few seconds and hey presto, you're ready to rock.

I naively assumed that by 2.x EF would have a suitable replacement for that feature. No.

OK.... sigh... so.... I'll convert it by hand. But soon enough I find that lines like this...

return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction("spCopyQuestionColumns", fromQuestionGuidParameter, toQuestionGuidParameter);

...have no counterpart in Core 2.x. The only solution that I found (this being a single-resultset stored procedure, you'll note) was to do something like this (which I've neither run nor tested yet, because my project doesn't even compile yet.

        public virtual List<int> spUnusedCategories(Nullable<int> surveyId, Nullable<int> interviewId)
        {
            var surveyIdParameter = surveyId.HasValue ?
                new SqlParameter("SurveyId", surveyId) :
                new SqlParameter("SurveyId", typeof(int));

            var interviewIdParameter = interviewId.HasValue ?
                new SqlParameter("InterviewId", interviewId) :
                new SqlParameter("InterviewId", typeof(int));
            this.Database.OpenConnection();
            DbCommand cmd = this.Database.GetDbConnection().CreateCommand();
            cmd.CommandText = "spUnusedCategories";
            cmd.Parameters.Add(surveyIdParameter);
            cmd.Parameters.Add(interviewIdParameter);
            using (DbDataReader dr = cmd.ExecuteReader())
            {
                DataTable tbl = new DataTable();
                tbl.Load(dr);
                DataView dv = new DataView(tbl);
                var enumer = dv.ToTable().AsEnumerable().Select(r => System.Convert.ToInt32(r[0])).ToList();
                return enumer;
            }

So for a couple dozen routines, I find myself painstakingly converting each routine... not even knowing how much debugging and fixing will be needed when I can at least run the code.

If I understood your comment, @ajcvickers, you seem to be referring to solutions for single-resultsets that fall along the lines of this...

var students = context.Students.FromSql("GetStudents @p0","Bill").ToList(); and if the resultset doesn't exactly match an existing entity, I'm outta luck.

I could be revealing ignorance, but that doesn't look like a good solution to me. It feels like a hack.

All that said, I am very new to much of this, I totally accept that I'm may be going about this conversion completely the wrong way. But from what I've seen online... I cannot easily use .NET Core with anything like a large pile of stored procedures... (such as I have!) unless I want to create perfect matching entities for each by hand and then code up magic strings (slight exaggeration) to execute them.

I'm still thankful for all the tools, but I'm thinking I may not be able to use EF in the new world. Not yet. If I can, please just point me to a good link and I'll study the material I find there.

WellspringCS avatar Nov 15 '18 05:11 WellspringCS

@WellspringCS You can use QueryTypes to define the output of your SQL/SP/TVF, so you use an actual Entity if and only if the resultset is an exact match. Otherwise, just declare a QueryType matching the output and you are set.

Regarding multiple active resultsets, it's not that there is no support in EFCore, the fact is that there is no official way to materialize the output of a multiple resultsets query. Moreover, some functionalities (i.e. navigation properties) could not work as expected or at all. All in all, at least in my experience, this is not an issue: when dealing with multiple result sets there is no need for tracking, navigation properties and such things

The following code materializes the contents of a DbDataReader into an IEnumerable<T>

        public static IEnumerable<T> Materialize<T>(this DbContext dbContext, DbDataReader reader)
        {
            var materializerSource = dbContext.GetService<IEntityMaterializerSource>();
            var type = typeof(T);
            var entityType = dbContext.GetService<IModel>()
                                      .FindEntityType(type);
            if (entityType == null)
                throw new InvalidOperationException(string.Format("Unable to find a an entity type (or query type) matching '{0}'", type));
            var materializationAction = materializerSource.GetMaterializer(entityType);
            var relationalTypeMappingSource = dbContext.GetService<IRelationalTypeMappingSource>();
            var columns = Enumerable.Range(0, reader.FieldCount)
                                    .Select(x => new
                                                 {
                                                         Index = x,
                                                         Name = reader.GetName(x)
                                                 })
                                    .ToList();

            var propertiesTypeMaterializationInfo = entityType.GetProperties()
                                                              .Select(x =>
                                                                      {
                                                                          var column = columns.FirstOrDefault(y => string.Equals(y.Name,
                                                                                                                                 x.Relational()
                                                                                                                                  ?.ColumnName ?? x.Name)) ?? throw new InvalidOperationException(string.Format("Unable to find a column mapping property '{0}'.", x.Name));
                                                                          return new TypeMaterializationInfo(x.PropertyInfo.PropertyType, x, relationalTypeMappingSource, column.Index);
                                                                      })
                                                              .ToArray();
            var valueBufferFactory = dbContext.GetService<IRelationalValueBufferFactoryFactory>()
                                              .Create(propertiesTypeMaterializationInfo);

            while (reader.Read())
            {
                var valueBuffer = valueBufferFactory.Create(reader);
                yield return (T)materializationAction(new MaterializationContext(valueBuffer, dbContext));
            }
        }

A possible usage looks like this

            var optionsBuilder = new DbContextOptionsBuilder<TestDbContext>().UseSqlServer("Server=localhost;Database=Playground;Trusted_Connection=True;MultipleActiveResultSets=true");

            using (var dbContext = new TestDbContext(optionsBuilder.Options))
            {
                var connection = dbContext.Database.GetDbConnection();
                using (var command = connection.CreateCommand())
                {
                    command.CommandText = @"SELECT f.*
  FROM (VALUES (1, 'One')
             , (2, 'Two')
             , (3, 'Three')
             , (4, 'Four')
             , (5, 'Five')) f (Id, Name);

SELECT s.*
  FROM (VALUES (1, 'ChildOfOne_1', 1)
             , (2, 'ChildOfOne_2', 1)
             , (3, 'ChildOfThree_1', 3)
             , (4, 'ChildOfFive_1', 5)
             , (5, 'ChildOfFour_1', 4)) s (Id, Name, ParentId);

SELECT t.*
  FROM (VALUES (1, 'ChildOfChildOfOne_1_1', 1)
             , (2, 'ChildOfChildOfOne_2_1', 2)
             , (3, 'ChildOfChildOfThree_1_1', 3)
             , (4, 'ChildOfChildOfFive_1_1', 4)
             , (5, 'ChildOfChildOfFive_1_2', 4)) t (Id, Name, ParentId);";

                    if (connection.State == ConnectionState.Closed)
                        connection.Open();
                    
                    using (var reader = command.ExecuteReader())
                    {
                        var firstEntities = dbContext.Materialize<FirstResultSetEntity>(reader)
                                                     .ToList();
                        reader.NextResult();
                        var secondEntities = dbContext.Materialize<SecondResultSetEntity>(reader)
                                                      .ToList();
                        reader.NextResult();
                        var thirdEntities = dbContext.Materialize<ThirdResultSetEntity>(reader)
                                                     .ToList();
                    }
                }
            }

where resultset types have been defined as Query types in the model

    public class FirstResultSetEntity
    {
        public int Id { get; set; }

        public string Name { get; set; }
    }

    public class SecondResultSetEntity
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public int ParentId { get; set; }
    }

    public class ThirdResultSetEntity
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public int ParentId { get; set; }
    }

    public class TestDbContext : DbContext
    {
        public TestDbContext(DbContextOptions<TestDbContext> options)
                : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Query<FirstResultSetEntity>();
            modelBuilder.Query<SecondResultSetEntity>();
            modelBuilder.Query<ThirdResultSetEntity>();
        }
    }

Notice that you can get creative with extensions, and hide the connection/command boilerplate, or even try to create code looking like FromSql, mimicking the SqlParameter naming resolution logic

        public static (IReadOnlyCollection<T1> FirstResultSet, IReadOnlyCollection<T2> SecondResultSet, IReadOnlyCollection<T3> ThirdResultSet) MultiResultSetsFromSql<T1, T2, T3>(this DbContext dbContext, RawSqlString sql, params object[] parameters)
        {
            var resultSetMappingTypes = new[]
                                        {
                                                typeof(T1), typeof(T2), typeof(T3)
                                        };

            var resultSets = new List<object>();

            var connection = dbContext.Database.GetDbConnection();
            var parameterGenerator = dbContext.GetService<IParameterNameGeneratorFactory>()
                                              .Create();
            var parameterBuilder = dbContext.GetService<IRelationalCommandBuilderFactory>()
                                            .Create()
                                            .ParameterBuilder;
            foreach (var parameter in parameters)
            {
                var generatedName = parameterGenerator.GenerateNext();
                if (parameter is DbParameter dbParameter)
                    parameterBuilder.AddRawParameter(generatedName, dbParameter);
                else
                    parameterBuilder.AddParameter(generatedName, generatedName);
            }

            using (var command = connection.CreateCommand())
            {
                command.CommandType = CommandType.Text;
                command.CommandText = sql.Format;
                command.Connection = connection;
                for (var i = 0; i < parameterBuilder.Parameters.Count; i++)
                {
                    var relationalParameter = parameterBuilder.Parameters[i];
                    relationalParameter.AddDbParameter(command, parameters[i]);
                }

                var materializerSource = dbContext.GetService<IEntityMaterializerSource>();
                if (connection.State == ConnectionState.Closed)
                    connection.Open();

                using (var reader = command.ExecuteReader())
                {
                    foreach (var pair in resultSetMappingTypes.Select((x, i) => (Index: i, Type: x)))
                    {
                        var i = pair.Index;
                        var resultSetMappingType = pair.Type;
                        if (i > 0 && !reader.NextResult())
                            throw new InvalidOperationException(string.Format("No result set at index {0}, unable to map to {1}.", i, resultSetMappingType));

                        var type = resultSetMappingType;
                        var entityType = dbContext.GetService<IModel>()
                                                  .FindEntityType(type);
                        if (entityType == null)
                            throw new InvalidOperationException(string.Format("Unable to find a an entity type (or query type) matching '{0}'", type));
                        var materializationAction = materializerSource.GetMaterializer(entityType);
                        var relationalTypeMappingSource = dbContext.GetService<IRelationalTypeMappingSource>();
                        var columns = Enumerable.Range(0, reader.FieldCount)
                                                .Select(x => new
                                                             {
                                                                     Index = x,
                                                                     Name = reader.GetName(x)
                                                             })
                                                .ToList();

                        var propertiesTypeMaterializationInfo = entityType.GetProperties()
                                                                          .Select(x =>
                                                                                  {
                                                                                      var column = columns.FirstOrDefault(y => string.Equals(y.Name,
                                                                                                                                             x.Relational()
                                                                                                                                              ?.ColumnName ?? x.Name)) ?? throw new InvalidOperationException(string.Format("Unable to find a column mapping property '{0}'.", x.Name));
                                                                                      return new TypeMaterializationInfo(x.PropertyInfo.PropertyType, x, relationalTypeMappingSource, column.Index);
                                                                                  })
                                                                          .ToArray();
                        var valueBufferFactory = dbContext.GetService<IRelationalValueBufferFactoryFactory>()
                                                          .Create(propertiesTypeMaterializationInfo);

                        var resultSetValues = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(type));
                        while (reader.Read())
                        {
                            var valueBuffer = valueBufferFactory.Create(reader);
                            resultSetValues.Add(materializationAction(new MaterializationContext(valueBuffer, dbContext)));
                        }

                        resultSets.Add(resultSetValues);
                    }
                }
            }

            return ((IReadOnlyCollection<T1>)resultSets[0], (IReadOnlyCollection<T2>)resultSets[1], (IReadOnlyCollection<T3>)resultSets[2]);
        }

A possible usage is like this

var (firstResultSet, secondResultSet, thirdResultSet) = dbContext.MultiResultSetsFromSql<FirstResultSetEntity, SecondResultSetEntity, ThirdResultSetEntity>(SQL);

(This extension expects 3 resultsets... you can create more generic versions if a different number of resultsets is expected).

Even in this case, the materialization relies on internal/infrastructure services, so it could break with future version of EFCore.

BladeWise avatar Nov 19 '18 11:11 BladeWise

@BladeWise Yesssss, thank you VERY much for the excellent post! The entity materialization from TypeMaterializationInfo[] was a missing piece of the puzzle and illuminated quite a bit for me. I think @ppn2 had posted a working solution for a previous version of EF Core (I presently receive deprecated messages from that solution while working in EF Core 2.1.4).

I was able to use information from solution posted by @BladeWise and information from the solution posted by @ppn2 to derive a solution that works for me.

I was able to get entities materialized and use navigational properties with proxy types using the following solution.

public static IEnumerable<T> Materialize<T>(this DbContext dbContext, DbDataReader reader)
    where T : class
{
    var materializerSource = dbContext.GetService<IEntityMaterializerSource>();
    var type = typeof(T);
    var entityType = dbContext.GetService<IModel>()
                              .FindEntityType(type);
    if (entityType == null)
        throw new InvalidOperationException(string.Format("Unable to find a an entity type (or query type) matching '{0}'", type));
    var materializationAction = materializerSource.GetMaterializer(entityType);
    var relationalTypeMappingSource = dbContext.GetService<IRelationalTypeMappingSource>();
    var columns = Enumerable.Range(0, reader.FieldCount)
                            .Select(x => new
                            {
                                Index = x,
                                Name = reader.GetName(x)
                            })
                            .ToList();

    var propertiesTypeMaterializationInfo = entityType.GetProperties()
                                                      .Select(x => {
                                                          var column = columns.FirstOrDefault(y => string.Equals(y.Name, x.Relational()?.ColumnName ?? x.Name, StringComparison.InvariantCultureIgnoreCase))
                                                          ?? throw new InvalidOperationException(string.Format("Unable to find a column mapping property '{0}'.", x.Name));
                                                          return new TypeMaterializationInfo(x.PropertyInfo.PropertyType, x, relationalTypeMappingSource, column.Index);
                                                      })
                                                      .ToArray();

    var valueBufferFactory = dbContext.GetService<IRelationalValueBufferFactoryFactory>()
                                      .Create(propertiesTypeMaterializationInfo);

    var stateManager = dbContext.Set<T>().GetService<IStateManager>() as StateManager;

    stateManager.BeginTrackingQuery();

    while (reader.Read())
    {
        var valueBuffer = valueBufferFactory.Create(reader);
        T entity = (T)materializationAction(new MaterializationContext(valueBuffer, dbContext));
        stateManager.StartTrackingFromQuery(entityType, entity, valueBuffer, null);
        yield return entity;
    }
}

Thanks very much for the contributions!

I believe @ajcvickers had mentioned it before, but I'm not entirely sure this will work everywhere and may fall down in some places. Though it seems to work well enough for me!

mscappini avatar Nov 19 '18 19:11 mscappini

Hello, Support of multiple result set using SP is rectified in EF Core 2.2??

ravi-darshankar avatar Dec 10 '18 12:12 ravi-darshankar

@ravi-darshankar This issue is in the Backlog milestone. This means that it is not going to happen for the 2.2 or 3.0 release. We will re-assess the backlog following the 3.0 release and consider this item at that time. However, keep in mind that there are many other high priority features with which it will be competing for resources.

ajcvickers avatar Dec 10 '18 19:12 ajcvickers

Hi

With reflections and data-readers , we could archive this. Following is the link to Git Repo

https://github.com/nilendrat/EfCoreMultipleResults

Hope this will be of help.

nilendrat avatar Jan 23 '19 13:01 nilendrat