65.9K
CodeProject 正在变化。 阅读更多。
Home

由重构的 NP.IoCy 和 AutofacAdapter 容器实现的通用最小控制反转/依赖注入接口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (17投票s)

2022年12月18日

MIT

33分钟阅读

viewsIcon

27627

我将解释控制反转,并提出用于实现 IoC 框架的简单而强大的接口。

引言

本文的目的

控制反转/依赖注入的主要问题在于,它经常被不正确地使用,未能增加关注点分离,也未能轻易地替换应用程序的各个部分。

IoC/DI 框架的另一个问题是它们不必要地复杂。只有一小部分功能被有效地利用。IoC/DI 框架领域的一些最新发展并非非常积极(我将在文章稍后讨论)。

本文的主要目的是强调 IoC 中真正有用的特性,并描述如何利用它们来改进软件。

我还提出了一个最小但功能强大的 IoC/DI 特性集,足以满足任何构建良好的 IoC 应用程序。本文提供了该特性集的两个实现——一个使用新的 NP.IoCy 框架,另一个则围绕 Autofac 构建。

文章简述

本文将详细解释控制反转 (IoC) 和依赖注入 (DI):它们是什么,为什么以及何时需要它们。还将讨论各种现有的 IoC/DI 框架及其有用的功能。

我们将考虑 IoC 错误应用可能导致的几种反模式,并讨论如何避免它们。

提出了一种最小化的 IoC/DI 接口,其中包含一小组特性,使 IoC 功能强大但又保持简单。该接口包含在 NP.DependencyInjection 包中。

此最小 IoC 接口由 NP.IoCy 库实现(新版本 API 已改进)。NP.IoCy nuget 包占用的空间仅是 Autofac、Castle Windsor、Ninject 或 Microsoft.Extensions.DependencyInjection 包的极小一部分(不到 0.1)。

我多年前就构建了 NP.IoCy 的第一个版本,但新版本功能更强大,分离了可修改的 ContainerBuilder 和不可修改的 Container,并且方法名更符合 Autofac 和 Castle Windsor 等流行框架。

为了展示围绕其他流行 IoC 框架实现最小 IoC/DI 接口的简便性,我还提供了 NP.DependencyInjection.AutofacAdapter 中的另一个实现,其行为方式完全相同,但构建在 Autofac 包之上。

我将展示并解释适用于 NP.IoCyNP.DependencyInjection.AutofacAdaptor 包的示例。

什么是控制反转 (IoC) 和依赖注入 (DI)?

IoC 是一种将某些对象的创建权交给称为 IoC 容器的单例的技术。IoC 容器通常会生成并返回一个实现特定接口或 abstract 类的对象,因此 Container 创建对象的消费者不需要知道它们的真实实现——只需要知道它们实现的接口。

事实上,容器对象的消费者可能甚至无法访问实现这些对象的类型——这些类型可以从外部库动态加载。

为了请求 IoC 对象,消费者通常会将 IoC 对象所需的接口类型以及一个可选参数——key 传递给容器。

IConsumer iocObj = container.Resolve(typeof(IConsumer)) as IConsumer;

IConsumer iocObj = container.Resolve(typeof(IConsumer), key) as IConsumer;  

IConsumer 是消费者功能可见的 IoC 对象接口。我们称之为解析类型。传递给第二个版本的 container.Resolve(...) 方法的 key 参数称为解析键。最常见的 解析键 是枚举或 string。在此,为了简单起见,我们始终使用 string,而在实际生活中,使用枚举更好且更类型安全。

组合起来,(解析类型, 解析键) 对(包括 解析键null 的情况——相当于第一个版本的 container.Resolve(...) 方法没有键)以唯一的方式指示 IoC 容器创建对象。我们将这样的对称为完整解析键

重要提示:IoC 容器在某种意义上代表了一个从完整解析键到包含创建和填充相应对象指令的单元的映射(或字典)。

创建对象的实际类型(通常在对象创建后不再使用)称为对象的实现类型

请注意,key 是必需的,以便能够为相同的解析类型以不同的方式创建 IoC 对象。

同样,请注意 C# 泛型允许我们简化上述 Resolve(...) 方法,以处理解析类型在编译时已知的常见情况。

IConsumer iocObj = container.Resolve<IConsumer>();  

IConsumer iocObj = container.Resolve<IConsumer>(key);   

容器通常由另一个称为 ContainerBuilder 的对象创建。完整解析类型 到创建对象的指令的映射在 ContainerBuilder 中设置(创建此类映射的过程通常称为注册)。

以下是一个将实现类型 Org 注册到完整解析键 (typeof(IOrg), "MyOrg") 的非常简短的示例。

// create container builder
var containerBuilder = new ContainerBuilder();

// register (map) Full Resolving Key (typeof(IOrg), "MyOrg) into Org type
// the object of Org type will be created by its type's default constructor
containerBuilder.RegisterType<IOrg, Org>("MyOrg");

// create a container out of the container builder:
var container = containerBuilder.Build();

// resolve the IOrg object by the Full Resolution Key:
IOrg org = container.Resolve<IOrg>("MyOrg");

根据我们上面的定义,typeof(IOrg)解析类型"MyOrg" 是解析键typeof(Org)实现类型

我们将在下面的部分提供更多将对象注册到 ContainerBuilder 和从 Container 解析对象的示例,因此您现在不必完全理解。

请注意,ContainerBuilder 可以修改(其注册可以添加、删除或覆盖),但 Container 对象是不可修改的,以便它可以被多个线程访问而无需锁定。

IoC 和 DI 的用途

使用 IoC 容器创建对象的灵活性,允许我们根据需要修改容器以返回实现相同接口的不同对象。例如,如果您有 IoC 容器返回的后端代理,您可以即时用返回模拟结果的模拟后端替换它们,而无需对 IoC 对象消费者功能进行任何更改,以用于测试目的。通过容器替换对象实现被称为依赖注入

IoC 容器的另一个更重要的用途是将应用程序拆分为许多高度松散耦合(几乎独立)的动态加载部分(提高关注点分离)。这样的部分通常称为插件。这对于视觉应用程序非常重要,其中不同的视图和视图模型可以独立编码、测试和扩展,然后由 IoC 容器动态加载到公共视觉外壳中,成为同一应用程序的一部分。

插件对于服务器也很重要,服务器的功能也可以组装成(几乎)独立的插件,处理不同类型的请求。

分层对象组合

通常,当 IoC 容器返回一个对象时,它也会被递归地组合,即,它的某些属性值也由容器返回,如果这些值是复合对象,它们的属性也将从容器中设置。

考虑一个具体案例

public interface IOrg
{
    string OrgName { get; set; }

    IPerson TopManager { get; set; }
}

public interface IPerson
{
    string FirstName { get; set; }

    string LastName { get; set; }

    IAddress Address { get; set; }
}

public interface IAddress
{
    int StreetNumber { get; set; }

    string StreetName { get; set; }

    string City { get; set; } 
}

当您请求 IOrg 对象时(假设实现类中有适当的 Inject 属性),IoC 容器还将通过解析 IPerson 类型来填充其 TopManager 字段,并在 IPerson 对象中,通过解析 IAddress 类型来填充 Address 字段。

对象创建实现和模式

本小节内容

在本小节中,我将简要介绍设计和实现 IoC 框架的主要原则,这些原则是我为最小 IoC/DI 接口以及 NP.IoCyNP.DependencyInjection.AutofacAdapter 实现所使用的。下面将在未来的章节中提供更详细的 IoC/DI 接口及其用法的描述。

键对单元容器映射

如前所述,IoC 容器在某种意义上代表了一个从完整解析键((解析类型, 解析键) 对)到容器单元的映射(字典);每个单元提供创建和组合相应 IoC 对象(s) 的指令。

ContainerBuilder 和 Container

如前所述,这些单元在容器构建时由 ContainerBuilder 放置(注册)到容器中。

IContainer container = containerBuilder.Build();

在容器由 ContainerBuilder 构建之后,它就不可修改了——因此它是线程安全的,并且可以在多个线程中调用其 Resolve(...) 方法。如果需要,可以通过注册新单元或覆盖旧注册来修改 containerBuilder 对象,并从中创建另一个容器。能够创建具有某些修改的多个容器对于测试尤其重要。

