Reputation: 1386
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.
Upvotes: 45
Views: 17938
Reputation: 334
My use case is slightly different but i do also want to solve this issue for a dbsetextension method I have called Save() which will perform an add or a modify to the dbset as needed depending on whether the item to save matches an item in the dbset.
this solution works for EF 6.4.4 derived from smartcaveman response
public static DbContext GetContext<TEntity>(this DbSet<TEntity> dbSet) where TEntity : class
{
var internalSetPropString = "System.Data.Entity.Internal.Linq.IInternalSetAdapter.InteralSet";
var bfnpi = BindingFlags.NonPublic | BindingFlags.Instance;
var bfpi = BindingFlags.Public | BindingFlags.Instance;
var internalSet = dbSet.GetType().GetProperty(internalSetPropString, bfnpi).GetValue(dbSet);
var internalContext = internalSet.GetType().BaseType.GetField("_internalContext", bfnpi).GetValue(internalSet);
var ownerProperty = internalContext.GetType().GetProperty("Owner", bfpi);
var dbContext = (dbContext)ownerProperty.GetValue(internalContext);
return dbContext;
}
my usecase in DbSetExtensions
//yes I have another overload where i pass the context in. but this is more fun
public static void Save<T>(this DbSet<T> dbset, Expresssion<Fun<T, bool>> func, T item) where T :class
{
var context = dbset.GetContext(); //<--
var entity = dbset.FrirstOrDefault(func);
if(entity == null)
dbset.Add(item);
else
{
var entry = context.Entry(entity);
entry.CurrentValues.SetValues(item);
entry.State = EntityState.Modified;
}
}
sample use of usecase
db.AppUsers.Save(a => a.emplid == appuser.emplid, appuser);
db.SaveChangesAsync();
Upvotes: 0
Reputation: 5804
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
Upvotes: 44
Reputation: 42246
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
.
In EF7 there is a private field _context directly in the class the implements DbSet. It's not hard to expose this field publicly
Upvotes: 31
Reputation: 16874
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();
}
Upvotes: 16