【问题标题】:Why is it required to reference EntityFramework.dll in the client project to make the DbContext IDisposable?为什么需要在客户端项目中引用 EntityFramework.dll 才能使 DbContext IDisposable?
【发布时间】:2016-11-03 00:09:59
【问题描述】:

创建一个包含实体框架模型和对象上下文的类库。然后将新的控制台应用程序添加到解决方案中。在控制台应用程序中,引用具有您的模型的项目。

现在在控制台应用程序中输入:

static void Main(string[] args)
{
    using (var context = new ExperimentalDbContext())
    {

    }
    Console.ReadKey();
}

在构建时,您会收到一个错误报告:

类型“System.Data.Entity.DbContext”在一个程序集中定义,该程序集 没有被引用。您必须添加对程序集的引用 EntityFramework...yada yada yada...

现在,这几年我已经做了很多次了,但是每次遇到这个错误,我又一次无奈地在网上搜索了我当时忘记的解决方案。

解决此问题需要您在 ConsoleClient 项目中安装 EntityFramework NuGet 包。

所以,我的问题不在于修复是什么,而是为什么?因为它没有任何意义!

为了完整起见,我使用的是实体框架的 v6.1.3。但多年来,我在早期版本中也多次看到此错误。

更新

问题似乎只是当您使用using 代码块时,该代码块旨在在IDisposables 上调用Dispose

要检验假设,请创建一个控制台应用程序,该应用程序在同一解决方案中引用 ClassLibrary1,在同一解决方案中引用 ClassLibrary2,代码如下:

using ClassLibrary1;
using System;

namespace TestHypothesis1
{
    class Program
    {
        // Testing the hypothesis presented in this answer: http://stackoverflow.com/a/38130945/303685
        // This seems to be the behavior with just (or may be even others I haven't tested for)
        // IDisposable.
        // anotherFoo instance is created just fine, but the moment I uncomment
        // the using statement code, it shrieks.
        static void Main(string[] args)
        {
            //using (var foo = new Foo())
            //{
            //    foo.Gar = "Gar";

            //    Console.WriteLine(foo.Gar);
            //}

            var anotherFoo = new Foo() { Gar = "Another gar" };
            Console.WriteLine(anotherFoo.Gar);

            Console.ReadKey();
        }
    }
}


using ClassLibrary2;
using System;

namespace ClassLibrary1
{
    public class Foo: Bar, IDisposable
    {
        public string Gar { get; set; }

        public void Dispose()
        {
            throw new NotImplementedException();
        }
    }
}


namespace ClassLibrary2
{
    public class Bar
    {
        public string Name { get; set; }
    }
}

您会发现编译器抱怨缺少引用仅针对第一个 Foo 的实例化,而不是第二个实例。

奇怪的是,在第一个 EntityFramework 示例中,如果您从控制台应用程序中删除了对 EntityFramework.dll 的引用并将Main 中的代码更改为此,它仍然会抱怨缺少引用。

static void Main(string[] args)
{
    var context = new ExperimentalDbContext();
    Console.ReadKey();
    context.Dispose();
}

此外,如果您注释掉对 context.Dispose() 的调用,即上面代码 sn-p 的最后一行,代码仍然可以正常工作,即使它抛出了 InvalidOperationException,但我猜这是由于在其迭代器完成其MoveNext 调用之前释放上下文的竞争条件。

static void Main(string[] args)
{
    var context = new ExperimentalDbContext();
    Console.ReadKey();
    // context.Dispose();
}

所以,新的附加问题现在变成了:

using 语句的实现方式是什么导致编译器在链接引用时停在其轨道上?

原来的问题仍然存在。

又一次更新

现在看来,问题可能进一步归结为对IDisposable.Dispose 方法的调用,因此问题不在于using 语句的实现。 using 声明似乎是一个无辜的保证,即 Dispose 将被调用,而不是其他任何东西。

因此,在上面的Foo 示例中,如果您在最后插入对anotherFoo.Dispose 的调用,编译器就会再次开始报错。像这样:

using ClassLibrary1;
using System;

namespace TestHypothesis1
{
    class Program
    {
        // Testing the hypothesis presented in this answer: http://stackoverflow.com/a/38130945/303685
        // This seems to be the behavior with just (or may be even others I haven't tested for)
        // IDisposable.
        // anotherFoo instance is created just fine, but the moment I uncomment
        // the using statement code, it shrieks.