Container.Resolve(...) 方法通过完整解析键在容器中查找相应的单元,然后调用 cell.GetObj(...) 方法来创建(如果需要)并返回 IoC 对象。完整解析键用于查找单元,并且单元确切地知道如何创建(如果需要)并返回对象。

具有不同生命周期模式的容器对象

就 IoC 对象的生命周期而言,有两种类型的单元——一种是创建并返回单例对象的,另一种是在每次调用 container.Resolve(...) 方法时创建并返回新(瞬时)对象的。

单例对象仅创建一次(在容器创建时或第一次调用 container.Resolve(...) 方法返回此类对象时)。单例单元在内部保留其单例对象,并在每次请求时返回相同的对象。单例对象的引用始终保留在其容器单元内,一旦创建,单例对象将一直存在直到容器一直存在——通常是应用程序的整个生命周期。

瞬时对象在每次调用 container.Resolve(...) 方法返回它们时创建和组合。容器不保留对这些对象的引用,它们的存在直到所有包含对其引用的对象都被移除。

一些框架——包括微软新推出的 Microsoft.Extensions.DependencyInjection——也允许拥有作用域对象和子作用域。作用域对象在同一作用域内类似于单例,但在不同作用域内则不同。根据我的经验,作用域生命周期是不需要的,并且仅用于(在一定程度上)弥补某些缺失的功能。我们将在下面讨论。

单元创建 IoC 对象的各种方式

单元可以通过对象的实现类型来创建对象。它选择一个构造函数,例如,由某个属性标记的构造函数并调用它来创建对象。如果构造函数没有参数,它将简单地调用 Activator.CreateInstance(Type objType) 来创建对象。如果构造函数有参数——它将首先解析这些参数,然后调用 Activator.CreateInstance(Type objType, new object[]{arg1, arg2, ... argN})

创建对象的另一种方法是调用一个 static 方法,例如,使用 Reflection API:MethodInfo.Invoke(...)。如果方法有参数,它们将首先由容器解析。

ContainerBuilder 描述

如前所述,单元以及完整解析键到单元的映射通常在 Container Builder 中定义。容器生成器通过其 ContainerBuilder.Build() 方法生成一个不可变的 Container。生成的容器不可修改(例如,以便它可以在多个线程中工作而无需锁定),但同一个 ContainerBuilder 对象可以被修改以创建另一个 Container 并进行一些修改。

创建 FullResolutionKey 和容器单元之间的映射的过程称为注册。因此,ContainerBulder 通常由许多 Register...(...) 方法组成,这些方法指定了 FullResolutionKey 和单元创建所需的参数。如果 FullResolutionKey 已存在,它将被新的单元参数覆盖。它还应该有一个 UnRegister(...) 方法来删除以前的注册。

使用 Attributes

我喜欢 MEF - 微软早期 IoC 组合框架的一点是它非常优雅地使用 Attributes 来组合对象。因此,我也决定在最小 IoC 接口中使用 Attributes。

这是一个用于存储和注入已注册类型的属性使用的非常简短的示例。

[RegisterType(isSingleton: true, resolutionKey:"MyConsoleLog")]
public class ConsoleLog : ILog
{
    public ConsoleLog()
    {

    }

    public void WriteLog(string info)
    {
        Console.WriteLine(info);
    }
}  

当注册为带属性的类型时(例如,通过调用 containerBuilder.RegisterAttributedClass(typeof(ConsoleLog));),它将创建一个 Singleton 单元,为 (typeof(ILog), "MyConsoleLog") 对表示的 FullResolutionKey 返回 ConsoleLog 类型的对象。

现在,要将其注入以成为另一个对象的一部分,我们可以使用 InjectAttribute,例如:

public class MyObj : IMyObj
{
   [Inject(resolvingType:typeof(ILog), resolutionKey:"MyConsoleLog")]
   public ILog MyLog { get; set; }
}

MyObj 对象由容器解析时,由于 InjectAttribute,它将把 ConsoleLog 注入到其 MyLog 属性中。由于 ILogMyLog 属性的类型,我们可以跳过在 InjectAttribute 中提及 resolvingType——以下代码仍然有效。

public class MyObj : IMyObj
{
   [Inject(resolutionKey:"MyConsoleLog")]
   public ILog MyLog { get; set; }
}  

现在我想展示另一个使用 static 方法生成的 Log 对象的示例。

public class FileLog : ILog
{
    const string FileName { get; };

    public FileLog(string fileName)
    {
        if (File.Exists(FileName))
        {
            File.Delete(FileName);
        }
    }

    public void WriteLog(string info)
    {
        using(StreamWriter writer = new StreamWriter(FileName, true))
        {
            writer.WriteLine(info);
        }
    }
}

[HasRegisterMethods]
public static class FactoryMethods
{
   [RegisterMethod(resolutionKey:"LogFileName", 
    isSingleton:true, ResolvingType = typeof(string))]
   public static string CreateFileName() => "MyLogFile.txt";

   [RegisterMethod(resolutionKey:"MyFileLog", isSingleton:true, 
    ResolvingType = typeof(string))]
   public static ILog GetFileLog([Inject(typeof(string), 
                      resolutionKey:"LogFileName"]string fileName)
   {
       return new FileLog(fileName);
   }
}

public class MyObj : IMyObj
{
   [Inject(resolutionKey:"MyFileLog")]
   public ILog MyLog { get; set; }
}  

通过 containerBuilder.RegisterAttributedStaticFactoryMethodsFromClass(typeof(FactoryMethods))FactoryMethods 类型添加到 ContainerBuilder 将注册两个单元:

  1. 映射到 (typeof(string), "LogFileName") 完整解析键的返回字符串 "MyLogFile.txt" 的单例单元。
  2. 映射到 (typeof(ILog), "MyFileLog") 完整解析键的返回 FileLog 对象,该对象写入 "MyLogFile.txt" 文件(从上一个单元注入)。

现在,如果 MyObj 对象由容器构建并返回,由于其 [Inject(resolutionKey:"MyFileLog")] 属性,它将把由 GetFileLog(...) 方法创建的 FileLog 注入为其 MyLog 属性的值。

staticFactiryMethods 上方的 HasRegisterMethod 属性只是为了指示在整个程序集加载到容器中时,需要扫描标记有 RegisterMethod 属性的 static 类。

IoC 反模式

缺乏关注点分离

在项目架构中使用 IoC/DI 模式的主要问题是使用它但没有充分利用它。我见过几个项目过度使用依赖注入,几乎在所有地方都使用了。这使得所有东西都依赖于所有东西,以至于:

  • 无法进行模拟,因为当你试图替换一个部分时,你还必须替换所有其他部分。
  • 无法测试、调试和扩展单个部分(例如,视图和视图模型),因为它们依赖于许多其他视图和视图模型。

在这种情况下——使用 IoC 是完全不必要的,因为它只会增加一层复杂性而没有带来任何好处。

事实上,IoC 容器返回的每个对象都应该易于模拟或几乎独立于其他对等对象(仅通过薄接口连接到其他对象),以便它可以独立地进行测试/调试/扩展,然后由 IoC 容器插入到公共外壳中。

服务定位器是反模式吗?

服务定位器模式——是全局访问 IoC 容器的能力。实现服务定位器的简单方法是提供 IoC 容器的静态引用。

大多数人认为服务定位器是反模式。这并不完全正确。服务定位器仅当容器组合和返回的对象使用容器本身的全局引用时才是反模式。 如果避免了这一点,容器的全局引用是可以的。

当然,如果项目的团队很大且不均衡,并且提供了容器的全局引用,那么有些人最终可能会在 IoC 容器对象中使用它,并且项目可能会因此变得混乱。有几种方法可以避免这种情况。

  1. 容器的全局引用应在可见性有限的项目中定义,例如在主项目中或在 IoC 对象接口和实现定义的区域不可见的某些其他项目中。总的来说,项目依赖项应完全由团队的架构师或技术负责人控制;不允许单个开发人员更改项目之间的依赖项,因此如果团队组织良好,这个条件可以很容易地强制执行。
  2. 通过彻底的代码审查。

当前 IoC/DI 框架的审查

以下 IoC 容器框架目前流行或过去流行过:

