I am dealing with creating billings that uses a fairly massive amount of data (2+ million records overall), so I was forced to use some direct SQL to speed up the data loading as using pure EF was too slow.
Using this entity:
[Keyless]
public class BillingAggregate
{
public List<Customer> Customers { get; set; }
public List<Debt> Debts { get; set; }
public List<Payment> Payments { get; set; }
public List<Credit> Credits { get; set; }
}
Added to the DataContext as:
public DbSet<BillingAggregate> BillingAggregate { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<BillingAggregate>().ToSqlQuery("EXEC [dto].[GetForMonthlyBilling]");
}
With my Stored Procedure as:
CREATE PROCEDURE [dbo].[GetForMonthlyBilling]
AS
BEGIN
SET NOCOUNT ON;
CREATE TABLE #TempCustomerIDsForBilling([CustomerId] bigint)
INSERT INTO #TempCustomerIDsForBilling([CustomerId])
SELECT DISTINCT [CustomerId]
FROM [dbo].[Debt] debt
INNER JOIN [dbo].[DebtType] debtType ON debt.[DebtTypeId] = debtType.[DebtTypeId]
WHERE debtType.[IsCollectible] = 1
SELECT * FROM [dbo].[Customer] WHERE [CustomerId] IN (SELECT [CustomerId] FROM #TempCustomerIDsForBilling)
SELECT * FROM [dbo].[Debt] WHERE [CustomerId] IN (SELECT [CustomerId] FROM #TempCustomerIDsForBilling)
SELECT * FROM [dbo].[Payment] WHERE [CustomerId] IN (SELECT [CustomerId] FROM #TempCustomerIDsForBilling)
SELECT *
FROM [dbo].[Credit] credit
INNER JOIN [dbo].[Debt] debt ON credit.[DebtId] = debt.[DebtId]
WHERE [CustomerId] IN (SELECT [CustomerId] FROM #TempCustomerIDsForBilling)
DROP TABLE #TempCustomerIDsForBilling
END
GO
This seems to be all that the documentation that I was able to find requires me to do... however, when I do a standard query:
var billingAggregate = await dbContext.BillingAggregate.FirstOrDefaultAsync();
It immediately throws the error:
{"Sequence contains no elements"}
The immediacy of the error makes me think that the Stored Procedure fails to even run, as running it in SQL alone takes 40+ seconds... what am I missing?
For whomever this may help, as per Svyatoslav Danyliv's link, it seems this is currently not possible with EF Core (though, was possible with EF 6). As a workaround, I had to drop down to a lower lever API that EF Core uses. I created it as an Extension Method so that I never have to see this "garbage" again :)
public static class DbContextExtensions
{
public static async Task<List<BillingHelper>> GetBillingsAsync(this MyDataContext dbContext)
{
var customers = new List<Customer>();
var customerAddresses = new List<CustomerAddress>();
var debts = new List<Debt>();
var payments = new List<Payment>();
var credits = new List<Credit>();
using (var command = dbContext.Database.GetDbConnection().CreateCommand())
{
command.CommandText = "[dbo].[GetForMonthlyBilling]";
command.CommandType = System.Data.CommandType.StoredProcedure;
if (command.Connection.State != System.Data.ConnectionState.Open)
command.Connection.Open();
using (var reader = await command.ExecuteReaderAsync())
{
if (reader != null && reader.HasRows)
{
var customerType = typeof(Customer);
var customerProperties = customerType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
var customerDictionary = customerProperties.ToDictionary(property => property.Name.ToUpper(), property => property);
while (await reader.ReadAsync())
{
var customer = new Customer();
for (int index = 0; index < reader.FieldCount; index++)
{
var currentColumnName = reader.GetName(index).ToUpper();
if (customerDictionary.ContainsKey(currentColumnName))
{
var property = customerDictionary[currentColumnName];
if ((property != null) && property.CanWrite)
{
var value = reader.GetValue(index);
property.SetValue(claimant, (value == DBNull.Value ? null : value), null);
}
}
}
customers.Add(claimant);
}
// This loads the next "SELECT", that is Result Set
await reader.NextResultAsync();
var customerAddressType = typeof(CustomerAddress);
// ... similar as above for Customer
await reader.NextResultAsync();
var debtType = typeof(Debt);
// ... similar as above for Customer
await reader.NextResultAsync();
// ...
}
}
}
// From here to the end of this method takes C# compiler a long time to do
// opportunity for optimization perhaps.
var billings = customers.Select(customer => new BillingHelper
{
CustomerId = customer.CustomerId,
Customer = customer,
Debts = debts.Where(x => x.CustomerId == customer.CustomerId).ToList(),
Payments = payments.Where(x => x.CustomerId == customer.CustomerId).ToList()
}).ToList();
foreach (var billing in billings)
{
var debtIds = billing.Debts.Select(x => x.DebtId);
billing.Credits = credits.Where(x => debtIds.Contains(x.DebtId)).ToList();
billing.Customer.Addresses = customerAddresses.Where(x => x.CustomerId == billing.CustomerId).ToList();
}
return billings;
}
}
public class Branch
{
[Sortable(OrderBy = "BranchId")]
public long BranchId { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string Type { get; set; }
}
this is my Model class and I also create a custom attribute
public class SortableAttribute : Attribute
{
public string OrderBy { get; set; }
}
now i create a pagination with orderby descending but this code not working
public static async Task<IPagedList<T>> ToPagedListAsync<T>(this IQueryable<T> source,
GeneralPagingRequest pagingRequest, int indexFrom = 0,
CancellationToken cancellationToken = default(CancellationToken))
{
if (indexFrom > pagingRequest.PageNumber)
{
throw new ArgumentException(
$"indexFrom: {indexFrom} > pageNumber: {pagingRequest.PageNumber}, must indexFrom <= pageNumber");
}
var count = await source.CountAsync(cancellationToken).ConfigureAwait(false);
var items = source.Skip(((pagingRequest.PageNumber - 1) - indexFrom) * pagingRequest.PageSize)
.Take(pagingRequest.PageSize);
var props = typeof(T).GetProperties();
PropertyInfo orderByProperty;
orderByProperty =
props.FirstOrDefault(x=>x.GetCustomAttributes(typeof(SortableAttribute), true).Length != 0);
if (pagingRequest.OrderBy == "desc")
{
items = items.OrderBy(x => orderByProperty.GetValue(x));
}
else
{
items = items.OrderBy(x => orderByProperty.GetValue(x));
}
var result = await items.ToListAsync(cancellationToken).ConfigureAwait(false);
var pagedList = new PagedList<T>
{
PageNumber = pagingRequest.PageNumber,
PageSize = pagingRequest.PageSize,
IndexFrom = indexFrom,
TotalCount = count,
Items = result,
TotalPages = (int) Math.Ceiling(count / (double) pagingRequest.PageSize)
};
return pagedList;
}
but the result variable create exception
.OrderBy() requires a delegate that would tell it HOW to select a key, not the key value itself. So you are looking at some meta-programming here.
Naturally, you will look at building a dynamic LINQ Expression tree that will fetch a property for you:
// your code up above
PropertyInfo orderByProperty = props.FirstOrDefault(x => x.GetCustomAttributes(typeof(SortableAttribute), true).Length != 0);
var p = Expression.Parameter(typeof(T), "x"); // you define your delegate parameter here
var accessor = Expression.Property(p, orderByProperty.GetMethod); // this basically becomes your `x => x.BranchId` construct
var predicate = Expression.Lambda(accessor, p).Compile(); // here's our problem: as we don't know resulting type at compile time we can't go `Expression.Lambda<T, long>(accessor, p)` here
if (pagingRequest.OrderBy == "desc")
{
items = items.OrderByDescending(x => predicate(x)); // passing a Delegate here will not work as OrderBy requires Func<T, TKey>
}
else
{
items = items.OrderBy(x => predicate(x)); // passing a Delegate here will not work as OrderBy requires Func<T, TKey>
}
var result = await items.ToListAsync(cancellationToken).ConfigureAwait(false);
// your code down below
Problem with the above code - you don't know TKey upfront. Therefore we will have to go a level deeper and build the whole items.OrderBy(x => x.BranchId) expression dynamically. The biggest leap of faith here will be the fact the OrderBy is an extension method and it actually resides on IQueryable type. After you've got the generic method reference, you will need to build a specific delegate type when you know your property type. So your method becomes something like this:
public static class ExtToPagedListAsync
{
private static readonly MethodInfo OrderByMethod = typeof(Queryable).GetMethods().Single(method => method.Name == "OrderBy" && method.GetParameters().Length == 2); // you need your method reference, might as well find it once
private static readonly MethodInfo OrderByDescendingMethod = typeof(Queryable).GetMethods().Single(method => method.Name == "OrderByDescending" && method.GetParameters().Length == 2); // you need your method reference, might as well find it once
public static async Task<IPagedList<T>> ToPagedListAsync<T>(this IQueryable<T> source, GeneralPagingRequest pagingRequest, int indexFrom = 0, CancellationToken cancellationToken = default(CancellationToken))
{
if (indexFrom > pagingRequest.PageNumber)
{
throw new ArgumentException(
$"indexFrom: {indexFrom} > pageNumber: {pagingRequest.PageNumber}, must indexFrom <= pageNumber");
}
var count = await source.CountAsync(cancellationToken).ConfigureAwait(false);
var items = source.Skip(((pagingRequest.PageNumber - 1) - indexFrom) * pagingRequest.PageSize)
.Take(pagingRequest.PageSize);
var props = typeof(T).GetProperties();
PropertyInfo orderByProperty = props.FirstOrDefault(x => x.GetCustomAttributes(typeof(SortableAttribute), true).Length != 0);
var p = Expression.Parameter(typeof(T), "x");
var accessor = Expression.Property(p, orderByProperty.GetMethod);
var predicate = Expression.Lambda(accessor, p); // notice, we're not yet compiling the predicate. we still want an Expression here
// grab the correct method depending on your condition
MethodInfo genericMethod = (pagingRequest.OrderBy == "desc") ? OrderByDescendingMethod.MakeGenericMethod(typeof(T), orderByProperty.PropertyType)
:OrderByMethod.MakeGenericMethod(typeof(T), orderByProperty.PropertyType);
object ret = genericMethod.Invoke(null, new object[] { items, predicate });
items = (IQueryable<T>)ret; // finally cast it back to Queryable with your known T
var result = await items.ToListAsync(cancellationToken).ConfigureAwait(false);
var pagedList = new PagedList<T>
{
PageNumber = pagingRequest.PageNumber,
PageSize = pagingRequest.PageSize,
IndexFrom = indexFrom,
TotalCount = count,
Items = result,
TotalPages = (int)Math.Ceiling(count / (double)pagingRequest.PageSize)
};
return pagedList;
}
}
I must disclose I did get some inspiration from this answer here, so do check it out for further reading.
I have two entities (TaskStep and Role) with many to many relation, I use a view model (named StepViewModel) to display taskstep and it's allowed Roles, StepViewModel is :
public class StepViewModel
{
public StepViewModel()
{
}
public TaskStep TaskStep { get; set; }
public virtual ICollection<AllowedRole> AllowedRole { get; set; }
}
and I use this method to display taskstep:
public static StepViewModel GetTaskStepData(int stepId)
{
using (var context = new WFContext())
{
return context.TaskSteps.Where(ts => ts.ID == stepId).Include(ts => ts.Roles)
.Select(ts => new StepViewModel()
{
AllowedRole = context.Roles.Select(r => new AllowedRole()
{
Role = r,
Allowed = ts.Roles.Select(x => x.ID).Contains(r.ID)
}).ToList(),
TaskStep = ts
})
.SingleOrDefault();
}
}
but when I save the taskstep, it's roles changes don't save. My save method is :
public static void SaveTaskStep(StepViewModel stepViewModel)
{
using (var context = new WorkFlowContext())
{
//TaskStep taskStep = context.TaskSteps.Where(ts => ts.ID == stepViewModel.TaskStep.ID).Include(ts => ts.Roles).SingleOrDefault();
TaskStep taskStep = stepViewModel.TaskStep;
List<int> aRolesIDs = stepViewModel.AllowedRole.Where(ar => ar.Allowed == true).Select(ar => ar.Role.ID).ToList();
List<Role> roles = context.Roles.Where(r => aRolesIDs.Contains(r.ID)).Include(r => r.AllowedTaskSteps).ToList();
taskStep.Roles.Clear();
foreach (Role role in roles)
{
if (context.Entry(role).State == EntityState.Detached)
context.Roles.Attach(role);
taskStep.Roles.Add(role);
}
if (taskStep.ID == 0)
context.TaskSteps.Add(taskStep);
else
context.Entry(taskStep).State = EntityState.Modified;
context.SaveChanges();
}
}
if I get taskstep from db with include roles (like first -comment- line in save method), it work's well. but in this way, I have to make all taskstep field's changes manually. In addition when I assign taskstep to stepviewmodel,I use "include" but it's still doesn't work.what can I do to save stepViewModel roles?
context.TaskSteps.Attach(taskStep) should work in your case
SaveTaskStep method:
using (var context = new WorkFlowContext())
{
TaskStep taskStep = stepViewModel.TaskStep;
if (taskStep.ID == 0)
context.TaskSteps.Add(taskStep);
else
context.TaskSteps.Attach(taskStep); //from this point EF start change tracking
//I just copy-pasted this part your code assuming it's correct
List<int> aRolesIDs = stepViewModel.AllowedRole.Where(ar => ar.Allowed == true).Select(ar => ar.Role.ID).ToList();
List<Role> roles = context.Roles.Where(r => aRolesIDs.Contains(r.ID)).Include(r => r.AllowedTaskSteps).ToList();
taskStep.Roles.Clear();
foreach (Role role in roles)
{
if (context.Entry(role).State == EntityState.Detached)
context.Roles.Attach(role);
taskStep.Roles.Add(role);
}
context.SaveChanges();
} // end of using (var context
I am not sure how to fit EF into my business logic tests. Let me give an example of how it works at runtime (no testing, regular application run):
Context.Set<T>.Add(instance);
When I add the entity using the above generic method, an instance is added to context, and EF fixes all the navigation properties behind the scenes. For example, if exists [instance.Parent] property, and [parent.Instances] collection property (1-to-many relationship), EF will automatically add the instance to parent.Instances collection behind the scenes.
My code depends on the [parent.Instances] collection, and if it is empty, it will fail. When I am writing unit tests using MS testing framework, how can I reuse the power of EF, so it can still do its behind-the-scenes job, but uaing the memory as data storage, and not the actual database? I am not really interested whether EF successfully added, modified or deleted something in the database, I am just interested in getting the EF magic on the in-memory sets.
I've been doing this with a mock DbContext and mock DbSet that I've created. They store test data in memory and allow you to do most of the standard things you can do on a DbSet.
Your code that acquires the DbContext initially will have to be changed so that it acquires a MockDbContext when it is running under unit test. You can determine if you are running under MSTest with the following code:
public static bool IsInUnitTest
{
get
{
return AppDomain.CurrentDomain.GetAssemblies()
.Any(assembly =>
assembly.FullName.StartsWith(
"Microsoft.VisualStudio.QualityTools.UnitTestFramework"));
}
}
Here is the code for MockDbContext:
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication5
{
// ProductionDbContext would be DbContext class
// generated by Entity Framework
public class MockDbContext: ProductionDbContext
{
public MockDbContext()
{
LoadFakeData();
}
// Entities (for which we'll provide MockDbSet implementation
// and test data)
public override DbSet<Account> Accounts { get; set; }
public override DbSet<AccountGenLink> AccountGenLinks { get; set; }
public override DbSet<AccountPermit> AccountPermits { get; set; }
public override DbSet<AcctDocGenLink> AcctDocGenLinks { get; set; }
// DbContext method overrides
private int InternalSaveChanges()
{
// Just return 0 in the mock
return 0;
}
public override int SaveChanges()
{
return InternalSaveChanges();
}
public override Task<int> SaveChangesAsync()
{
return Task.FromResult(InternalSaveChanges());
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken)
{
// Just ignore the cancellation token in the mock
return SaveChangesAsync();
}
private void LoadFakeData()
{
// Tables
Accounts = new MockDbSet<Account>(this);
Accounts.AddRange(new List<Account>
{
new Account
{
SSN_EIN = "123456789", CODE = "A", accttype = "CD",
acctnumber = "1", pending = false, BankOfficer1 = string.Empty,
BankOfficer2 = null, Branch = 0, type = "18", drm_rate_code = "18",
officer_code = string.Empty, open_date = new DateTime(2010, 6, 8),
maturity_date = new DateTime(2010, 11, 8), HostAcctActive = true,
EffectiveAcctStatus = "A"
},
new Account
{
SSN_EIN = "123456789", CODE = "A", accttype = "DD",
acctnumber = "00001234", pending = false, BankOfficer1 = "BCK",
BankOfficer2 = string.Empty, Branch = 0, type = "05", drm_rate_code = "00",
officer_code = "DJT", open_date = new DateTime(1998, 9, 14),
maturity_date = null, HostAcctActive = true,
EffectiveAcctStatus = "A"
},
new Account
{
SSN_EIN = "123456789", CODE = "A", accttype = "LN", acctnumber = "1",
pending = false, BankOfficer1 = "LMP", BankOfficer2 = string.Empty,
Branch = 0, type = "7", drm_rate_code = null, officer_code = string.Empty,
open_date = new DateTime(2001, 10, 24),
maturity_date = new DateTime(2008, 5, 2), HostAcctActive = true,
EffectiveAcctStatus = "A"
}
});
AccountGenLinks = new MockDbSet<AccountGenLink>(this);
AccountGenLinks.AddRange(new List<AccountGenLink>
{
// Add your test data here if needed
});
AccountPermits = new MockDbSet<AccountPermit>(this);
AccountPermits.AddRange(new List<AccountPermit>
{
// Add your test data here if needed
});
AcctDocLinks = new MockDbSet<AcctDocLink>(this);
AcctDocLinks.AddRange(new List<AcctDocLink>
{
new AcctDocLink { ID = 1, SSN_EIN = "123456789", CODE = "A", accttype = "DD",
acctnumber = "00001234", DocID = 50, DocType = 5 },
new AcctDocLink { ID = 25, SSN_EIN = "123456789", CODE = "6", accttype = "CD",
acctnumber = "1", DocID = 6750, DocType = 5 }
});
}
}
}
And here is the code for MockDbSet:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication5
{
public sealed class MockDbSet<TEntity> : DbSet<TEntity>, IQueryable,
IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity> where TEntity : class
{
public MockDbSet(MockDbContext context)
{
// Get entity set for entity
// Used when we figure out whether to generate
// IDENTITY values
EntitySet = ((IObjectContextAdapter) context).ObjectContext
.MetadataWorkspace
.GetItems<EntityContainer>(DataSpace.SSpace).First()
.BaseEntitySets
.FirstOrDefault(item => item.Name == typeof(TEntity).Name);
Data = new ObservableCollection<TEntity>();
Query = Data.AsQueryable();
}
private ObservableCollection<TEntity> Data { get; set; }
Type IQueryable.ElementType
{
get { return Query.ElementType; }
}
private EntitySetBase EntitySet { get; set; }
Expression IQueryable.Expression
{
get { return Query.Expression; }
}
IEnumerator IEnumerable.GetEnumerator()
{
return Data.GetEnumerator();
}
public override ObservableCollection<TEntity> Local
{
get { return Data; }
}
IQueryProvider IQueryable.Provider
{
get { return new MockDbAsyncQueryProvider<TEntity>(Query.Provider); }
}
private IQueryable Query { get; set; }
public override TEntity Add(TEntity entity)
{
GenerateIdentityColumnValues(entity);
Data.Add(entity);
return entity;
}
public override IEnumerable<TEntity> AddRange(IEnumerable<TEntity> entities)
{
foreach (var entity in entities)
Add(entity);
return entities;
}
public override TEntity Attach(TEntity entity)
{
return Add(entity);
}
public override TEntity Create()
{
return Activator.CreateInstance<TEntity>();
}
public override TDerivedEntity Create<TDerivedEntity>()
{
return Activator.CreateInstance<TDerivedEntity>();
}
public override TEntity Find(params object[] keyValues)
{
throw new NotSupportedException();
}
public override Task<TEntity> FindAsync(params object[] keyValues)
{
return FindAsync(CancellationToken.None, keyValues);
}
public override Task<TEntity> FindAsync(CancellationToken cancellationToken, params object[] keyValues)
{
throw new NotSupportedException();
}
private void GenerateIdentityColumnValues(TEntity entity)
{
// The purpose of this method, which is called when adding a row,
// is to ensure that Identity column values are properly initialized
// before performing the add. If we were making a "real" Entity Framework
// Add() call, this task would be handled by the data provider and the
// value(s) would then be propagated back into the entity. In the case
// of this mock, there is nothing that will do that, so we have to make
// this at-least token effort to ensure the columns are properly initialized.
// In SQL Server, an Identity column can be of one of the following
// data types: tinyint, smallint, int, bigint, decimal (with a scale of 0),
// or numeric (with a scale of 0); This method handles the integer types
// (the others are typically not used).
foreach (var member in EntitySet.ElementType.Members.ToList())
{
if (member.IsStoreGeneratedIdentity)
{
// OK, we've got a live one; do our thing.
//
// Note that we'll get the current value of the column and,
// if it is nonzero, we'll leave it alone. We do this because
// the test data in our mock DbContext provides values for the
// Identity columns and many of those values are foreign keys
// in other entities (where we also provide test data). We don't
// want to disturb any existing relationships defined in the test data.
Type columnDataType = null;
foreach (var metadataProperty in member.TypeUsage.EdmType.MetadataProperties.ToList())
{
if (metadataProperty.Name != "PrimitiveTypeKind")
continue;
switch ((PrimitiveTypeKind)metadataProperty.Value)
{
case PrimitiveTypeKind.SByte:
columnDataType = typeof(SByte);
break;
case PrimitiveTypeKind.Int16:
columnDataType = typeof(Int16);
break;
case PrimitiveTypeKind.Int32:
columnDataType = typeof(Int32);
break;
case PrimitiveTypeKind.Int64:
columnDataType = typeof(Int64);
break;
default:
throw new InvalidOperationException();
}
var identityColumnGetter = entity.GetType().GetProperty(member.Name).GetGetMethod();
var identityColumnSetter = entity.GetType().GetProperty(member.Name).GetSetMethod();
Int64 specifiedColumnValue = 0;
switch (columnDataType.Name)
{
case "SByte":
specifiedColumnValue = (SByte)identityColumnGetter.Invoke(entity, null);
break;
case "Int16":
specifiedColumnValue = (Int16)identityColumnGetter.Invoke(entity, null);
break;
case "Int32":
specifiedColumnValue = (Int32)identityColumnGetter.Invoke(entity, null);
break;
case "Int64":
specifiedColumnValue = (Int64)identityColumnGetter.Invoke(entity, null);
break;
}
if (specifiedColumnValue != 0)
break;
Int64 maxExistingColumnValue = 0;
switch (columnDataType.Name)
{
case "SByte":
foreach (var item in Local.ToList())
maxExistingColumnValue = Math.Max(maxExistingColumnValue, (SByte)identityColumnGetter.Invoke(item, null));
identityColumnSetter.Invoke(entity, new object[] { (SByte)(++maxExistingColumnValue) });
break;
case "Int16":
foreach (var item in Local.ToList())
maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Int16)identityColumnGetter.Invoke(item, null));
identityColumnSetter.Invoke(entity, new object[] { (Int16)(++maxExistingColumnValue) });
break;
case "Int32":
foreach (var item in Local.ToList())
maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Int32)identityColumnGetter.Invoke(item, null));
identityColumnSetter.Invoke(entity, new object[] { (Int32)(++maxExistingColumnValue) });
break;
case "Int64":
foreach (var item in Local.ToList())
maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Int64)identityColumnGetter.Invoke(item, null));
identityColumnSetter.Invoke(entity, new object[] { (Int64)(++maxExistingColumnValue) });
break;
}
}
}
}
}
IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
{
return new MockDbAsyncEnumerator<TEntity>(Data.GetEnumerator());
}
IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
{
return Data.GetEnumerator();
}
public override TEntity Remove(TEntity entity)
{
Data.Remove(entity);
return entity;
}
public override IEnumerable<TEntity> RemoveRange(IEnumerable<TEntity> entities)
{
foreach (var entity in entities)
Remove(entity);
return entities;
}
public override DbSqlQuery<TEntity> SqlQuery(string sql, params object[] parameters)
{
throw new NotSupportedException();
}
}
internal class MockDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
{
internal MockDbAsyncQueryProvider(IQueryProvider queryProvider)
{
QueryProvider = queryProvider;
}
private IQueryProvider QueryProvider { get; set; }
public IQueryable CreateQuery(Expression expression)
{
return new MockDbAsyncEnumerable<TEntity>(expression);
}
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
return new MockDbAsyncEnumerable<TElement>(expression);
}
public object Execute(Expression expression)
{
return QueryProvider.Execute(expression);
}
public TResult Execute<TResult>(Expression expression)
{
return QueryProvider.Execute<TResult>(expression);
}
public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken)
{
return Task.FromResult(Execute(expression));
}
public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
{
return Task.FromResult(Execute<TResult>(expression));
}
}
internal class MockDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
{
public MockDbAsyncEnumerable(IEnumerable<T> enumerable)
: base(enumerable)
{
}
public MockDbAsyncEnumerable(Expression expression)
: base(expression)
{
}
IQueryProvider IQueryable.Provider
{
get { return new MockDbAsyncQueryProvider<T>(this); }
}
public IDbAsyncEnumerator<T> GetAsyncEnumerator()
{
return new MockDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
}
IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
{
return GetAsyncEnumerator();
}
}
internal class MockDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
{
public MockDbAsyncEnumerator(IEnumerator<T> enumerator)
{
Enumerator = enumerator;
}
public void Dispose()
{
Enumerator.Dispose();
}
public T Current
{
get { return Enumerator.Current; }
}
object IDbAsyncEnumerator.Current
{
get { return Current; }
}
private IEnumerator<T> Enumerator { get; set; }
public Task<bool> MoveNextAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Enumerator.MoveNext());
}
}
}
If you are using the EntityFramework-Reverse-POCO-Code-First-Generator from Simon Hughes, with its generated FakeContext, Jeff Prince's approach is still possible with some tweaking. The little difference here is we are using the partial class support and implementing the InitializePartial() methods in the FakeContext and FakeDbSet. The bigger difference is that the Reverse POCO FakeContext does not inherit from a DbContext, so we can't easily get the MetadataWorkspace to know which columns are identities. The answer is to create a 'real' context with a bogus connection string and use that to get the EntitySetBase for the FakeDbSet. This should be pasted inside the proper namespace of a new source file, renaming the context, and you shouldn't need to do anything further in the rest of your project.
/// <summary>
/// This code will set Identity columns to be unique. It behaves differently from the real context in that the
/// identities are generated on add, not save. This is inspired by https://stackoverflow.com/a/31795273/1185620 and
/// modified for use with the FakeDbSet and FakeContext that can be generated by EntityFramework-Reverse-POCO-Code-
/// First-Generator from Simon Hughes.
///
/// Aside from changing the name of the FakeContext and the type used to in its InitializePartial() as
/// the 'realContext' this file can be pasted into another namespace for a completely unrelated context. If you
/// have additional implementation for the InitializePartial methods in the FakeContext or FakeDbSet, change the
/// name to InitializePartial2 and they will be called after InitializePartial is called here. Please don't add
/// code unrelated to the above purpose to this file - make another file to further extend the partial class.
/// </summary>
partial class FakeFooBarBazContext
{
/// <summary> Initialization of FakeContext to handle setting an identity for columns marked as
/// <c>IsStoreGeneratedIdentity</c> when an item is Added to the DbSet. If this signature
/// conflicts with another partial class, change that signature to implement
/// <see cref="InitializePartial2"/>, as that will be called when this is complete. </summary>
partial void InitializePartial()
{
// Here we need to get a 'real' ObjectContext so we can get the metadata for determining
// identity columns. Since FakeContext doesn't inherit from DbContext, create
// the real one with a bogus connection string.
using (var realContext = new FooBarBazContext("Server=."))
{
var objectContext = (realContext as IObjectContextAdapter).ObjectContext;
// Reflect over the public properties that return DbSet<> and get it. If it is
// of type FakeDbSet<>, call InitializeWithContext() on it.
var fakeDbSetGenericType = typeof(FakeDbSet<>);
var dbSetGenericType = typeof(DbSet<>);
var properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in properties)
{
if (!prop.PropertyType.IsGenericType || prop.GetMethod == null)
continue;
if (prop.PropertyType.GetGenericTypeDefinition() != dbSetGenericType)
continue;
var dbSetObj = prop.GetMethod.Invoke(this, null);
var dbSetObjType = dbSetObj?.GetType();
if (dbSetObjType?.GetGenericTypeDefinition() != fakeDbSetGenericType)
continue;
var initMethod = dbSetObjType.GetMethod(nameof(FakeDbSet<object>.InitializeWithContext),
BindingFlags.NonPublic | BindingFlags.Instance,
null, new[] {typeof(ObjectContext)}, new ParameterModifier[] { });
initMethod.Invoke(dbSetObj, new object[] {objectContext});
}
}
InitializePartial2();
}
partial void InitializePartial2();
}
partial class FakeDbSet<TEntity>
{
private EntitySetBase EntitySet { get; set; }
/// <summary> Initialization of FakeDbSet to handle setting an identity for columns marked as
/// <c>IsStoreGeneratedIdentity</c> when an item is Added to the DbSet. If this signature
/// conflicts with another partial class, change that signature to implement
/// <see cref="InitializePartial2"/>, as that will be called when this is complete. </summary>
partial void InitializePartial()
{
// The only way we know something was added to the DbSet from this partial class
// is to hook the CollectionChanged event.
_data.CollectionChanged += DataOnCollectionChanged;
InitializePartial2();
}
internal void InitializeWithContext(ObjectContext objectContext)
{
// Get entity set for entity. Used when we figure out whether to generate IDENTITY values
EntitySet = objectContext
.MetadataWorkspace
.GetItems<EntityContainer>(DataSpace.SSpace).First()
.BaseEntitySets
.FirstOrDefault(item => item.Name == typeof(TEntity).Name);
}
private void DataOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Add)
return;
foreach (TEntity entity in e.NewItems)
GenerateIdentityColumnValues(entity);
}
/// <summary> The purpose of this method, which is called after a row is added, is to ensure that Identity column values are
/// properly initialized. If this was a real Entity Framework, this task would be handled by the data provider
/// when SaveChanges[Async]() is called and the value(s) would then be propagated back into the entity.
/// In the case of FakeDbSet, there is nothing that will do that, so we have to make this at-least token effort
/// to ensure the columns are properly initialized, even if it is done at the incorrect time.
/// </summary>
private void GenerateIdentityColumnValues(TEntity entity)
{
foreach (var member in EntitySet.ElementType.Members)
{
if (!member.IsStoreGeneratedIdentity)
continue;
foreach (var metadataProperty in member.TypeUsage.EdmType.MetadataProperties)
{
if (metadataProperty.Name != "PrimitiveTypeKind")
continue;
var entityProperty = entity.GetType().GetProperty(member.Name);
var identityColumnGetter = entityProperty.GetGetMethod();
// Note that we'll get the current value of the column and,
// if it is nonzero, we'll leave it alone. We do this because
// the test data in our mock DbContext provides values for the
// Identity columns and many of those values are foreign keys
// in other entities (where we also provide test data). We don't
// want to disturb any existing relationships defined in the test data.
bool isDefaultForType;
var columnType = (PrimitiveTypeKind)metadataProperty.Value;
switch (columnType)
{
case PrimitiveTypeKind.SByte:
isDefaultForType = default(SByte) == (SByte)identityColumnGetter.Invoke(entity, null);
break;
case PrimitiveTypeKind.Int16:
isDefaultForType = default(Int16) == (Int16)identityColumnGetter.Invoke(entity, null);
break;
case PrimitiveTypeKind.Int32:
isDefaultForType = default(Int32) == (Int32)identityColumnGetter.Invoke(entity, null);
break;
case PrimitiveTypeKind.Int64:
isDefaultForType = default(Int64) == (Int64)identityColumnGetter.Invoke(entity, null);
break;
case PrimitiveTypeKind.Decimal:
isDefaultForType = default(Decimal) == (Decimal)identityColumnGetter.Invoke(entity, null);
break;
default:
// In SQL Server, an Identity column can be of one of the following data types:
// tinyint (SqlByte, byte), smallint (SqlInt16, Int16), int (SqlInt32, Int32),
// bigint (SqlInt64, Int64), decimal (with a scale of 0) (SqlDecimal, Decimal),
// or numeric (with a scale of 0) (SqlDecimal, Decimal). Those are handled above.
// 'If we don't know, we throw'
throw new InvalidOperationException($"Unsupported Identity Column Type {columnType}");
}
// From this point on, we can return from the method, as only one identity column is
// possible per table and we found it.
if (!isDefaultForType)
return;
var identityColumnSetter = entityProperty.GetSetMethod();
lock (Local)
{
switch (columnType)
{
case PrimitiveTypeKind.SByte:
{
SByte maxExistingColumnValue = 0;
foreach (var item in Local.ToList())
maxExistingColumnValue = Math.Max(maxExistingColumnValue, (SByte) identityColumnGetter.Invoke(item, null));
identityColumnSetter.Invoke(entity, new object[] {(SByte) (++maxExistingColumnValue)});
return;
}
case PrimitiveTypeKind.Int16:
{
Int16 maxExistingColumnValue = 0;
foreach (var item in Local.ToList())
maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Int16) identityColumnGetter.Invoke(item, null));
identityColumnSetter.Invoke(entity, new object[] {(Int16) (++maxExistingColumnValue)});
return;
}
case PrimitiveTypeKind.Int32:
{
Int32 maxExistingColumnValue = 0;
foreach (var item in Local.ToList())
maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Int32) identityColumnGetter.Invoke(item, null));
identityColumnSetter.Invoke(entity, new object[] {(Int32) (++maxExistingColumnValue)});
return;
}
case PrimitiveTypeKind.Int64:
{
Int64 maxExistingColumnValue = 0;
foreach (var item in Local.ToList())
maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Int64) identityColumnGetter.Invoke(item, null));
identityColumnSetter.Invoke(entity, new object[] {(Int64) (++maxExistingColumnValue)});
return;
}
case PrimitiveTypeKind.Decimal:
{
Decimal maxExistingColumnValue = 0;
foreach (var item in Local.ToList())
maxExistingColumnValue = Math.Max(maxExistingColumnValue, (Decimal) identityColumnGetter.Invoke(item, null));
identityColumnSetter.Invoke(entity, new object[] {(Decimal) (++maxExistingColumnValue)});
return;
}
}
}
}
}
}
partial void InitializePartial2();
}