Can you get the DbContext from a DbSet?
Asked Answered
H

5

45

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.

Harlandharle answered 17/7, 2013 at 21:40 Comment(0)
A
31

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

Allbee answered 18/7, 2013 at 0:31 Comment(5)
Wow thanks very much. That's an impressive bit of code. However, I agree with your warnings about internals and reflection, and I've decided to go with @TimothyWalters answer as that seems to be an "officially supported" routeHarlandharle
Awesome snippet of code. I believe there are some legitimate uses for getting the DbContext from a collection.Katheryn
In EF7 there is a private field _context directly in the class the implements DbSet<T>. It's not hard to expose this field publicly.Pasol
Actually, as I was told by now, this should be possible with EF7: var myContext = mySet.GetService<DbContext>(); I can't test it now. Can anybody confirm this works?Pasol
@Pasol your last statement is dependent on how the user configures their project, in my case no that didn't workAmersfoort
P
41

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

Perl answered 14/6, 2018 at 19:12 Comment(5)
Warning: this will not work starting from .NET Core 3.1, there are some assembly changes that will cause runtime exceptions.Vaules
I didn't test it with 3.x version but according to the docs, it should still work: learn.microsoft.com/en-us/dotnet/api/…Sacrilegious
Sorry, my bad. I got errors because two projects were using different versions of EFC packages (2.x and 3.1), which is never good idea.Vaules
I am using this in ASP.NET 5, and there is no need for Context any more: var context = myDbSet.GetService<ICurrentDbContext>();Laband
Can confirm that this solution still works with EF 7Butz
A
31

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

Allbee answered 18/7, 2013 at 0:31 Comment(5)
Wow thanks very much. That's an impressive bit of code. However, I agree with your warnings about internals and reflection, and I've decided to go with @TimothyWalters answer as that seems to be an "officially supported" routeHarlandharle
Awesome snippet of code. I believe there are some legitimate uses for getting the DbContext from a collection.Katheryn
In EF7 there is a private field _context directly in the class the implements DbSet<T>. It's not hard to expose this field publicly.Pasol
Actually, as I was told by now, this should be possible with EF7: var myContext = mySet.GetService<DbContext>(); I can't test it now. Can anybody confirm this works?Pasol
@Pasol your last statement is dependent on how the user configures their project, in my case no that didn't workAmersfoort
T
16

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();
}
Tubate answered 18/7, 2013 at 2:0 Comment(3)
Thanks very much! Just to let you know I will be using this answer, however I've marked smartcaveman's answer as the accepted answer because it actually answers my question (even if the answer isn't as helpful as yours).Harlandharle
I find that the correct answer is to first re-think your question, in this case I thought in terms of your goal, instead of how you wanted to achieve it. I believe that's the core principal of "thinking outside the box", a great skill to learn.Tubate
Why use DbSet instead of DbContext? Because it's easier to use. Think of context.Entities.Action() (you're already used to that) instead of context.Action<Entity>() (ugly angles mix). It would certainly be the better solution if the context instance was easier accessible from there. You might use type inference if you have a parameter of that type, but then it's even less obvious what type of entity the method is working on.Pasol
I
0

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

Iridissa answered 18/7, 2013 at 0:3 Comment(0)
T
0

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();
Triturate answered 13/6, 2023 at 22:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.