  1. Autofac
  2. Microsoft.Extensions.DependencyInjection
  3. Castle Windsor
  4. Ninject
  5. Microsoft MEF
  6. Microsoft Unity

以上所有框架都有相同的缺点——它们比良好的 IoC/DI 所需的复杂得多,并且提供了多得多的功能。其中只有一个——MEF——利用了使对象组合更简单、更优雅的 Attributes。

目前最流行的 IoC 框架无疑是 Microsoft.Extensions.DependencyInjection。从我的角度来看,它之所以流行,仅仅因为它是一个由微软推广的新框架,而不是因为它自身的优点。

除了与其他 IoC 框架有相同的问题(过于复杂)之外,它还有一个非常不幸的缺点。它不允许通过键解析对象——它只允许通过解析类型解析对象。因此,有两个不同的容器单元返回 IOrg 的两个不同实现将是一个问题。通常,人们会在 Microsoft.Extensions.DependencyInjection 功能之上添加按键解析功能作为自定义代码。这样的代码通常丑陋、庞大、充满错误,并且需要额外的维护以及团队中理解它的一个人。

最小 IoC/DI 接口简述

最小 IoC 接口的源代码位于 NP.DependencyInjection git 项目中,并且在 nuget.com 上有同名的 nuget 包。

最小 IoC/DI 接口提供了强大但仍然非常强大的 IoC/DI 功能,能够实现 IoC 提供的所有好处。

最小功能可以有不同的实现,特别是,我提供了两个实现:

  1. NP.IoCy——一个占用的磁盘空间非常小的实现——不到主要 IoC 框架(包括 Microsoft.Extensions.DependencyInjection、Autofac、MEF、Ninject、Unity、Castle Windsor)的 0.1%。
  2. NP.DependencyInjection.AutofacAdapter——一个基于 Autofac 的所有最小接口功能的实现。这适用于那些希望继续使用流行的 Autofac 而不是切换到更新且鲜为人知的 NP.IoCy 的人。

如果有需求,我还计划创建其他实现,包括 Winsdor Castle 和 MEF 的。

NP.DependencyInjection 功能包括两个 public 接口和 Attributes。

接口是:

  1. public interface IContainerBuilder<TKey> { ... }
  2. public interface IDependencyInjectionContainer<TKey> { ... }

设置泛型参数类型 TKey 允许将解析键限制为 TKey 类型(例如,stringenum)。通过使用 <object?> 泛型参数可以获得这些接口的非泛型版本。

  1. public interface IContainerBuilder : IContainerBuilder<object?>{ ... }
  2. public interface IDependencyInjectionContainer : IContainerBuilder<object?> { ... }

在未来,我们将同时使用这些接口的泛型和非泛型版本以及实现它们的类。

IContainerBuilder<TKey> 具有:

  1. 许多 void IContainerBuilder.Register...(...) 方法,允许注册单元。
  2. void IContainerBuilder.UnRegister(Type resolvingType, TKey? resolutionKey = default) 方法,允许注销(删除)对应于 FullResolutionKey (resovlingType, resolutionKey) 的单元。
  3. IDependencyInjectionContainer<TKey> IContainerBuilder.Build() 方法,返回一个不可变的 IDependencyInjectionContainer<TKey> 对象。IContainerBuilder<TKey> 仍然可以通过添加、删除或替换一些单元来修改,以便通过再次调用 IContainerBuilder.Build() 来生成另一个 Container 对象。

我将在后面的示例中对 IContainerBuilder 代码提供更详细的解释。

IDependencyInjectionContainer 接口的代码足够简单,可以完整发布:

namespace NP.DependencyInjection.Interfaces
{
    // unmodifyable IoC (DI) container that returns composed object
    public interface IDependencyInjectionContainer<TKey>
    {
        // composes all the properties marked with InjectAttribute 
        // for the object
        void ComposeObject(object obj);

        // returns (and if appropriate also composes) an object
        // corresponding to (resolvingType, resolutionKey) pair
        object Resolve(Type resolvingType, TKey resolutionKey = default);

        // returns (and if appropriate also composes) an object
        // corresponding to (typeof(TResolving), resolutionKey) pair
        TResolving Resolve<TResolving>(TKey resolutionKey = default);
    }
}  

NP.DependencyInjection 包提供了以下属性:RegisterTypeRegisterMethodInjectHasRegisterMethodsCompositeConstructorPluginAssembly 属性目前未使用)。以上所有属性(除了 CompositeConstructor 属性)已在 Attributes Usage 小节中解释。

CompositeConstructor 是一种用于标记类中单个构造函数的属性,该构造函数将用于在容器内构建该类的对象。如果构造函数有参数,这些参数应该用 Inject 属性标记,并且将在调用构造函数之前由容器解析。

演示实现最小 IoC/DI 接口的容器能力的示例

示例代码基本描述

有两种示例集——一种是为新的 NP.IoCy,另一种是为基于 Autofac 构建的 NP.DependencyInjection.AutofacAdapter。这两个容器框架都实现了最小 IoC 接口,并提供了基本相同的方法和行为。

容器对象及其接口分别定义在名为 ImplementationsInterfaces 的两个项目中。容器对象描述了一个简单的组织(class Org : IOrg {...}),其中包含一个类型为 IPersonManager 属性(类型为 Person 的实际实现)。IPerson 又包含类型为 IAddressAddress 属性。

容器对象和接口将在下面更详细地描述。

示例代码位置

示例代码位于 DependencyInjectionSamples 下。

Implementations 子文件夹包含 IoC 容器对象的实现,而 Interfaces 子文件夹包含接口。

两个子文件夹 IoCyTestsIoCyDynamicLoadingTests 包含 NP.IoCy 实现的测试。

两个子文件夹 AutofacAdapterTestsAutofacAdapterDynamicLoadingTests 包含 AutofacAdapter 实现的测试。

正如您可能从项目名称中注意到的,NP.IoCyAutofacAdapter 的测试都包含简单的测试和动态加载功能的测试。

容器对象接口和实现

此处,我们描述由我们的测试容器生成的类型以及它们实现的接口。

设想一个小组织,它有一个 Manager 和一个 ProjLead(类型为 IPerson)。组织对象还具有一个类型为 ILogLog 属性,用于写入日志消息,为了使其更复杂一些,它还有一个名为 Log2 的另一个 ILog 属性。

这是 IOrg 接口的代码:

public interface IOrg
{
    string? OrgName { get; set; }

    IPerson? Manager { get; set; }

    IPerson? ProjLead { get; set; }

    ILog? Log { get; set; }

    ILog? Log2 { get; set; }

    void LogOrgInfo();
}

这是 Org 实现的代码:

public class Org : IOrg
{
    [Inject]
    public IPerson? Manager { get; set; } public string? OrgName { get; set; } 

    public IPerson? ProjLead { get; set; }

    [Inject]
    public ILog? Log { get; set; }

    [Inject(resolutionKey:"MyLog")]
    public ILog? Log2 { get; set; }

    public void LogOrgInfo()
    {
        Log?.WriteLog($"OrgName: {OrgName}");
        Log?.WriteLog($"Manager: {Manager!.PersonName}");
        Log?.WriteLog($"Manager's Address: {Manager!.Address.City}, 
                     {Manager.Address.ZipCode}");
    }
} 

IPerson 具有 string 类型的 PersonName 属性和 IAddress 类型的 Address 属性。

public interface IPerson
{
    string? PersonName { get; set; }

    IAddress? Address { get; set; }
}

public class Person : IPerson
{
    public string? PersonName { get; set; }

    [Inject]
    public IAddress? Address { get; set; }
}

现在这是 IAddressAddress 接口和类的代码:

public interface IAddress
{
    string? City { get; set; }

    string? ZipCode { get; set; }
}

public class Address : IAddress
{
    public string? City { get; set; }

    public string? ZipCode { get; set; }
}

IOrg 中还使用了 ILog 接口。这是它的非常简单的代码:

public interface ILog
{
    void WriteLog(string info);
}  

ILog 有两个不同的实现:ConsoleLogFileLog

[RegisterType(isSingleton: true)]
public class ConsoleLog : ILog
{
    public ConsoleLog()
    {

    }

    public void WriteLog(string info)
    {
        Console.WriteLine(info);
    }
}

public class FileLog : ILog
{
    const string FileName = "MyLogFile.txt";

