_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_
I think this question is more about retrieving multiple result sets from a sproc, and less so about MVC view models.
@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.
@RickyG-Akl , the friendly folks in this repo will give you an answer 馃槃
@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 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.
@ppn2, I am curious about how you are using Translate()
. Can you confirm if you need to have readers with multiple results?
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.
@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.
```C#
public static List
{
var entityList = new List
if (reader == null || reader.HasRows == false) return entityList;
var entityType = set.GetService
var valueBufferParameter = Expression.Parameter(typeof(ValueBuffer));
var entityMaterializerSource = set.GetService
var valueBufferFactory = set.GetService
var stateManager = set.GetService
Func
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
```C#
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);
}
}
}
@sjh37 FYI!
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 Notice that ObjectContext is not present in EF Core
@ppn2 We looked through your code in triage and we think it should be okay for simple cases. Some things to keep in mind:
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.
@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 :
@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.
Is there any update on this? can I use EF Core and FromSql to get multiple result set using stored procedure in model?
@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.
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.
Somehow I managed to tackle my situation using the code written by @ppn2, thanks
@anildbest83 What did you do to get it to work? Can you post a snippet?
Something like this
Used SqlHerper.cs class (attached)
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();
});`
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:
IRelationalValueBufferFactory
from CLR type to be used for the Create
function on the IRelationalValueBufferFactoryFactory
implementation?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!
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 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..
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.
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 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>
```c#
public static IEnumerable
{
var materializerSource = dbContext.GetService
var type = typeof(T);
var entityType = dbContext.GetService
.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
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
```c#
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
```c#
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
```c#
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
c#
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 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!
Hello,
Support of multiple result set using SP is rectified in EF Core 2.2??
@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.
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.
Can this be done in EF6? It sucks that ObjectContext.Translate cannot handle TPH types, but surely there's a way to do by writing custom code like this, since EF itself is able to do it for normal queries.
@triynko I'm not sure exactly which part of this issue you are referring to, but we don't plan to do any work in this area for EF6.
I'd really like to see support for multiple resultsets returned from stored procedures
Any update on this? Or do you have any good workaround for this issue?
@deepundneni This issue is in the Backlog milestone. This means that it is not planned for the next release (EF Core 5.0). We will re-assess the backlog following the this 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.
Potential workarounds are discussed above.
@ajcvickers I appreciate the ping, but I think you might have tagged the wrong person ;)
Upvote, if entity framework core could support something like this from dapper:
https://dapper-tutorial.net/querymultiple
Loading object graph to memory from multiple results sets instead of big cartesian product in one go.
Here is an update to @BladeWise to support EFCore 3.1 and TPH.
public static async Task<IList<IList>> MultiResultSetsFromSql(this DbContext dbContext, ICollection<Type> resultSetMappingTypes, string sql, params object[] parameters)
{
var resultSets = new List<IList>();
var connection = dbContext.Database.GetDbConnection();
var parameterGenerator = dbContext.GetService<IParameterNameGeneratorFactory>()
.Create();
var commandBuilder = dbContext.GetService<IRelationalCommandBuilderFactory>()
.Create();
foreach (var parameter in parameters)
{
var generatedName = parameterGenerator.GenerateNext();
if (parameter is DbParameter dbParameter)
commandBuilder.AddRawParameter(generatedName, dbParameter);
else
commandBuilder.AddParameter(generatedName, generatedName);
}
using var command = connection.CreateCommand();
command.CommandType = CommandType.Text;
command.CommandText = sql;
command.Connection = connection;
for (var i = 0; i < commandBuilder.Parameters.Count; i++)
{
var relationalParameter = commandBuilder.Parameters[i];
relationalParameter.AddDbParameter(command, parameters[i]);
}
var materializerSource = dbContext.GetService<IEntityMaterializerSource>();
if (connection.State == ConnectionState.Closed)
await connection.OpenAsync();
using var reader = await command.ExecuteReaderAsync();
foreach (var pair in resultSetMappingTypes.Select((x, i) => (Index: i, Type: x)))
{
var i = pair.Index;
var resultSetMappingType = pair.Type;
if (i > 0 && !(await reader.NextResultAsync()))
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 relationalTypeMappingSource = dbContext.GetService<IRelationalTypeMappingSource>();
var columns = Enumerable.Range(0, reader.FieldCount)
.Select(x => new
{
Index = x,
Name = reader.GetName(x)
})
.ToList();
var relationalValueBufferFactoryFactory = dbContext.GetService<IRelationalValueBufferFactoryFactory>();
int discriminatorIdx = -1;
var discriminatorProperty = entityType.GetDiscriminatorProperty();
var entityTypes = entityType.GetDerivedTypesInclusive();
var instanceTypeMapping = entityTypes.Select(et => new
{
EntityType = et,
Properties = et.GetProperties()
.Select(x =>
{
var column = columns.FirstOrDefault(y => string.Equals(y.Name,
x.GetColumnName() ?? x.Name, StringComparison.OrdinalIgnoreCase)) ?? throw new InvalidOperationException(string.Format("Unable to find a column mapping property '{0}'.", x.Name));
if (x == discriminatorProperty)
discriminatorIdx = column.Index;
return new TypeMaterializationInfo(x.PropertyInfo.PropertyType, x, relationalTypeMappingSource, column.Index);
})
.ToArray()
})
.Select(x => new
{
EntityType = x.EntityType,
Properties = x.Properties,
ValueBufferFactory = relationalValueBufferFactoryFactory.Create(x.Properties)
})
.ToDictionary(e => e.EntityType.GetDiscriminatorValue() ?? e.EntityType, e => e)
;
var resultSetValues = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(type));
while (await reader.ReadAsync())
{
var instanceInfo = discriminatorIdx < 0 ? instanceTypeMapping[entityType] : instanceTypeMapping[reader[discriminatorIdx]];
var valueBuffer = instanceInfo.ValueBufferFactory.Create(reader);
var materializationAction = materializerSource.GetMaterializer(instanceInfo.EntityType);
resultSetValues.Add(materializationAction(new MaterializationContext(valueBuffer, dbContext)));
}
resultSets.Add(resultSetValues);
}
return resultSets;
And the extension typed methods
public static async Task<(IReadOnlyCollection<T1> FirstResultSet, IReadOnlyCollection<T2> SecondResultSet)> MultiResultSetsFromSql<T1, T2>(this DbContext dbContext, string sql, params object[] parameters)
{
var resultSetMappingTypes = new[]
{
typeof(T1), typeof(T2)
};
var resultSets = await MultiResultSetsFromSql(dbContext, resultSetMappingTypes, sql, parameters);
return ((IReadOnlyCollection<T1>)resultSets[0], (IReadOnlyCollection<T2>)resultSets[1]);
}
public static async Task<(IReadOnlyCollection<T1> FirstResultSet, IReadOnlyCollection<T2> SecondResultSet, IReadOnlyCollection<T3> ThirdResultSet)> MultiResultSetsFromSql<T1, T2, T3>(this DbContext dbContext, string sql, params object[] parameters)
{
var resultSetMappingTypes = new[]
{
typeof(T1), typeof(T2), typeof(T3)
};
var resultSets = await MultiResultSetsFromSql(dbContext, resultSetMappingTypes, sql, parameters);
return ((IReadOnlyCollection<T1>)resultSets[0], (IReadOnlyCollection<T2>)resultSets[1], (IReadOnlyCollection<T3>)resultSets[2]);
}
Most helpful comment
I think this question is more about retrieving multiple result sets from a sproc, and less so about MVC view models.