在类库中初始化IoC容器的好策略是什么?

本文关键字:策略 是什么 类库 初始化 IoC | 更新日期: 2023-09-27 18:13:27

我正在编写一个类库(c#),它将与我无法控制的应用程序一起分发。我的库也有点安全敏感,因此我不想允许调用应用程序为类库做任何依赖配置。它必须是自包含的,并初始化自己的依赖项。

同时,我希望它是可单元测试的和松散耦合的,并且我想使用IoC容器来管理依赖关系。目前,我正在使用内部构造函数和[InternalsVisibleTo()],以便单元测试可以进行手动注入。我更喜欢使用Ninject作为我的IoC容器,但我认为这与问题无关。

我正在努力想出一个好的策略来在生产中初始化我的IoC容器,因为类库并没有真正定义入口点,它有许多可以实例化的类,无法知道应用程序将首先使用哪个。

我想知道是否可能有某种AssemblyLoad事件,确实AppDomain似乎有这样的事件,但我的程序集必须已经加载到AppDomain之前,我甚至可以钩到它,所以我总是错过由我自己的程序集被加载引发的事件。我还考虑过使用静态初始化器或构造函数,但我不愿意用IoC容器设置污染每个可能的类,因为这会使它们与容器紧密耦合。我不想解耦我的代码,然后把它耦合到IoC容器。

我发现了一些其他的问题讨论这个话题,但没有一个真正处理我的情况。一个建议使用静态初始化器,另一个建议应用程序应该始终是组合的根。

还有别的办法吗?

在类库中初始化IoC容器的好策略是什么?

您的需求之间存在矛盾。

首先,出于安全考虑,您不希望使用组合根。其次,您希望依赖项由容器解析。但是由于标准库不控制容器,所以几乎任何东西都可以注入到代码中,包括试图从内部破坏任何东西的东西。

一种方法是明确你的依赖项,这样库客户端就负责提供你的依赖项。

namespace Library
{
    public class Foo1
    {
        //  a classical IoC dependendency
        public Foo1( IBar bar )
        {
        }
    }
}
另一方面,在初始化库时使用一个显式外部点的Composition根并不意味着库被污染了。CR与负责内部对象创建的本地工厂(又名依赖解析器)一起工作得很好,并且它是从CR内部设置的。
namespace Library
{
    public interface IFoo { }
    // local Foo factory, with a customizable provider
    public class FooFactory
    {
        private static Func<IFoo> _provider;
        public static void SetProvider( Func<IFoo> provider )
        {
           _provider = provider;
        }
        public IFoo CreateFoo()
        {
            return _provider();
        }
    } 
    // Bar needs Foo
    public class Bar
    {
        public void Something()
        {
            // you can use the factory here safely
            // but the actual provider is configured elsewhere
            FooFactory factory = new FooFactory();
            IFoo foo = factory.CreateFoo();
        }
    }
}

,然后在组合根目录(接近应用程序的入口点)

// kernel is set up to map IFoo to an implementation of your choice
public void ComposeRoot( IKernel kernel )
{
    FooFactory.SetProvider( () => kernel.Get<IFoo>() );
}

正如您所看到的,Local Factory不是在多个类中提供多个注入点,而是一个单一的注入点,它提供了整个库的干净配置,使其成为自包含的。

您甚至可以有一个不涉及任何IoC容器的提供者,而是创建一个具体的实现,从而使它在没有任何容器的情况下易于测试。切换到另一个IoC很简单,你只需要提供另一个提供者。

你的库越大,你的类越内聚(经常相互使用),使用本地工厂就越方便。你不需要在你的类之间重新抛出依赖(没有本地工厂,如果你的类A需要I,你的B需要A,然后自动B需要I),相反,你所有的类依赖于一个单一的工厂。

这里有两种情况:

A)你的消费者没有使用你的Ninject容器:

如果你不希望它们能够提供替代配置(或者注入内部类),你必须创建一个构造函数来自己解决这些依赖关系。这将是你的入口。

B)你的消费者正在使用你的Ninject容器:

你需要向你的消费者公开你的Ninject内核实例。如果你想隐藏正在使用Ninject的事实,可以将其包装在ServiceLocator中,或者直接暴露内核本身。在任何一种情况下,你都可以把它作为一个静态类的属性,作为你的入口点。

从内部角度来看,我更喜欢使用选项B,但作为第三方库的频繁消费者,我从未见过任何人公开他们的IoC容器,我也不希望他们这样做。我只是希望能够实例化一个类,而不担心任何内部实现或依赖关系。当然,你的里程可能会有所不同。