    public FileLog()
    {
        if (File.Exists(FileName))
        {
            File.Delete(FileName);
        }
    }

    public void WriteLog(string info)
    {
        using(StreamWriter writer = new StreamWriter(FileName, true))
        {
            writer.WriteLine(info);
        }
    }
}

为了演示和测试 CompositeConstructor 属性的使用,我们还有 IOrgGettersOnly 接口和实现它的 AnotherOrg 类,它依赖于构造函数来组合对象而不是属性。

[RegisterType(resolutionKey:"TheOrg")]
public class AnotherOrg : IOrgGettersOnly
{
    public string OrgName { get; set; } 

    public IPersonGettersOnly Manager { get; }

    public ILog Log { get; }

    [CompositeConstructor]
    public AnotherOrg([Inject(resolutionKey:"AnotherPerson")] 
           IPersonGettersOnly manager, [Inject]ILog log)
    {
        Manager = manager;
        Log = log;
    }

    public void LogOrgInfo()
    {
        Log?.WriteLog($"OrgName: {OrgName}");
        Log?.WriteLog($"Manager: {Manager!.PersonName}");
        Log?.WriteLog($"Manager's Address: {Manager!.Address.City}, 
                     {Manager.Address.ZipCode}");
    }
} 

为了展示组合的递归性,我们还有接口 IPersonGettersOnly 和实现它的类——AnotherPerson,它也依赖于其 CompositeConstructor 进行组合。

[RegisterType(resolutionKey:"AnotherPerson")]
public class AnotherPerson : IPersonGettersOnly
{
    public string PersonName { get; set; }

    public IAddress Address { get; }

    [CompositeConstructor]
    public AnotherPerson([Inject(resolutionKey: "TheAddress")] IAddress address)
    {
        this.Address = address;
    }
}  

NP.IoCy 测试

IoCyTests 解决方案(主要功能)

测试解决方案的代码位于 *IoCTests* 文件夹下。所有测试都在主 NP.Samples.IoCyTests 项目的 *Program.cs* 文件中。主项目依赖于 NP.Samples.ImplementationsNP.Samples.Interfaces 项目。它还依赖于 NP.IoCyFluentAssertions nuget 包。FluentAssertions 是进行简单功能断言测试所必需的。

NP.Samples.Implementations 项目依赖于 NP.Samples.Interface 项目和 NP.DependencyInjection 包(用于 IoC/DI 属性)。

主项目的 Program 类有三个辅助 static 方法(在 Main(...) 方法之上):

