IoC容器,在编译时检查错误

时间:2012-03-27 19:29:54

标签: .net build dependency-injection error-handling inversion-of-control

我有一个简单的问题。

假设我有一个.Net解决方案,有不同的项目,比如一些类库(bll,dal等)和一个可以作为web应用程序或wpf应用程序的主项目,这没关系。

现在假设我想使用IoC容器(如Windsor,Ninject,Unity等)来解析验证器,存储库,通用接口实现等问题。

我把它们放在一起。编译并运行良好。然后,有一天,我添加了一个新服务,在我的代码中,我只是尝试通过IoC容器解决它。事实是,我忘了在IoC配置中注册它。

Everything编译,应用程序部署并运行。一切正常,除了页面的代码要求容器的新服务,容器回答“嘿,我对此服务一无所知”。

您会记录您的错误,以及用户友好的错误页面。您将检查错误,查看问题并进行修复。很标准。

现在假设我们想改进这个过程,并且在编译时能够知道我们期望IoC容器处理的每个服务是否在代码中正确注册。

如何实现这一目标?有一件事,单元测​​试被排除在可能的答案之外,我正在寻找另一种方式,如果确实存在的话。

思想?

编辑 - 经过一些回答和评论后,单元测试似乎确实是实现此功能的唯一途径。

我想知道的是,如果单元测试是 - 由于任何原因 - 不可能,因此IoC无法在编译时进行测试,这是否会阻止您使用IoC容器并选择直接实例化你的代码?我的意思是,您是否认为使用IoC和后期绑定太不安全和冒险,并认为其优势被这个“漏洞”所超越?

4 个答案:

答案 0 :(得分:10)

编译器无法验证整个程序的运行情况。您的程序编译的事实并不意味着它正常工作(即使不使用IoC)。为此,您需要自动化测试和手动测试。这并不意味着您不应该尽量让编译器尽可能多地执行此操作,但出于这个原因而远离IoC是不好的,因为IoC旨在使您的应用程序保持灵活,可测试和可维护。如果没有IoC,您将无法正确测试代码,如果没有任何自动化测试,几乎不可能编写任何合理大小的可维护软件。

然而,具有IoC提供的灵活性,确实意味着某些特定代码片段的依赖关系不再被编译器验证。所以你需要以另一种方式做到这一点。

某些DI框架允许您验证容器的正确性。例如,Simple Injector包含一个Verify()方法,它将简单地遍历所有注册并解析每个注册的实例。通过在应用程序启动期间调用此方法(或使用类似方法),您将在(开发人员)测试期间发现DI配置出现问题并且它将阻止应用程序启动。你甚至可以在单元测试中做到这一点。

重要的是,测试DI配置不需要太多维护。如果您必须为您注册的每种类型添加单元测试以验证容器,那么您将失败,因为缺少注册(因此缺少单元测试)将成为首先失败的原因。

这为您提供了“差不多”的编译时支持。但是,您需要了解应用程序的设计以及将事物连接在一起的方式。以下是一些提示:

  1. 远离隐式属性注入,如果无法找到已注册的依赖项,则允许容器跳过注入属性。这将禁止您的应用程序快速失败,并且稍后会导致NullReferenceException。显式属性注入,你强制容器注入属性是很好的,但是,尽可能使用构造函数注入。
  2. 如果可能,请明确注册所有根对象。例如,在容器中显式注册所有ASP.NET MVC Controller实例。这样,容器可以从根对象开始检查完整的依赖关系图。您应该以自动方式注册所有根对象,例如通过使用反射来查找所有根类型。例如,Simple Injector的MVC3 Integration NuGet Package包含一个RegisterMvcControllers扩展方法,可以为您执行此操作。其他容器的集成包包含类似的功能。
  3. 如果无法或不可能注册根对象,请在启动期间手动测试每个根对象的创建。例如,使用ASP.NET Web Form Page类,您可能会在其构造函数中调用容器(因为Page类必须具有默认构造函数)。这里的关键是使用反射一次性找到它们。通过使用反射查找所有Page类并实例化它们,您将发现(在应用程序启动或测试时),您的DI配置是否存在问题。
  4. 让您的IoC容器为您管理的所有服务都有一个公共构造函数。多个构造函数会导致歧义,并且可能以不可预测的方式破坏您的应用程序。拥有multiple constructors is an anti-pattern
  5. 有些情况下,在应用程序启动期间无法创建某些依赖项。为了确保应用程序可以正常启动并且仍然可以验证DI配置的其余部分,请在代理或抽象工厂后面抽象这些依赖项。

答案 1 :(得分:4)

  

我想知道的是,如果单元测试是 - 由于任何原因 - 不可能,因此IoC无法在编译时进行测试,这是否会阻止您使用IoC容器并选择直接实例化你的代码?

你在这里提出false choice:要么使用容器,要么“直接在你的代码中实例化”。但实际上你可以在没有任何容器的情况下练习依赖注入而不是这样做:

public void Main(string[] args)
{
    var container = new Container();
    // ... register various types here...

    // only Resolve call in entire application
    var program = container.Resolve<Program>(); 

    program.Run();
}

你这样做:

public void Main(string[] args)
{
    var c = new C();
    var b = new B(c);
    var a = new A(b);
    var program = new Program(a);
    program.Run();
}

您仍然可以通过这种方式获得依赖注入的所有优点:组件不会相互创建并且可以保持高度分离。组件的构造仍然是应用程序composition root的责任,即使在那里没有使用容器。您还可以获得所需的编译时检查。

还有两个评论:

  1. 你标记了你的问题dependency-injection,所以我假设你确实使用了依赖注入,而不是服务定位器模式。换句话说,我假设您没有在整个代码中公开和调用容器,这是不必要的not recommended。如果您使用Service Locator,我的答案将不再适用。请将其视为针对服务定位器的罢工,而不是针对我的回答。 Constructor injection是更好的选择。

  2. 程序员通常会选择使用容器而不是这种手动方法,因为在大型应用程序中,保持实例化顺序正确可能非常重要 - 可能需要进行大量重新排序某处有一个新的依赖。容器还提供额外的功能,使生活更轻松。

答案 2 :(得分:2)

您无法在构建时使用编译器测试所有代码。听起来很荒谬。无论如何,你必须编写不同类型的测试。容器的配置是集成测试的理想选择。您可以将测试配置为自动作为构建后事件运行,但这会增加构建时间。在任何情况下,拒绝IoC容器使用都是一个不好的理由。

答案 3 :(得分:0)

我已经使用roslyn SourceGenerators编写了一个编译时IOC容器,并且确实会在出现错误时提供编译时警告和错误。

当然,有时只能在运行时提供某些东西,但是有一些方法可以明确地做到这一点,这意味着如果缺少依赖项,我们仍然会给您错误。

https://github.com/YairHalberstadt/stronginject上查看