Can you get the DbContext from a DbSet? - c#

In my application it is sometimes necessary to save 10,000 or more rows to the database in one operation. I've found that simply iterating and adding each item one at a time can take upwards of half an hour.
However, if I disable AutoDetectChangesEnabled it takes ~ 5 seconds (which is exactly what I want)
I'm trying to make an extension method called "AddRange" to DbSet which will disable AutoDetectChangesEnabled and then re-enable it upon completion.
public static void AddRange<TEntity>(this DbSet<TEntity> set, DbContext con, IEnumerable<TEntity> items) where TEntity : class
{
// Disable auto detect changes for speed
var detectChanges = con.Configuration.AutoDetectChangesEnabled;
try
{
con.Configuration.AutoDetectChangesEnabled = false;
foreach (var item in items)
{
set.Add(item);
}
}
finally
{
con.Configuration.AutoDetectChangesEnabled = detectChanges;
}
}
So, my question is: Is there a way to get the DbContext from a DbSet? I don't like making it a parameter - It feels like it should be unnecessary.

With Entity Framework Core (tested with Version 2.1) you can get the current context using
// DbSet<MyModel> myDbSet
var context = myDbSet.GetService<ICurrentDbContext>().Context;
How to get a DbContext from a DbSet in EntityFramework Core 2.0

Yes, you can get the DbContext from a DbSet<TEntity>, but the solution is reflection heavy. I have provided an example of how to do this below.
I tested the following code and it was able to successfully retrieve the DbContext instance from which the DbSet was generated. Please note that, although it does answer your question, there is almost certainly a better solution to your problem.
public static class HackyDbSetGetContextTrick
{
public static DbContext GetContext<TEntity>(this DbSet<TEntity> dbSet)
where TEntity: class
{
object internalSet = dbSet
.GetType()
.GetField("_internalSet",BindingFlags.NonPublic|BindingFlags.Instance)
.GetValue(dbSet);
object internalContext = internalSet
.GetType()
.BaseType
.GetField("_internalContext",BindingFlags.NonPublic|BindingFlags.Instance)
.GetValue(internalSet);
return (DbContext)internalContext
.GetType()
.GetProperty("Owner",BindingFlags.Instance|BindingFlags.Public)
.GetValue(internalContext,null);
}
}
Example usage:
using(var originalContextReference = new MyContext())
{
DbSet<MyObject> set = originalContextReference.Set<MyObject>();
DbContext retrievedContextReference = set.GetContext();
Debug.Assert(ReferenceEquals(retrievedContextReference,originalContextReference));
}
Explanation:
According to Reflector, DbSet<TEntity> has a private field _internalSet of type InternalSet<TEntity>. The type is internal to the EntityFramework dll. It inherits from InternalQuery<TElement> (where TEntity : TElement). InternalQuery<TElement> is also internal to the EntityFramework dll. It has a private field _internalContext of type InternalContext. InternalContext is also internal to EntityFramework. However, InternalContext exposes a public DbContext property called Owner. So, if you have a DbSet<TEntity>, you can get a reference to the DbContext owner, by accessing each of those properties reflectively and casting the final result to DbContext.
Update from #LoneyPixel
In EF7 there is a private field _context directly in the class the implements DbSet. It's not hard to expose this field publicly

Why are you doing this on the DbSet? Try doing it on the DbContext instead:
public static void AddRangeFast<T>(this DbContext context, IEnumerable<T> items) where T : class
{
var detectChanges = context.Configuration.AutoDetectChangesEnabled;
try
{
context.Configuration.AutoDetectChangesEnabled = false;
var set = context.Set<T>();
foreach (var item in items)
{
set.Add(item);
}
}
finally
{
context.Configuration.AutoDetectChangesEnabled = detectChanges;
}
}
Then using it is as simple as:
using (var db = new MyContext())
{
// slow add
db.MyObjects.Add(new MyObject { MyProperty = "My Value 1" });
// fast add
db.AddRangeFast(new[] {
new MyObject { MyProperty = "My Value 2" },
new MyObject { MyProperty = "My Value 3" },
});
db.SaveChanges();
}