  1. bool IsSingleton<t>(this IDependencyInjectionContainer container, object? key = null )</t>——检查完整解析键为 (typeof(T), key) 的容器对象是否存在并且是单例。
  2. IOrg CreateOrg()——创建组织,填充其 OrgNameManagerLogManager.Address 属性,并为其赋值。
  3. IOrg CreateOrgWithArgument([Inject(resolutionKey: "TheManager")] IPerson person)——静态 CreateOrg(...) 方法的另一个版本,具有注入的 person 参数。
  4. void TestOrg(this IDependencyInjectionContainer container, bool isSingleton, object key = null)——测试解析键为 keyIOrg 对象是否为单例(取决于 bool 参数 isSingleton 的值),并测试所获得的 IOrg 对象的 OrgName 是否设置为 "Other Department Store"。

现在看看 void Main(...) 方法的开头;以下是我们如何填充 ContainerBuilder 并创建容器:

// create container builder with keys restricted to nullable strings
var containerBuilder = new ContainerBuilder<string?>();

// register Person object to be returned by IPerson resolving type
containerBuilder.RegisterType<IPerson, Person>();
containerBuilder.RegisterType<IAddress, Address>();
containerBuilder.RegisterType<IOrg, Org>();

// Register FilesLog as a singleton to be returned by ILog type
containerBuilder.RegisterSingletonType<ILog, FileLog>();

// register ConsoleLog to be returned by (ILog type, "MyLog" key) combination
containerBuilder.RegisterType<ILog, ConsoleLog>("MyLog");

// Create container
IDependencyInjectionContainer<string?> container1 = containerBuilder.Build();  

请注意,名称中不包含“Singleton”的 Register...(...) 方法会创建瞬时单元。

这里有几个带解释的例子:

containerBuilder.RegisterType<IPerson, Person>();  

将使用其默认构造函数创建的 Person 类型的瞬时对象注册,以返回给完整解析键 (typeof(IPerson), null)。请注意,解析键为 null,因此完整解析键本质上只包含解析类型:typeof(IPerson)

containerBuilder.RegisterSingletonType<ILog, FileLog>();

FileLog 类型的单例对象注册,以返回给 ILog 类型(映射没有解析键),并且 FileLog 对象将使用其默认构造函数创建。

containerBuilder.RegisterType<ILog, ConsoleLog>("MyLog");  

ConsoleLog 类型的瞬时对象注册,以返回给 (typeof(ILog), "MyLog") 对。

// resolve and compose organization
// all its injectable properties will be populated
// this stage. 
IOrg org = container1.Resolve<IOrg>();

// make sure ProjLead is null (it does not have
// InjectAttribute)
org.ProjLead.Should().BeNull();  

org.ProjLead 属性应该为 null,因为该属性没有 Inject 属性,不会被自动注入。

现在检查 Manager 属性不是 null(由于 [Inject] 属性,它应该已被填充)。

org.Manager.Should().NotBeNull();   

下面的代码解析 IPerson 并确保它与 org.Manager 不是同一个对象,因为 (IPerson, null) 没有映射到 Singleton 对象。

// get another IPerson object from the container
IPerson person = container1.Resolve<IPerson>();

// make sure that the new IPerson object is not the same 
// as org.Manager (since IPerson - Person combination was not 
// registered as a singleton: containerBuilder.RegisterType<IPerson, Person>();
person.Should().NotBeSameAs(org.Manager);

接下来的代码将获取 ILog 类型的解析,我们将确保它指向与 org.Log 相同的对象,因为 ILog 被映射到一个 Singleton 对象。

// Get another ILog (remember it was registered as a singleton:
//  containerBuilder.RegisterSingletonType<ILog, FileLog>();)
ILog log = container1.Resolve<ILog>();

// Since it is a singleton, the new log should 
// be the same as the org.Log
log.Should().BeSameAs(org.Log);  

现在让我们看看 OrgLog2 属性。请记住,它被注入了解析键 "MyLog":[Inject(resolutionKey:"MyLog")]public ILog? Log2 { get; set; },并且还记得我们注册了 ConsoleLog 以映射到 (typeof(ILog), "MyLog") 对:containerBuilder.RegisterType<ilog, consolelog="">("MyLog");</ilog,>。所以 Log2 应该被赋值为 ConsoleLog 对象,并且它不应该是单例。这正是我们接下来在代码中测试的内容。

// Log2 is injected by (ILog, "MyLog") type-key combination.
// This combination has been registered as a non-singleton:
// containerBuilder.RegisterType<ILog, ConsoleLog>("MyLog");
org.Log2.Should().NotBeNull();
org.Log2.Should().BeOfType<ConsoleLog>();

ILog log2 = container1.Resolve<ILog>("MyLog");

log2.Should().NotBeNull();

// the new log should not be the same as the old one
// since it is not a singleton.
log2.Should().NotBeSameAs(org.Log2);
log2.Should().BeOfType<ConsoleLog>();  

接下来,我们为组织及其经理提供一些值,并检查它们是否被 Log 属性注入的 FileLog 对象正确打印,并且文件 *MyLogFile.txt* 已在可执行文件所在的文件夹中创建并正确填充(您需要找到并打开文件进行检查)。

// assign some values to the organization's properties
org.OrgName = "Nicks Department Store";
org.Manager.PersonName = "Nick Polyak";
org.Manager.Address.City = "Miami";
org.Manager.Address.ZipCode = "12345";

// since org.Log is of FileLog type, these value
// will be printed to MyLogFile.txt file within the same
// folder as the executable
org.LogOrgInfo();

现在我们将注册一个 ConsoleLog 类型的单例实例对象,以映射到具有相同 containerBuildertypeof(ILog)

// replace ILog (formerly resolved to FileLog) to be resolved to 
// ConsoleLog
ConsoleLog consoleLog = new ConsoleLog();
containerBuilder.RegisterSingletonInstance<ILog>(consoleLog); 

请记住——它将覆盖包含非单例 FileLog 的旧单元,并替换为单例 ConsoleLog

然后我们从相同的容器生成器创建一个新的 Container

// and create another container from containerBuilder. This new container
// will reflect the change - instead of ILog resolving to FileLog
// it will be resolving to ConsoleLog within the new container - <code>container2</code>.
var container2 = containerBuilder.Build();  

然后我们再次获取一个新的 IOrg 对象,并检查其新的 Log 属性是否设置为单例 ConsoleLog(与之前的 FileLog 相反)对象。该对象实例应与上面创建的 consoleLog 字段相同。

// resolve org from another Container.
IOrg orgWithConsoleLog = container2.Resolve<IOrg>();

orgWithConsoleLog.Log.Should().NotBeNull();

// check that the resolved ILog is the same instance
// as the consoleLog used for the singleton instance.
orgWithConsoleLog.Log.Should().BeSameAs(consoleLog);  

我们可以为新的 orgWithConsoleLog 对象赋值,并看到它打印到控制台而不是文件。

// assign some data to the newly resolved IOrg object
orgWithConsoleLog.OrgName = "Nicks Department Store";
orgWithConsoleLog.Manager.PersonName = "Nick Polyak";
orgWithConsoleLog.Manager.Address.City = "Miami";
orgWithConsoleLog.Manager.Address.ZipCode = "12345";

// send org data to console instead of a file.
orgWithConsoleLog.LogOrgInfo();  

您需要通过调试器单步执行上述代码,以确保当调用 LogOrgInfo() 方法时,它会打印到控制台。

接下来,我们将使用 ContainerBuilder 注册 static 方法 IOrg CreateOrg()

containerBuilder.RegisterFactoryMethod(CreateOrg);  

请记住,该方法只是创建一个组织并为其填充一些数据。

public static IOrg CreateOrg()
{
    IOrg org = new Org();

    org.Manager = new Person();

    org.OrgName = "Other Department Store";
    org.Manager.PersonName = "Joe Doe";

    org.Manager.Address = new Address();
    org.Manager.Address.City = "Boston";
    org.Manager.Address.ZipCode = "12345";

    org.Log = new ConsoleLog();

    return org;
}  

由于未向 RegisterFactoryMethod 提供 resolutionKey 参数,它将简单地将工厂方法映射到 typeof(IOrg) 类型,覆盖之前的注册。因此,一旦我们创建一个新的容器,并对其调用 container3.Resolve<IOrg>(),工厂方法 CreateOrg 将被用来创建瞬时 IOrg 对象。

// create a container with registered CreateOrg method
var container3 = containerBuilder.Build();

// test that organization is not a singleton and has its
// properties correctly populated
container3.TestOrg(false);  

调用 container3.TestOrg(false) 将精确地测试从新容器解析的 IOrg 对象是瞬时的(因为 TestOrg(...) 方法的第一个参数为 false),并且其 OrgName 被设置为 "Other Department Store"(如 CreateOrg() 方法所设置)。

接下来,让我们确保我们可以将工厂方法注册到具有非 null ResolutionKey(解析类型, 解析键) 对。

containerBuilder.RegisterFactoryMethod(CreateOrg, "TheOrg"); 
var container4 = containerBuilder.Build();

我们已将相同的工厂方法 CreateOrg() 注册到 (typeof(IOrg), "TheOrg") 对。

现在测试新的单元是否映射到瞬时 CreateOrg 方法。

container4.TestOrg(false, "TheOrg" /* the resolution key */); 

请注意,我们没有覆盖任何内容,因为该对之前未被用作键。旧的注册映射类型 typeof(IOrg) 仍然存在。让我们也验证一下。

container4.TestOrg(false); // should still work because 
                           // the old registration is also there  

现在,让我们将 IOrg(没有 resolutionKey)重新注册为单例

containerBuilder.RegisterSingletonFactoryMethod(CreateOrg);  

这将覆盖旧的注册,因此当我们创建一个新的 Container 时,它将使用 CreateOrg() 方法解析 typeof(IOrg) 来生成一个单例对象(与之前的瞬时对象相反)。

IDependencyInjectionContainer<string?> container5 = containerBuilder.Build();
// test that the resulting IOrg is a singleton
container5.TestOrg(true /* test for a singleton */);  

true 作为第一个参数传递给 container.TestOrg(...) 方法将测试单例。

现在,让我们用单例覆盖带键(命名)的单元,并测试新容器将为此单元生成一个单例对象。

containerBuilder.RegisterSingletonFactoryMethod(CreateOrg, "TheOrg");

// create the container
var container6 = containerBuilder.Build();

// make sure the result is a singleton
container6.TestOrg(true, "TheOrg" /* resolution key*/);  

请注意,另一个映射类型 typeof(IOrg)(没有 resolutionKey)应该仍然存在,所以我们也可以测试。

container6.TestOrg(true);    

现在,我们将测试可以从容器生成器中注销单元,以便新容器在尝试解析这些单元时返回 null。我们将注销解析类型设置为 IOrg 的两个单元,一个带键,一个不带键。

// unregister FactoryMethod pointed to by  IOrg
containerBuilder.UnRegister(typeof(IOrg));

// unregister FactoryMethod pointed to by (IOrg, "TheOrg") pair
containerBuilder.UnRegister(typeof(IOrg), "TheOrg"); 

构建新容器并测试返回的 IOrg 对象对于这两个注册都是 null

var container7 = containerBuilder.Build();

org = container7.Resolve<IOrg>();
org.Should().BeNull();

org = container7.Resolve<IOrg>("TheOrg");
org.Should().BeNull();

接下来,我们将测试注册其反射 MethodInfo 的方法。这种注册/解析类型的强大之处在于,方法参数(用 [Inject(...)] 属性标记)是递归解析的。

我们将使用方法 Program.CreateOrgWithArgument(IPerson person) 作为示例。

public static IOrg CreateOrgWithArgumen
(
    [Inject(resolutionKey:"TheManager")]IPerson person)
{
    return new Org { OrgName = "Other Department Store", Manager = person };
}  

请注意,由于新的 CreateOrgWithArgument(...) 方法注入了映射到 (typeof(IPerson), "TheManager") 对的属性,因此我们将不得不注册一个映射到 (typeof(IPerson), "TheManager") 完整解析键的单元。

MethodInfo createOrgMethodInfo =
    typeof(Program).GetMethod(nameof(CreateOrgWithArgument));

// register the singleton Person cell for (typeof(IPerson), "TheManager") pair
// to be injected as the argument to CreateOrgWithArgument method
containerBuilder.RegisterSingletonType<IPerson, Person>("TheManager");

// register factory methods by their MethodInfo
containerBuilder.RegisterSingletonFactoryMethodInfo<IOrg>(createOrgMethodInfo, "TheOrg");
IDependencyInjectionContainer container8 = containerBuilder.Build();
container8.TestOrg(true, "TheOrg"); // test the resulting org is a singleton  

上面的代码示例测试了 containerBuilder.RegisterSingletonFactoryMethodInfo(...) 方法是否正常工作并生成了一个 Singleton 单元。

接下来,我们将测试 containerBuilder.RegisterFactoryMethodInfo(...) 方法,该方法应生成一个 Transient 单元。

containerBuilder.RegisterFactoryMethodInfo<IOrg>(createOrgMethodInfo, "TheOrg");
IDependencyInjectionContainer<string?> container9 = containerBuilder.Build();
container9.TestOrg(false, "TheOrg"); // test the resulting org is not a singleton  

现在,让我们测试 ContainerBuilder.RegisterAttributedClass(Type type) 方法。此方法接受整个类并检查它是否具有 RegisterType 属性。如果没有,它将抛出异常。如果找到该属性,它将根据属性的参数创建一个容器单元。例如,我们的 AnotherOrg 类带有 RegisterType 属性标记:

[RegisterType(resolutionKey:"MyOrg")]
public class AnotherOrg : IOrgGettersOnly
{

}

基于此属性,容器生成器将创建一个瞬时单元,映射到 (typeof(IOrgGettersOnly), "MyOrg") 完整解析键。

[CompositeConstructor] 属性标记的构造函数将被选用于构建对象。如果没有任何构造函数用此类属性标记,则将使用默认构造函数。

这是我们的 AnotherOrg 类的构造函数:

[CompositeConstructor]
public AnotherOrg
(
    [Inject(resolutionKey:"AnotherPerson")] IPersonGettersOnly manager, 
    [Inject] ILog log)
{
    Manager = manager;
    Log = log;
}

Inject 标记的参数将被递归组合。

这是我们测试的构建方式:

// create a brand new container builder for building types with RegisterType attribute
var attributedTypesContainerBuilder = new ContainerBuilder<string?>();

// RegisterTypeAttribute will have parameters specifying the resolving type 
// and resolution Key (if applicable). It will also specify whether the
// cell should be singleton or not. 
attributedTypesContainerBuilder.RegisterAttributedClass(typeof(AnotherOrg));
attributedTypesContainerBuilder.RegisterAttributedClass(typeof(AnotherPerson));
attributedTypesContainerBuilder.RegisterAttributedClass(typeof(ConsoleLog));
attributedTypesContainerBuilder.RegisterType<IAddress, Address>("TheAddress");

// create container
var container10 = attributedTypesContainerBuilder.Build();  

我们创建一个全新的 ContainerBuilder,然后使用其 RegisterAttributedClass 方法来构建基于其属性的容器对象。这是 AnotherPerson 类的 RegisterType 属性和复合构造函数:

[RegisterType(resolutionKey:"AnotherPerson")]
public class AnotherPerson : IPersonGettersOnly
{
    public string PersonName { get; set; }