        // Final update:
        // The trigger for the error seems to be the call to the Dispose method and not
        // particularly the implementation of the using statement, which apparently, simply
        // ensures that Dispose is called, as is also well-known and documented.
        static void Main(string[] args)
        {
            //using (var foo = new Foo())
            //{
            //    foo.Gar = "Gar";

            //    Console.WriteLine(foo.Gar);
            //}

            var anotherFoo = new Foo() { Gar = "Another gar" };
            Console.WriteLine(anotherFoo.Gar);

            anotherFoo.Dispose();

            Console.ReadKey();
        }
    }
}

那么,最后一个问题,总结来说是这样的:

为什么对Dispose 的调用会阻止编译器链接程序集引用?

我认为我们现在正在取得进展。

【问题讨论】:

  • 您是如何在不要求项目中引用 EntityFramework 的情况下使用 Context 的?
  • @krillgar model 项目应该需要它,而不是 client。客户想要参考是没有意义的。我在问为什么会这样。

标签: c# .net entity-framework


【解决方案1】:

原答案

我不认为它是特定于 DbContext,但或多或​​少是因为在您的类库中引用的依赖 DLL 没有传递到控制台应用程序。因此,当您构建时,编译器只知道控制台应用程序中的引用,而不知道对 EntityFramework 的链式引用。它抱怨的唯一原因是因为编译器使用using 语句运行检查以确保该类属于IDisposable,并且它可以知道的唯一方法是它是否解析了EntityFramework 库中的引用。

更新

事实证明,我仍然认为这是对的。如果在您的示例中,您忘记了 IDisposable 并且只是尝试在控制台应用程序中的 Bar 类上使用属性 Name,您会发现您得到一个它不知道该属性的异常,因为它位于未引用的程序集中。

未引用的汇编错误示例:

(inside Main)
Console.WriteLine(anotherFoo.Name);

不管它的价值如何,您实际上可以引用具有嵌套引用的库,并且永远不会在您的应用程序中包含这些嵌套引用,只要调用代码从未真正到达引用或需要嵌套库的代码路径。这可能是错误的一个简单来源,尤其是对于部署/发布场景。想象一个场景,您的发布不包括您的应用程序所需的所有库,但需要深度嵌套库的代码路径很少被调用。然后有一天,你接到一个电话,说“应用程序坏了!”有人立即倾向于说“但没有任何改变!自上次以来我们没有部署!”这是在测试、QA、部署后等期间具有良好代码覆盖率的重要原因之一。

【讨论】:

  • 该死!这似乎不对。或者可能是我又一次醒得太久了。看起来我脑子里有一些非常基本的东西是错误的。编写一些测试代码来挑战这一点并验证我目前的信念,即不应该要求引用并且应该很容易地沿着 chain 推断出引用。
  • 实际上,我第一次看到一些奇怪的行为,我写的一个小样本挑战了我所知道的这个非常基本的东西。它适用于大多数情况,但不适用于某些情况。我将用我的测试代码发布一个 .NET fiddle。
  • 看来问题仅在于您使用 IDisposable 接口时,它不能推断出正确的引用。请参阅此示例代码。我写了一个控制台项目,它引用了一个引用 ClassLibrary2 项目的 ClassLibrary1 项目。当然,在 .NET 小提琴中,我无法添加项目,但如果您添加它们并运行代码,您将观察到我所询问的这种行为。 dotnetfiddle.net/GcR1ea
  • 是的,我已经更新了这个问题。这似乎是 C# 编译器实现 using 语句的方式。现在,如果@EricLippert 或 C# 团队中的某个人可以告诉我们,那将解开谜团。 :-)
  • 我确实认为编译器将始终查找您正在运行代码的项目的配置。这也意味着它将检查您的项目的 app.config(或 web.config)为了根据您的连接字符串创建上下文,并且由于没有定义实体框架对象而失败。我相信导致此错误的不是 Dispose() 语句,而是类型定义本身,因为 DbContext 派生自 IDisposable。
【解决方案2】:

这不是专门对Dispose() 的调用,也不是与 EF 有任何关系。这只是编译器对任何引用程序集的工作方式:如果您想与在当前项目未直接引用的程序集中定义的对象(阅读:使用属性或方法)进行交互,您不能:您必须直接添加对该程序集的引用。