maybe you could create a helper that disabled this for you and then just call the helper from within the AddRange method

Related

What is the correct flow of requests when working with multiple viewModels on EF Core/MVVM?

Futher into my studies on MVVM I found an issue I can't understand, and I couldn't find (rather, I think I couldn't word my Google searches correctly) information specific to this situation:
I have the following entities:
public class Sale : EntityBase
{
public ICollection<SaleItem> SaleItems {get;set;}
}
public class SaleItem : EntityBase
{
public Sale Sale {get;set;}
public Stock Stock {get;set;}
public TaxType TaxType {get;set;}
}
public class Stock : EntityBase
{
public TaxType TaxType {get;set;}
public ICollection<Sale> SaleStocks {get;set;}
}
public class EntityBase
{
[Key]
public int ID {get;set;}
}
public TaxType
{
[Key]
public int TaxCode {get;set;}
public string Description {get;set;
}
TaxType is seeded by the database migrations. I'm using MySQL.
From what I've read on what is the Alternate for AddorUpdate method in EF Core?, to record a new entry on my database, I should just call _context.Update(Sale) and _context.SaveChangesAsync().
However I still can't understand what am I doing wrong on a simple CRUD:
User is directed to a viewModel:
SaleViewModel.cs
private SaleItemStore _saleItemStore;
public Sale Sale {get;set;} = new();
public ObservableCollection<SaleItem> SaleItems {get;set;} = new();
public SaleViewModel(IServiceProvider serviceProvider)
{
_saleItemStore = serviceProvider.GetRequiredService<SaleItemStore>();
SaveSale = new SaveSaleCommand(this, serviceProvider);
}
public class SaveSaleCommand()
{
public SaveSaleCommand(SaleViewModel saleViewModel, IServiceProvidere serviceProvider)
{
_parentViewModel = saleViewModel;
_saleDataService = serviceProvider.GetRequiredService<SalaDataService>();
}
public Execute()
{
foreach (SaleItem saleItem in SaleItems)
{
Sale.SaleItems.Add(saleItem);
}
_saleDataService.AddOrUpdate(Sale sale);
}
}
On a different viewModel, the user can select, among other properties, the TaxType, from a dropdown combobox. The combobox's ItemsSource is bound to TaxTypesList like:
SelectStockToSaleItemViewModel
private TaxTypeDataService _taxTypeDataService;
public SaleItem SaleItem {get;set;} = new();
public TaxType SelectedTaxType {get;set;}
public ObservableCollection<TaxType> TaxTypesList {get;} = new();
public SelectStockToSaleItemViewModel(IServiceProvider serviceProvider)
{
_taxTypeDataService = serviceProvider.GetRequiredService<TaxTypeDataService>();
SendSaleItemBack = new SendSaleItemBackCommand(this, serviceProvider);
Task.Run(FillLists);
}
public async Task FillLists()
{
foreach (TaxType taxType in await _taxTypeDataService.GetAllAsNoTracking())
{
TaxTypesList.Add(taxType);
}
}
public SendSaleItemBackCommand()
{
private SaleItemStore _saleItemStore;
public SendSaleItemBackCommand(SelectStockToSaleItemViewModel selectStockToSaleItemViewModel, IServiceProvider serviceProvider)
{
_parentViewModel = selectStockToSaleItemViewModel;
_saleItemStore = serviceProvider.GetRequiredService<SaleItemStore>();
}
public Execute()
{
_saleItemStore.Message = new SaleItem()
{
TaxType = _parentViewModel.SelectedTaxType
}
//Takes user back to previous ViewModel;
}
}
So, my idea is that on SelectStockToSaleItemViewModelthe user selects a TaxType from a combobox filled with _taxTypeDataService.GetAllAsNoTracking(), and when they execute SendSaleItemBackCommand, SaleItemStore has its property set to a SaleItem with the TaxType not null;
Afterwards, should the user add more SaleItems to Sale, they'll have to open SelectStockToSaleItemViewModel again, and do the process for each extra SaleItem they wand to add to Sale.
Finally, they will have to execute SaveSaleCommand, and I'd end up with a Sale and its associated SaleItems on my database.
My dataServices are:
public class SaleDataService
{
private MyDbContext _context;
public SaleDataService(IServiceProvider serviceProvider)
{
_context = serviceProvider.GetRequiredService<MyDbContext>();
}
public async Task<Sale> AddOrUpdate(Sale sale)
{
_context.Update(sale);
await _context.SaveChangesAsync();
return sale;
}
}
public class TaxTypeDataService
{
private MyDbContext _contex;
public TaxTypeDataService(IServiceProvider serviceProvider0
{
_context = serviceProvider.GetRequiredService<MyDbContext>();
}
public async Task<List<TaxType>> GetAllAsNoTracking()
{
return _context.Set<TaxType>().AsNoTracking().ToListAsync();
}
}
public class Messaging<TObject> : IMessaging<TObject>
{
public TObject Message { get; set; }
}
SaleItemStore is singleton. SelectStockToSaleItemViewModel, and SelectStockToSaleItemViewModel are transient; TaxTypeDataService and SaleDataService are scoped; As for the context,
public static IHostBuilder AddDbContext(this IHostBuilder host)
{
host.ConfigureServices((_, services) =>
{
string connString = $"MyConnString";
ServerVersion version = new MySqlServerVersion(new Version(8, 0, 23));
Action <DbContextOptionsBuilder> configureDbContext = c =>
{
c.UseMySql(connString, version, x =>
{
x.CommandTimeout(600);
x.EnableRetryOnFailure(3);
});
c.EnableSensitiveDataLogging();
};
services.AddSingleton(new AmbiStoreDbContextFactory(configureDbContext));
services.AddDbContext<AmbiStoreDbContext>(configureDbContext);
});
return host;
}
The issue happens when the user tries to save a Sale with two or more SaleItems using the same TaxType, as EF Core throws an "another instance of TaxType is already being tracked". I understand the TaxType starts being tracked when the first Sale.SaleItem is updated, but how do I deal with this issue?
According to The instance of entity type 'Product' cannot be tracked because another instance with the same key value is already being tracked and The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked I did fill the combobox with TaxType using an AsNoTracking call, but I don't think it applies to my situation. Also, they say to "flush" the tracked entries before updating by setting the context's tracked entries' states to Detached, but, again, TaxType starts being tracked while being saved. I've checked the context's tracked entries collection, and there is no mention of TaxTypebeing tracked at all before Update(Sale) is called.
The only way I can think of as a workaround is using Fluent API and setting the foreign key, rather than the property itself (i.e. TaxTypeId = SelectedTaxType.ID), which doesn't look like the best way to deal with this.
Alright, I read some more about EF Core and I think I understood "the recommended way".
Whenever working with data services and dependency injection, the entities must be loaded by the same context that will be using them to add or update the context's database.
My mistake was loading the collections used by the interface (comboboxes, drop downs and checkboxes) using AsNoTracking. What happened was that whenever I tried to add or update two entities that used the same AsNoTracking-obtained entity, EF Core (as it should) tried to add the entity to the context's change tracker. So when the second entity tried to add/update, it threw a The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked exception.
So the "correct flow" is to load a collection/list using the same context that will be using them. Either using a singleton dbContext for the whole application and not using AsNoTracking, which is not recommended due to the possibility of ending up with concurrent operations; or using scoped dbContext for each "unit-of-work", i.e. a single dialog to add a new record, or an updating window with a record pulled from the same instance of the context that will be saving it, and disposing of the context (and it's change tracker) after whatever you wanted to do has been dealt with.
One of the big mistakes I was making when using scoped contexts was creating one context to load a datagrid with entries, which, when double-clicked, would open a dialog (with a new scoped context) to edit it without releasing that record from the previous context, throwing the already being tracked exception.

Get DbContext from Entity in Entity Framework Core

Is there a way to get an instance of the DbContext an entity is being tracked by (if any)?
I found the following suggestion/solution for EF6
Get DbContext from Entity in Entity Framework
public static DbContext GetDbContextFromEntity(object entity)
{
var object_context = GetObjectContextFromEntity( entity );
if ( object_context == null )
return null;
return new DbContext( object_context, dbContextOwnsObjectContext: false );
}
private static ObjectContext GetObjectContextFromEntity(object entity)
{
var field = entity.GetType().GetField("_entityWrapper");
if ( field == null )
return null;
var wrapper = field.GetValue(entity);
var property = wrapper.GetType().GetProperty("Context");
var context = (ObjectContext)property.GetValue(wrapper, null);
return context;
}
Is there a way to get this result in EF Core?
No. EF Core does not have lazy loading yet. If it had, then, a proxy generated from it would eventually have a reference to the DbContext that loaded it. As of now, there is no such reference.
One could use dependency injection on the instance/entity at creation. To allow the owning dbcontext to be retrieved from the entity later.
eg
class Book
{
public readonly DBContext _dbcontext;
public Book(DBContext dbcontext)
{
_dbcontext = dbcontext;
}
}
There is no good way to do this. There seems to be no easy way to inject any code into the process after an entity object is constructed but before it is enumerated through in the calling code.
Subclassing InternalDbSet was something I considered but you can only fix calls to the .Find methods and the IQueryable implementation (the main way you'd use a DbSet) is out of reach.
So the only option I can see left is to not allow access to the DbSet at all but have accessor functions which will set the .Owner (or whatever you want to call it) property for me. This is messy since you would normally have to write a function for every query type you'd want to make, and the caller couldn't use LINQ any more. But we can use generics and callbacks to preserve most of the flexibility though it looks ugly. Here is what I came up with.
I am working on porting and cleaning up a complex system so I am not in a position to really test this yet but the concept is sound. The code may need further tweaking to work as desired. This should not have any penalties with eg pulling down the entire table before processing any records as long as you use EnumerateEntities to enumerate, instead of QueryEntities, but again I have yet to do any real testing on this.
private void InitEntity(Entity entity) {
if (entity == null) {
return;
}
entity.Owner = this;
// Anything you want to happen goes here!
}
private DbSet<Entity> Entities { get; set; }
public IEnumerable<Entity> EnumerateEntities() {
foreach (Entity entity in this.Entities) {
this.InitEntity(entity);
yield return entity;
}
}
public IEnumerable<Entity> EnumerateEntities(Func<DbSet<Entity>, IEnumerable<Entity>> filter) {
IEnumerable<Entity> ret = filter(this.Entities);
foreach (Entity entity in ret) {
this.InitEntity(entity);
yield return entity;
}
}
public T QueryEntities<T>(Func<DbSet<Entity>, T> filter) {
if (filter is Func<DbSet<Entity>, Entity>) {
T ret = filter(this.Entities);
this.InitEntity(ret as Entity);
return ret;
}
if (filter is Func<DbSet<Entity>, IEnumerable<Entity>>) {
IEnumerable<Entity> ret = filter(this.Entities) as IEnumerable<Entity>;
// You should be using EnumerateEntities, this will prefetch all results!!! Can't be avoided, we can't mix yield and no yield in the same function.
return (T)ret.Select(x => {
this.InitEntity(x);
return x;
});
}
return filter(this.Entities);
}
public void QueryEntities(Action<DbSet<Entity>> filter) => filter(this.Entities);

Repository pattern Generic Write Repository issue - Object State Manager Issue

I am doing an MVC project using a repository pattern and I have a core write repository as follows
public abstract class WriteRepository<TContext> : IWriteRepository
where TContext : DbContext, new()
{
private readonly TContext _context;
protected TContext Context { get { return _context; } }
protected WriteRepository()
{
_context = new TContext();
}
public TItem Update<TItem>(TItem item, bool saveImmediately = true) where TItem : class, new()
{
return PerformAction(item, EntityState.Modified, saveImmediately);
}
public TItem Delete<TItem>(TItem item, bool saveImmediately = true) where TItem : class, new()
{
return PerformAction(item, EntityState.Deleted, saveImmediately);
}
public TItem Insert<TItem>(TItem item, bool saveImmediately = true) where TItem : class, new()
{
return PerformAction(item, EntityState.Added, saveImmediately);
}
public void Save()
{
_context.SaveChanges();
}
protected virtual TItem PerformAction<TItem>(TItem item, EntityState entityState, bool saveImmediately = true) where TItem : class, new()
{
_context.Entry(item).State = entityState;
if (saveImmediately)
{
_context.SaveChanges();
}
return item;
}
public void Dispose()
{
_context.Dispose();
}
}
I wanted to update a single field in my db on an action method and I was doing a get all before I could update that value like below
public ActionResult UpdateTenant(string id)
{
Tenant model = new Tenant();
model = _TenantServices.GetItemById(Guid.Parse(id));
model.IsLoginEnabled = true;
_TenantServices.Update(model);
return RedirectToAction("ViewDetails", new { id = model.TenantId });
}
When I do that I am getting an error saying "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key."
I am using AsNoTracking to retrieve data as follow
public Tenant GetItemById(Guid id)
{
return Context.Tenants.AsNoTracking().Where(t => t.TenantId == id).FirstOrDefault();
}
Any Idea how can I solve this ?
Whenever you retrieve an object from the database, Entity Framework begins tracking (attaches) the object immediately. You will be able to make changes to the retrieved object (i.e. set property values) and call SaveChanges() so that the object will be updated in the database, without the need to set the EntityState.
And in fact, if you attempt to Attach or set the EntityState of an already-tracked object, you will get the error you mentioned above.
So, to resolve the error, you can:
Use one instance of of your TContext to retrieve and another instance to update. In this case, you should attach and set the EntityState in the update method for the changes to get persisted.
Use a single instance of your TContext to retrieve and update, but don't attempt to Attach or to set the EntityState anymore. Call SaveChanges directly after setting the property values.
Use a single instance of your TContext, but when retrieving the record you can call AsNoTracking(). This will allow you to safely Attach or set EntityState during the update.
Hope that helps.

Invoking DbSet<Type>.Add() method using reflections, in a child class of DbContext

I've got a DbContext child class that talks to the Database through entity framework. I'm using MVC C#.
I've got n DbSets related to my tables. I want to invoke the Add method for any DbSet.Add(entityObject), in this case UserAccounts.Add(entityUserAccount) using reflections or dynamic variable.
I'm doing this because I need to inject DbContect by dependency injection in my controllers.
public class DataContext: DbContext
{
public DbSet<UserAccount> UserAccounts { get; set; }
public void Insert<TypeEntity>(TypeEntity entity) where TypeEntity : BaseEntity
{
var typesToRegister = typeof(DataContext).GetProperties().
Where(p => p.PropertyType == typeof(DbSet<TypeEntity>));
//This is where the code explodes in face:-)
//Please let me know how to fix it
var dbSetItem = typesToRegister.First();
var methodAdd = dbSetItem.GetType().GetMethod("Add");
methodAdd.Invoke(this, new TypeEntity[] { entity });
}
}
//Insert<TypeEntity>(TypeEntity entity) is my service
//method to invoke the DbSet<T>.Add() method.
This is the error I get: Object reference not set to an instance of an object.
I fixed your code. There were two mistakes:
Instead of
var methodAdd = dbSetItem.GetType().GetMethod("Add");
you should write: (because dbSetItem.GetType() will return propertyType not type of the field what you expected)
dbSetItem.GetValue(this).GetType().GetMethod("Add");
And change this row (because Add method should be invoked that way: db.Items.Add(...) not db.Add(...))
methodAdd.Invoke(this, new TypeEntity[] { entity> });
to that one:
methodAdd.Invoke(dbSetItem.GetValue(this), new TypeEntity[] { entity> });
And did you forget to invoke SaveChanges method at the end of your code?
public void Insert<TypeEntity>(TypeEntity entity) where TypeEntity: BaseEntity
{
var typesToRegister = typeof(DataContext).GetProperties().
Where(p => p.PropertyType == typeof(DbSet<TypeEntity>));
var dbSetItem = typesToRegister.First();
var methodAdd = dbSetItem.GetValue(this).GetType().GetMethod("Add");
methodAdd.Invoke(dbSetItem.GetValue(this), new TypeEntity[] { entity });
SaveChanges();
}
dbSetItem is of type PropertyInfo because you called GetProperties(). It does not have an Add method which is why you are getting a null (GetMethod returns null if a method can't be found that matches).
var dbSetItem = typesToRegister.First().PropertyType;
You cannot pass this into the first parameter of Invoke, it needs to be an object of type DbSet<TypeEntity>, also your array should be object[] (you can pass something else as it will automatically upconvert).
methodAdd.Invoke(dbSetItem, new object[] { entity });
I think your approach is a bit off.
Since you are already in the DbContext class i'm assuming this is the context.
var dbSet = this.Set<TypeEntity>();
dbSet.Add(entity);
Update
You should change
dbsetItem.GetType().GetMethod("Add");
to
dbsetItem.PropertyType.GetMethod("Add");
The typesToRegister.First() returns a PropertyInfo so we can't use GetType .. instead we use PropertyType.
And you'll need this change as well.
Update 2
var dbSet = this.Set<TypeEntity>(); // an actual instance that can be passed to Invoke
methodAdd.Invoke(dbSet, new TypeEntity[] { entity });

C# Entity Framework - IEntityChangeTracker issue

The exception I am receiving is An entity object cannot be referenced by multiple instances of IEntityChangeTracker. My code is structured like so...
My context class looks like this:
public class MyContext : DbContext, IDataContext
{
public MyContext (string connectionString) :
base(connectionString)
{
}
public DbSet<AssigneeModel> Assignees { get; set; }
public DbSet<AssetAssignmentModel> AssetAssignments { get; set; }
}
public class AssigneeController : Controller
{
protected MyContext db = new MyContext(ConnectionString);
[HttpPost]
public ActionResult Import(SomeObjectType file)
{
AssigneeModel assignee = new AssigneeModel();
assignee.FirstName = "Joe";
assignee.LastName = "Smith";
// Assignees have assets, and the relationship is established via an AssetAssignmentModel entity
AssetAssignmentModel assetAssignmentModel = new AssetAssignmentModel
{
Asset = someExistingAsset,
// Assignee = assignee, // Don't establish relationship here, this object will be added to the assignee collection
}
assignee.AssetAssignments.Add(assetAssignmentModel); // Manually add object to establish relationship
db.Assignees.Add(assignee); // Add the assignee object
// Exception occurs when adding the object above
};
}
EF Version 4.1
The problem is from your Asset object, when you're getting it from the other method, you'll need to explicitly detach it from that context, before adding it to this new context. As Julie mentioned, the entity instance will carry the context with it, but the porblem wasn't with the AssigneeModel you created, but with the someExistingAsset you retrieved.
You've tagged this as EF4.1 (where I expected code first & dbcontext) but it looks like a side effect of EntityObject (edmx, objectcontext, default code gen in VS2008 & VS2010).
In that case, if you have an entity (that derives from EntityObject) and you dispose its' context without first detaching the entity, the entity instance still has an artifact of that context. So when you try to attach it to another context, it gives this message. THat was a problem with EF 3.5 and EF4 if you aren't using POCOs. I haven't had to wrestle with it in a long time but I remember the sting. :)

Categories