    public IAddress Address { get; }

    [CompositeConstructor]
    public AnotherPerson([Inject(resolutionKey: "TheAddress")] IAddress address)
    {
        this.Address = address;
    }
}

这是 ConsoleLogRegisterType 属性:

[RegisterType(isSingleton: true)]
public class ConsoleLog : ILog
{

}

ConsoleLog 是一个单例,基于其 RegisterType 属性参数。

现在让我们回到主 *Program.cs* 文件:

// get the organization also testing the composing constructors
IOrgGettersOnly orgGettersOnly =
    container10.Resolve<IOrgGettersOnly>("MyOrg");

// make sure that Manager and Address are not null
orgGettersOnly.Manager.Address.Should().NotBeNull();

// make sure ILog is a singleton.
container10.IsSingleton<ILog>().Should().BeTrue();  

我们获取 IOrgGettersOnly 对象(解析为 AnotherOrg),确保其 org10.Manager.Address 属性不为 null。同时也要确保该容器中的 ILog 是单例。

最后,让我们测试 containerBuilder.RegisterAttributedStaticFactoryMethodsFromClass(Type classContainingStaticFactoryMethods) 方法。此方法接受一个类,其中包含一些带有 RegisterMethod(...) 属性的 static 工厂方法。这些方法根据 RegisterMethod(...) 参数转换为容器单元。带属性的 static 工厂方法位于 NP.Samples.Implementations 项目下的 FactoryMethods 类中。这是它的代码:

[HasRegisterMethods]
public static class FactoryMethods
{
    [RegisterMethod(resolutionKey: "TheAddress")]
    public static IAddress CreateAddress()
    {
        return new Address { City = "Providence" };
    }

    [RegisterMethod(isSingleton: true, resolutionKey: "TheManager")]
    public static IPerson CreateManager
    (
         [Inject(resolutionKey: "TheAddress")] IAddress address)
    {
        return new Person { PersonName = "Joe Doe", Address = address };
    }

    [RegisterMethod(resolutionKey: "TheOrg")]
    public static IOrg CreateOrg([Inject(resolutionKey: "TheManager")] IPerson person)
    {
        return new Org { OrgName = "Other Department Store", Manager = person };
    }
}  

工厂方法为 IOrgIPersonIAddress 类型创建三个命名单元。IPerson 单元是单例——其余两个是瞬时

这是测试代码:

IContainerBuilder<string?> containerBuilder11 = new ContainerBuilder<string?>();

containerBuilder11.RegisterAttributedStaticFactoryMethodsFromClass
                   (typeof(FactoryMethods));

var container11 = containerBuilder11.Build();

IOrg org11 = container11.Resolve<IOrg>("TheOrg");

// check that the org11.OrgName was set by the factory method 
// to "Other Department Store"
org11.OrgName.Should().Be("Other Department Store");

// check that the org11.Manager.PersonName was set by the factory method to "Joe Doe"
org11.Manager.PersonName.Should().Be("Joe Doe");

// Check that the org11.Manager.City is "Providence"
org11.Manager.Address.City.Should().Be("Providence");

// get another org
IOrg anotherOrg11 = container11.Resolve<IOrg>("TheOrg");

// test that it is not the same object as previous org
// (since org is transient)
org11.Should().NotBeSameAs(anotherOrg11);

// test that the manager is the same between the two orgs
// because CreateManager(...) creates a singleton
org11.Manager.Should().BeSameAs(anotherOrg11.Manager);

// get another address
IAddress address11 = container11.Resolve<IAddress>("TheAddress");

// test that the new address object is not the same
// since CreateAddress(...) is not Singleton
address11.Should().NotBeSameAs(org11.Manager.Address); 

再次,我们创建一个全新的 ContainerBuilder,然后使用方法 containerBuilder11.RegisterAttributedStaticFactoryMethodsFromClass(typeof(FactoryMethods)) 来根据 RegisterMethod 属性创建单元。

然后我们测试对象的属性,确保它们的值与工厂方法设置的值相同。

IoCyTests 解决方案续(多单元功能)

最近,我添加了允许将多个已注册对象合并到一个对象集合中的新功能——多单元功能。我相应地向 IoCyTests 解决方案的主程序添加了测试以演示和测试此类功能。那些使用过 MEF 的人可能会看到一些与 ImportMany 属性的相似之处。在某些方面它很相似,但构建得更合理。特别是在 NP.IoCy 中,属性或参数注入不必为多单元而不同,因此在注入阶段,软件不需要知道它是在处理多单元还是一个返回项目集合的常规单元。区别仅在于注册多单元时:然后我们使用包含 MultiCell 名称的方法或属性。

请注意,所有注册为多单元部分(components)的单元都是单例。

请注意,多单元功能仅是 NP.IoCy 的一部分——我没有为 Autofac 重现它(至少目前还没有)。

多单元示例(来自同一个 *Program.cs* 文件)开始展示如何将多个字符串合并到一个多单元中,该多单元返回这些 string 的集合。

// MultiCell tests 

var containerBuilder12 = new ContainerBuilder<string>(true);

// CellType is typeof(string), resolutionKey is "MyStrings" 

// example of a single string object "Str1" added to the multi-cell
containerBuilder12.RegisterMultiCellObjInstance(typeof(string), "Str1", "MyStrings");

// example of a colleciton of string objects - {"Str2", "Str3" } 
// added to the multi-cell
containerBuilder12.RegisterMultiCellObjInstance
         (typeof(string), new[] { "Str2", "Str3" }, "MyStrings");

// build the container
var container12 = containerBuilder12.Build();

// Get the collection - of strings containing 
// all strings above - { "Str1", "Str2", "Str3" }
IEnumerable<string> myStrings = container12.Resolve<IEnumerable<string>>("MyStrings");

// test that the collection is correct
myStrings.Should().BeEquivalentTo(new[] { "Str1", "Str2", "Str3" });

请注意,在上面的代码中,我们首先使用 RegisterMultiCellObjInstance 来注册单个对象 "Str1",然后注册一个对象数组——new[]{"Str2", "Str3"}。另请注意,传递给方法的 CellType 参数设置为 typeof(string)CellType 指定结果集合中单个单元的类型。由于我们使用相同的 CellType 和相同的 resolutionKey"MyStrings"),RegisterMultiCellObjInstance(...) 的两个调用结果都映射到同一个多单元。结果集合的类型将是 IEnumerable<CellType>,或在本例中——IEnumerable<string>。这就是为什么我们通过传递 IEnumerable<string> 类型参数来解析它: IEnumerable<string> myStrings = container12.Resolve<IEnumerable<string>>("MyStrings");。

最后一行——验证解析出的字符串集合确实包含 "Str1""Str2""Str3" 值。

下一个示例展示了如何创建一个由 ILog 接口的两个不同实现组成的多单元ConsoleLogFileLog。这与 MEF 中的 ImportMany 属性的作用非常相似。代码如下:

// now test putting different log types in a multi-cell:
var containerBuilder13 = new ContainerBuilder<string>(true);

// our multicell has CellType - typeof(ILog) and resolutionKey - "MyLogs"

// we set two objects to the cell - one of ConsoleLog and the other of FileLog types
containerBuilder13.RegisterMultiCellType<ILog, ConsoleLog>("MyLogs");
containerBuilder13.RegisterMultiCellType<ILog, FileLog>("MyLogs");

// get the container 
var container13 = containerBuilder13.Build();

// get the logs
IEnumerable<ILog> logs = container13.Resolve<IEnumerable<ILog>>("MyLogs");

// check there are two ILog objects within the collection
logs.Count().Should().Be(2);

// check that the first object is of type ConsoleLog
logs.First().Should().BeOfType<ConsoleLog>();

// check that the second object is of type FileLog
logs.Skip(1).First().Should().BeOfType<FileLog>();

RegisterMultiCellType<ILog, ...>("MyLogs") 的两个调用都具有相同的 CellTypeILog)和 resolutionKey"MyLogs"),因此这两个结果条目都映射到同一个多单元。第一个条目创建一个 ConsoleLog 类型的对象,第二个创建 FileLog 类型的对象。然后,像往常一样,我们构建容器并调用 Resolve 方法,传递集合类型(IEnumerable<ILog>)和 resolutionKey"MyLogs"): IEnumerable<ILog> logs = container13.Resolve<IEnumerable<ILog>>("MyLogs");。最后,我们测试结果集合确实包含两个元素,其中一个类型为 ConsoleLog,另一个类型为 FileLog

