你能从DbSet获取DbContext吗?

45

我的应用程序中有时需要一次性保存10,000行或更多行到数据库中。我发现简单地逐个迭代并添加每个项目可能需要多达半个小时。

但是,如果我禁用AutoDetectChangesEnabled,则只需大约5秒钟(这正是我想要的)

我正在尝试创建一个名为“AddRange”的扩展方法,将其添加到DbSet中,该方法将禁用AutoDetectChangesEnabled,然后在完成后重新启用它。

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;
        }
    }
所以,我的问题是:是否有一种方法可以从DbSet中获取DbContext?我不喜欢把它作为参数 - 感觉应该是不必要的。
5个回答

41

警告:从.NET Core 3.1开始,存在一些程序集更改,将导致运行时异常。 - klenium
我没有在3.x版本上测试过,但根据文档,它应该仍然可以工作:https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.infrastructure.icurrentdbcontext?view=efcore-3.1 - Christoph Lütjen
抱歉,是我的错。我遇到了错误,因为两个项目使用了不同版本的 EFC 包(2.x 和 3.1),这是一个不好的做法。 - klenium
1
我正在在ASP.NET 5中使用此功能,现在不再需要Context了:var context = myDbSet.GetService<ICurrentDbContext>(); - Felix
1
可以确认这个解决方案仍然适用于EF 7。 - Shazi

31

是的,你可以从 DbSet<TEntity> 中获取 DbContext,但解决方案需要大量使用反射。我在下面提供了一个示例。

我测试了以下代码,并成功地检索到生成 DbSetDbContext 实例。请注意,虽然它回答了你的问题,几乎肯定有更好的解决方案来解决你的问题

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); 
    } 
}

使用示例:

using(var originalContextReference = new MyContext())
{
   DbSet<MyObject> set = originalContextReference.Set<MyObject>();
   DbContext retrievedContextReference = set.GetContext();
   Debug.Assert(ReferenceEquals(retrievedContextReference,originalContextReference));
}

说明:

根据Reflector,DbSet<TEntity>有一个类型为InternalSet<TEntity>的私有字段_internalSet。该类型是EntityFramework dll中的内部类型。它从InternalQuery<TElement>(其中TEntity : TElement)继承。 InternalQuery<TElement>也是EntityFramework dll中的内部类型。它具有类型为InternalContext的私有字段_internalContextInternalContext也是EntityFramework的内部类型。但是,InternalContext公开了一个名为Owner的public DbContext属性。因此,如果您有一个DbSet<TEntity>,可以通过反射访问每个属性并将最终结果强制转换为DbContext来获取对DbContext所有者的引用。

来自@LoneyPixel的更新

在EF7中,类实现了DbSet,直接有一个私有字段_context。公开此字段并不难。


哇,非常感谢。那是一段令人印象深刻的代码。然而,我同意你关于内部和反射的警告,并且我决定采用@TimothyWalters的答案,因为那似乎是一个“官方支持”的路线。 - berkeleybross
很棒的代码片段。我相信从集合中获取DbContext有一些合法的用途。 - RitchieD
EF7中,类中直接实现了DbSet<T>,并且有一个私有字段_context。将此字段公开并不难。 - ygoe
实际上,据我现在所知,这应该可以通过EF7实现:var myContext = mySet.GetService<DbContext>(); 我现在无法测试它。有人能确认这是否有效吗? - ygoe
1
@ygoe,你最后的陈述取决于用户如何配置他们的项目,在我的情况下,那并没有起作用。 - johnny 5

16

为什么你要在DbSet上这样做?试着在DbContext上进行操作:

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;
    }
}

然后使用它就像这样简单:
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();
}

非常感谢!只是想让您知道,我将使用这个答案,但我已经标记smartcaveman的答案为被接受的答案,因为它实际上回答了我的问题(即使答案不像您的那么有帮助)。 - berkeleybross
2
我发现正确的答案是首先重新思考你的问题,例如,在这种情况下,我是根据你的目标来考虑的,而不是关注你想要如何实现它。我相信这就是“跳出固有思维框架”的核心原则,这是一个非常值得学习的技能。 - Timothy Walters
为什么使用DbSet而不是DbContext?因为它更容易使用。想象一下 context.Entities.Action()(你已经习惯了这样的方式)而不是 context.Action<Entity>()(角度混合得很丑)。如果从那里更容易访问上下文实例的话,这肯定是更好的解决方案。如果你有一个该类型的参数,可以使用类型推断,但此时方法作用于哪种类型的实体就更不明显了。 - ygoe

0
也许你可以创建一个帮助程序来为你禁用它,然后只需从AddRange方法中调用该帮助程序。

0

我的使用情况略有不同,但我也想解决这个问题,对于我所称之为Save()的dbsetextension方法,它将根据需要执行添加或修改操作,具体取决于要保存的项是否与dbset中的项匹配。

这个解决方案适用于EF 6.4.4,源自smartcaveman的回答。

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;
}

我的使用案例在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; 
    }
}

使用用例的示例

db.AppUsers.Save(a => a.emplid == appuser.emplid, appuser);
db.SaveChangesAsync();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接