这里发生的情况是您对Dispose() 的调用是从DbContext 调用该方法,因为您尚未在第一级程序集中实现它。正如@poke 指出的那样在某些情况下,您可以通过在您的类上实现一个调用另一个程序集的方法来轻松解决这个问题:只要您不尝试直接访问它就可以了。这在某些情况下可能适用于人们,但它不适用于 EF 上下文的特定情况。

问题是您的上下文将具有DbSet<your entity type> 类型的属性,它本身就是对System.DataSystem.Data.Entity 命名空间的引用。在搞砸的同时,我发现这会阻止与“第一级”对象的任何交互:它允许您新建一个就好了,但是当您尝试访问 any 属性或方法时,您将获得同样的编译器错误。我认为这是由于编译器优化了代码,因此创建但从未使用过的对象实际上从未被创建。


只是为了澄清您在其中一个更新中所说的内容:

using 语句似乎只是一个无辜的保证,即 Dispose 将被调用,而不是其他任何东西。

这完全正确。您可以在the spec 的 8.13 中找到确切的详细信息,但一般用例是它扩展为

{
    ResourceType resource = expression;
    try {
        statement;
    }
    finally {
        ((IDisposable)resource).Dispose();
    }
}

基本上只是将您的语句包装在 try...finally 中,并将资源范围限定为新块。那里没有魔法。如前所述,您可以通过在“第一级”类上实现IDisposable 并在那里实现Dispose 来调用“第二级”程序集来解决此问题。在某些情况下,这可能会让你走得足够远。


这里有一些以前的问题以更一般的方式询问这个问题的例子:

  1. Why must I chain reference assemblies?
  2. Why do I (sometimes) have to reference assemblies referenced by the assembly I reference?
  3. Why does the compiler when using an overload in another assembly sometimes require you to also reference a subassembly?

有关表明这种行为很常见的快速示例,这里是little repo on GitHub 或查看以下代码:

// our first, base class. put this in it's own project.
namespace SecondLevel
{
    public class SecondLevel
    {
        public void DoSomething()
        {

        }
    }
}


// our second class that references the base class. Add this to it's own project as well. 
namespace FirstLevel
{
    public class FirstLevel
    {
        public SecondLevel.SecondLevel reference;

        public FirstLevel()
        {
            reference = new SecondLevel.SecondLevel();
        }

        public void DoSomethingWithReferencedClass()
        {
            reference.DoSomething();
        }
    }
}


// our "Client" console app that does nothing, but indicates the compiler errors.
// also in it's own project 
// Reference the project that "FirstLevel" is in, 
// but not the one that "Second Level" is in. 
namespace ConsoleTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var primary = new FirstLevel.FirstLevel();

            primary.reference.DoSomething(); // will cause compiler error telling you to reference "Second Level"

            primary.DoSomethingWithReferencedClass(); // no compiler error: does the same thing the right way.
        }
    }
}

注意primary.DoSomethingWithReferenceClass();会调用同样的方法,但不会报错,因为它不会直接尝试访问二级类。

至于您为什么的问题,您必须询问 Microsoft 团队。解决方法是要么接受您必须添加引用,要么编写代码,以便客户端只知道被引用程序集的单个级别。

【讨论】:

  • 在 OP 的情况下,为中间类(ExperimentalDbContext)再次实现IDisposable 会解决这个问题吗?由于ExperimentalDbContext 会直接实现IDisposable,编译器不需要上去查看dispose 实现。
  • @poke 这是个好主意,我不知道为什么我没有想到。是的,这可以正常工作,您可以将实际的DbContext.Dispose() 包装在ExperimentalDbContext.Dispose() 中,然后您只引用第一个参考,这样就可以正常工作。如果您不介意,我会将其添加到我的答案中。
  • 好的,继续!感谢您的回答,这绝对为我解决了问题
  • @poke 呃,这并不容易 - 请参阅我的编辑。包装调用在理论上可以正常工作,但是因为上下文类也为 DbSets 引用了 System.Data.Entity,所以您仍然会收到错误消息。如果您遇到此问题,请注意它可能是一个很好的解决方案,但它不允许您在不添加引用的情况下使用 EF。
猜你喜欢
  • 2015-06-06
  • 1970-01-01
  • 1970-01-01
  • 2018-11-28
  • 2017-12-23
  • 1970-01-01
  • 2020-08-20
  • 2013-10-02
  • 2011-08-20
相关资源
最近更新 更多