最后一个示例展示了如何使用多单元属性——RegisterMultiCellTypeRegisterMultiCellMethod。带属性且注入的代码位于 *OrgContainer.cs* 文件中。

// using this attribute, an object of type MyOrg with OrgName set to "MyOrg1",
// will be created and made part of the Multi-Cell
// defined by CellType - IOrg and resolutionKey "TheOrgs"
[RegisterMultiCellType(cellType: typeof(IOrg), "TheOrgs")]
public class MyOrg : Org
{
    public MyOrg()
    {
        OrgName = "MyOrg1";
    }
}

[HasRegisterMethods]
public static class OrgFactory
{
    // returns a single Org object with OrgName set to "MyOrg2"
    [RegisterMultiCellMethod(typeof(IOrg), "TheOrgs")]
    public static IOrg CreateSingleOrg()
    {
        return new Org { OrgName = "MyOrg2" };
    }

    // returns an array of two objects with OrgNames 
    // "MyOrg3" and "MyOrg4" correspondingly
    [RegisterMultiCellMethod(typeof(IOrg), "TheOrgs")]
    public static IEnumerable<IOrg> CreateOrgs()
    {
        return new IOrg[]
        {
            new Org { OrgName = "MyOrg3" },
            new Org { OrgName = "MyOrg4" }
        };
    }
}

public class OrgsContainer
{
    public IEnumerable<IOrg> Orgs { get; }

    // injects the constructor with orgs argument of resolving type IEnumerable<IOrg>
    // and resolutionKey - "TheOrgs" that point us to the MultiCell created above. 
    [CompositeConstructor]
    public OrgsContainer([Inject(resolutionKey: "TheOrgs")] IEnumerable<IOrg> orgs)
    {
        Orgs = orgs;
    }
}

当带属性的类注册到容器生成器后,它们将创建一个指向 CellType IOrgresolutionKey "TheOrgs" 的多单元。将有 4 个单元——每个单元都实现 IOrg 接口,其中包含 OrgName 属性,分别设置为 "MyOrg1"、"MyOrg2""MyOrg3""MyOrg4"OrgsContainer 类的 CompositeConstructor 将多单元的内容注入到类构造函数的 orgs 参数中,该参数被赋值给类的 Orgs 属性。

这是 *Program.cs* 代码,它读取这些类和带属性的方法,创建多单元并将其注入到 OrgsContainer 对象中:

// create a containerBuilder for testing MultiCell attributes
var containerBuilder14 = new ContainerBuilder<string>(true);

// MyOrg class has 
// [RegisterMultiCellType(cellType: typeof(IOrg), "TheOrgs")] attribute
// that means that an object of that type is created with a cell 
// with type IOrg and resolutionKey "TheOrgs"
// It creates an organization with OrgName - "MyOrg1"
containerBuilder14.RegisterAttributedClass(typeof(MyOrg));

// OrgFactory is a static class containing two attributed methods: 
// CreateSingleOrg() - creating a single org with OrgName - "MyOrg2"
// and CreateOrgs() - creating an array of two organizations 
// with OrgNames "MyOrg3" and "MyOrg4"
containerBuilder14.RegisterAttributedStaticFactoryMethodsFromClass(typeof(OrgFactory));

// OrgsContainer has a property Orgs set via a constructor
// The constructor has a single argument injected via the resolving type 
// IEnumerable<IOrg> and resolutionKey "TheOrgs":
// [Inject(resolutionKey: "TheOrgs")] IEnumerable<IOrg> orgs
containerBuilder14.RegisterType<OrgsContainer, OrgsContainer>();

// build the container
var container14 = containerBuilder14.Build();

// resolve the orgs container, its Orgs property should 
// be populated with the content of the MultiCell
var orgsContainer = container14.Resolve<OrgsContainer>();

// get the org names
var orgNames = orgsContainer.Orgs.Select(org => org.OrgName).ToList();

// test that the orgNames collection contains all the organizations - 
// { "MyOrg1", "MyOrg2", "MyOrg3", "MyOrg4" }
orgNames.Should().BeEquivalentTo(new[] { "MyOrg1", "MyOrg2", "MyOrg3", "MyOrg4" });

我们使用 containerBuilder.RegisterAttributedClass(...) 方法来读取带属性的类 MyOrg,并使用 containerBuilder.RegisterAttributedStaticFactoryMethodsFromClass(...) 来读取带有带属性工厂方法的 static 类。我们还添加了一个创建 OrgsContainer 类的瞬时单元:containerBuilder14.RegisterType<OrgsContainer, OrgsContainer>();。然后我们构建容器,并从中解析 OrgsContainer 对象:var orgsContainer = container14.Resolve<OrgsContainer>();。最后,我们检查对象的 Orgs 属性确实填充了 4 个对象——每个对象都实现 IOrg 接口,并且具有相应的 "MyOrg1""MyOrg2""MyOrg3""MyOrg4" OrgNames

IoCyDynamicLoadingTests 解决方案

该 *IoCyDynamicLoadingTests.sln* 位于同名文件夹下。其目的是测试一个非常重要的场景:容器创建属于动态加载库的对象(主程序不知道对象的确切类型,并且只能通过主程序已知的接口来控制这些对象)。

此示例演示了主应用程序动态加载单个插件的场景。

打开解决方案资源管理器并仔细查看依赖项。

主项目 NP.Samples.IoCyDynamicLoadingTests 不再依赖于 NP.Samples.Implementations 项目。

请注意,NP.Samples.Implementations 项目有一个生成后事件:

xcopy "$(OutDir)" 
      "$(SolutionDir)\bin\$(Configuration)\net6.0\Plugins\$(ProjectName)\" /S /R /Y /I  

此生成后事件会将包含构建结果的文件夹的内容复制到 *IoCyDynamicLoadingTests\bin\Debug\net6.0* 文件夹下的 *Plugins\NP.Sample.Implementation* 文件夹中,该文件夹包含可执行文件。如果 *Plugins\NP.Sample.Implementation* 文件夹不存在,它将被创建。

这是 Program.Main(...) 方法的代码:

