【问题标题】:Can you get the DbContext from a DbSet?你能从 DbSet 中获取 DbContext 吗?
【发布时间】:2013-07-16 15:42:10
【问题描述】:

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

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

我正在尝试为 DbSet 创建一个名为“AddRange”的扩展方法,该方法将禁用 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?我不喜欢把它作为一个参数——感觉它应该是不必要的。

【问题讨论】:

    标签: c# entity-framework entity-framework-4.1 dbcontext dbset


    【解决方案1】:

    使用 Entity Framework Core(使用 2.1 版测试),您可以使用获取当前上下文

    // DbSet<MyModel> myDbSet
    var context = myDbSet.GetService<ICurrentDbContext>().Context;
    

    How to get a DbContext from a DbSet in EntityFramework Core 2.0

    【讨论】:

    • 警告:从 .NET Core 3.1 开始,这将不起作用,there are some assembly changes 会导致运行时异常。
    • 我没有用 3.x 版本测试它,但根据文档,它应该仍然可以工作:docs.microsoft.com/en-us/dotnet/api/…
    • 对不起,我的错。我遇到了错误,因为两个项目使用不同版本的 EFC 包(2.x 和 3.1),这绝不是一个好主意。
    • 我在 ASP.NET 5 中使用这个,并且不再需要Contextvar context = myDbSet.GetService&lt;ICurrentDbContext&gt;();
    【解决方案2】:

    是的,您可以从DbSet&lt;TEntity&gt; 获得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&lt;TEntity&gt; 有一个 _internalSet 类型为 InternalSet&lt;TEntity&gt; 的私有字段。该类型在 EntityFramework dll 内部。它继承自InternalQuery&lt;TElement&gt;(其中TEntity : TElement)。 InternalQuery&lt;TElement&gt; 也是 EntityFramework dll 的内部。它有一个私有字段_internalContext,类型为InternalContextInternalContext 也是 EntityFramework 内部的。但是,InternalContext 公开了一个名为 Owner 的公共 DbContext 属性。因此,如果您有一个DbSet&lt;TEntity&gt;,则可以通过反射性地访问每个属性并将最终结果转换为DbContext 来获得对DbContext 所有者的引用。

    @LoneyPixel更新

    在 EF7 中,在实现 DbSet 的类中直接有一个私有字段 _context。公开这个领域并不难

    【讨论】:

    • 哇非常感谢。这是一段令人印象深刻的代码。但是,我同意您关于内部和反射的警告,我决定采用@TimothyWalters 的回答,因为这似乎是“官方支持”的路线
    • 很棒的sn-p代码。我相信从集合中获取 DbContext 有一些合法用途。
    • EF7 中有一个私有字段_context 直接在实现DbSet&lt;T&gt; 的类中。公开这个领域并不难。
    • 实际上,正如我现在所知道的,EF7 应该可以做到这一点:var myContext = mySet.GetService&lt;DbContext&gt;(); 我现在无法测试它。任何人都可以确认这是有效的吗?
    • @ygoe 您的最后一条语句取决于用户如何配置他们的项目,在我的情况下,这不起作用
    【解决方案3】:

    为什么要在 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 的答案标记为已接受的答案,因为它实际上回答了我的问题(即使答案不如您的有用)。
    • 我发现正确的答案是首先重新考虑你的问题,在这种情况下,我考虑的是你的目标,而不是你想如何实现它。我相信这是“跳出框框思考”的核心原则,是一项非常值得学习的技能。
    • 为什么使用DbSet 而不是DbContext?因为它更容易使用。想想context.Entities.Action()(你已经习惯了)而不是context.Action&lt;Entity&gt;()(丑陋的角度混合)。如果上下文实例更容易从那里访问,那肯定是更好的解决方案。如果您有该类型的参数,您可能会使用类型推断,但是该方法正在处理的实体类型就更不明显了。
    【解决方案4】:

    也许您可以创建一个帮手为您禁用此功能,然后从 AddRange 方法中调用该帮手

    【讨论】:

      猜你喜欢
      • 2012-11-11
      • 2017-12-10
      • 2016-05-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-31
      相关资源
      最近更新 更多