static void Main(string[] args)
{
    // create container builder
    var builder = new ContainerBuilder<string?>();

    builder.RegisterPluginsFromSubFolders("Plugins");

    // create container
    var container = builder.Build();

    // resolve and compose organization
    // all its injectable properties will be injected at
    // this stage. 
    IOrgGettersOnly orgWithGettersOnly = container.Resolve<IOrgGettersOnly>("MyOrg");

    // set values
    orgWithGettersOnly.OrgName = "Nicks Department Store";
    orgWithGettersOnly.Manager.PersonName = "Nick Polyak";
    orgWithGettersOnly.Manager.Address!.City = "Miami";
    orgWithGettersOnly.Manager.Address.ZipCode = "12245";

    // print to console.
    orgWithGettersOnly.LogOrgInfo();

    IOrg org = container.Resolve<IOrg>("TheOrg");

    // test that the properties values
    org.OrgName.Should().Be("Other Department Store");
    org.Manager.PersonName.Should().Be("Joe Doe");
    org.Manager.Address.City.Should().Be("Providence");

    IOrg anotherOrg = container.Resolve<IOrg>("TheOrg");

    // not a singleton
    org.Should().NotBeSameAs(anotherOrg);

    // singleton
    org.Manager.Should().BeSameAs(anotherOrg.Manager);

    IAddress address2 = container.Resolve<IAddress>("TheAddress");

    // not a singleton
    address2.Should().NotBeSameAs(org.Manager.Address);

    Console.WriteLine("The END");
}  

我们使用方法 builder.RegisterPluginsFromSubFolders("Plugins") 将 "Plugins" 文件夹下所有子文件夹中的所有公共带属性对象注册到 ContainerBuilder。然后我们获取 IOrgGettersOnly 对象并测试它是否由复合构造函数填充。

然后我们获取 IOrg 对象,并测试它确实由工厂方法填充。我们还测试 IPerson单例,而 IOrgIAdress 对象是瞬时

多个插件测试

演示加载多个插件的示例位于 PluginsTest 解决方案下。在此,我们有 MainProgram 项目,它运行两个插件的 Main(...) 方法,每个插件都在自己的项目中:

  1. DoubleManipulationPlugin——提供两个操作 doubles 的方法:Plus(...) 用于对两个数字求和,Times(...) 用于对两个数字求积。
  2. StringManipulationPlugins 也提供两个字符串操作方法:Concat(...)——用于连接两个字符串,Repeat(...)——用于重复一个 string 几次。

这两个插件彼此不依赖,主项目也没有依赖它们。相反,两个插件和主项目都依赖于包含两个接口的 PluginInterfaces 项目,每个接口对应一个插件。

public interface IDoubleManipulationsPlugin
{
    double Plus(double number1, double number2);

    double Times(double number1, double number2);
}

public interface IStringManipulationsPlugin
{
    string Concat(string str1, string str2);

    string Repeat(string str, int numberTimesToRepeat);
}

插件 DLL 和可执行文件通过其生成后事件复制到主项目输出文件夹中 plugins 子文件夹下的同名文件夹中。

现在看看主程序:

static void Main(string[] args)
{
    // create container builder
    var builder = new ContainerBuilder<string?>();

    // load plugins dynamically from sub-folders of Plugins folder
    // localted under the same folder that the executable
    builder.RegisterPluginsFromSubFolders("Plugins");

    // build the container
    IDependencyInjectionContainer<string?> container = builder.Build();

    // get the pluging for manipulating double numbers
    IDoubleManipulationsPlugin doubleManipulationsPlugin = 
        container.Resolve<IDoubleManipulationsPlugin>();

    // get the result of 4 * 5
    double timesResult = 
        doubleManipulationsPlugin.Times(4.0, 5.0);

    // check that 4 * 5 == 20
    timesResult.Should().Be(20.0);

    // get the result of 4 + 5
    double plusResult = doubleManipulationsPlugin.Plus(4.0, 5.0);

    // check that 4 + 5 is 9
    plusResult.Should().Be(9.0);

    // get string manipulations plugin
    IStringManipulationsPlugin stringManipulationsPlugin = 
        container.Resolve<IStringManipulationsPlugin>();

    // concatinate two strings "Str1" and "Str2
    string concatResult = stringManipulationsPlugin.Concat("Str1", "Str2");

    // verify that the concatination result is "Str1Str2"
    concatResult.Should().Be("Str1Str2");

    // repeast "Str1" 3 times
    string repeatResult = stringManipulationsPlugin.Repeat("Str1", 3);

    // verify that the result is "Str1Str1Str1"
    repeatResult.Should().Be("Str1Str1Str1");

    Console.WriteLine("The END");
}

插件从它们的文件夹中动态加载,并根据它们的接口使用和测试。

由于插件之间没有依赖关系,主程序也没有直接依赖插件,因此每个插件都可以独立开发、调试和测试,例如,通过另一个测试项目或主项目,然后插件可以组装起来并在 NP.IoCy 框架的帮助下协同工作。

带有多单元的多个插件

当一个多单元将多个插件中的各种单元组合起来时,多单元功能最为有用。这就是为什么我在多个插件示例中也添加了一个多单元示例。请查看 DoubleManipulationsPlugin 项目中的文件 DoubleManipulationFactoryMethods.csStringManipulationsPlugin 项目中的文件 StringManipulationFactoryMethods.cs

DoubleManipulationFactoryMethods.cs:

[HasRegisterMethods]
public static class DoubleManipulationFactoryMethods
{
    [RegisterMultiCellMethod(cellType:typeof(string), resolutionKey:"MethodNames")]
    public static IEnumerable<string> GetDoubleMethodNames()
    {
        return new[]
        {
            nameof(IDoubleManipulationsPlugin.Plus),
            nameof(IDoubleManipulationsPlugin.Times)
        };
    }
}

StringManipulationFactoryMethods.cs:

[HasRegisterMethods]
public static class StringManipulationFactoryMethods
{
    [RegisterMultiCellMethod(cellType:typeof(string), resolutionKey:"MethodNames")]
    public static IEnumerable<string> GetStringMethodNames()
    {
        return new[]
        {
            nameof(IStringManipulationsPlugin.Concat),
            nameof(IStringManipulationsPlugin.Repeat)
        };
    }
}

它们每个都返回一个名称数组,这些名称是来自相应插件的方法名称,映射到一个多单元,其中 CellTypestringresolutionKey"MethodNames"

现在,在 *Program.cs* 文件中,我们通过解析类型 IEnumerable<string>resolutionKey - "MethodNames" 来查找这些方法名称。

var methodNames = container.Resolve<IEnumerable<string>>("MethodNames");

methodNames.Count().Should().Be(4);

methodNames.Should().Contain(nameof(IDoubleManipulationsPlugin.Plus));
methodNames.Should().Contain(nameof(IDoubleManipulationsPlugin.Times));
methodNames.Should().Contain(nameof(IStringManipulationsPlugin.Concat));
methodNames.Should().Contain(nameof(IStringManipulationsPlugin.Repeat));

然后我们检查结果是否包含 4 个条目对应 4 个方法名称,并且每个方法都存在。

NP.DependencyInjection.AutofacAdapterTests

Autofact 的测试解决方案 AutofacAdapterTests.slnAutofacAdapterDynamicLoadingTests.sln 包含在 NP.Samples 存储库的同名文件夹下的 NP.Samples 文件夹中。

这两个测试的行为与 IoCy 测试完全相同,代码也几乎相同。唯一的区别是我们依赖于 NP.DependencyInjection.AutofacAdapter 包而不是 NP.IoCy,并且为了创建容器,我们调用 new AutofacContainerBuilder() 构造函数而不是 IoCy 的 new ContainerBuilder()

摘要

本文深入探讨了 IoC/DI 的概念和应用方法。我提出了一种易于实现的最小 IoC/DI 接口。此外,本文展示了该接口的两个实现——NP.IoCyNP.DependencyInjection.AutfacAdapter 容器。

NP.IoCy 是一个非常简单且小巧的容器,占用的空间不到其他容器(包括 Autofac、Castle Windsor、Microsoft.Extensions.DependencyInjection 和 MEF)的 0.1%。

NP.DependencyInjection.AutofacAdapter 是 Autofac 的一个包装器。使用它的人也可以使用任何其他 Autofac 功能和 API。此库适用于那些更喜欢使用一个更成熟、经过更充分测试且 API 更熟悉的库的人。

如果有需求,我计划将来为其他流行框架创建更多适配器。

历史

  • 2022年12月18日:初始版本
  • 2023年1月16日 - 添加了关于多单元的部分
© . All rights reserved.