Loading

第十三章-Autofac DI容器

在前面的章节中,我们讨论了总体上适用于DI的模式和原理,但是除了一些示例之外,我们还没有详细研究如何使用任何特定的DI容器(DI Container)应用它们。 在本章中,您将看到这些整体模式如何映射到Autofac。 您需要熟悉上一章中的内容,才能从中充分受益。

Autofac是一个相当全面的DI容器(DI Container),提供了精心设计且一致的API。 自2007年下半年以来一直存在,在撰写本文时,它是最受欢迎的容器之一。

在本章中,我们将研究如何使用Autofac来应用第1-3部分中介绍的原理和模式。 本章分为四个部分。 您可以独立阅读每个部分,尽管第一部分是其他部分的前提条件,而第四部分则依赖于第三部分中介绍的某些方法和类。

本章应使您入门,并处理每天使用Autofac时可能出现的最常见问题。 这不是对Autofac的完整处理; 这将需要花费更多的章节或一整本书本身。 如果您想了解有关Autofac的更多信息,最好的起点是位于https://autofac.org的Autofac主页。

介绍Autofac(Introducing Autofac)

在本部分中,您将学习在哪里获得Autofac,获得什么以及如何开始使用它。 我们还将介绍常见的配置选项。 表13.1提供了您可能需要入门的基本信息。

使用Autofac与使用其他DI容器(DI Container)没有什么不同,我们将在以下章节中讨论。 与Simple InjectorMicrosoft.Extensions .DependencyInjection一样,用法是一个两步过程,如图13.1所示。 首先,您配置一个ContainerBuilder,并在完成后使用它来构建一个容器来解析组件。

图13.1 使用Autofac的模式是首先对其进行配置,然后解析组件。
image

完成本节后,您应该对Autofac的整体使用情况有很好的了解,并且应该能够在行为规范的情况下开始使用它-所有组件都遵循正确的DI模式,例如构造函数注入(Constructor Injection)。 让我们从最简单的场景开始,看看如何使用Autofac容器解析对象。

解析对象(Resolving objects)

任何DI容器(DI Container)的核心服务是组成对象图。 在本部分中,我们将介绍使您可以使用Autofac组成对象图的API。

默认情况下,Autofac要求您注册所有相关组件,然后才能解决它们。 但是,此行为是可配置的。 下面的清单显示了Autofac的最简单的使用方法之一。

清单13.1 Autofac的最简单用法

var builder = new ContainerBuilder();
builder.RegisterType<SauceBéarnaise>();
IContainer container = builder.Build();
ILifetimeScope scope = container.BeginLifetimeScope();
SauceBéarnaise sauce = scope.Resolve<SauceBéarnaise>();

如图13.1所示,您需要一个ContainerBuilder实例来配置组件。 在这里,您向构建器注册了具体的SauceBéarnaise类,以便当您要求其构建容器时,将使用SauceBéarnaise类来配置生成的容器。 这再次使您能够从容器中解析SauceBéarnaise类。

但是,使用Autofac,您永远不会从根容器本身进行解析,而只能从生存期范围中进行解析。 第13.2.1节详细介绍了生存期范围以及为什么从根容器进行解析是一件坏事。

警告 使用Autofac,直接从根容器进行解析是一种不好的做法。 这很容易导致内存泄漏或并发错误。 相反,您应该始终从生命周期范围内进行解析。

如果您未注册SauceBéarnaise组件,尝试对其进行解析将抛出ComponentNotRegisteredException并显示以下消息:

​ 所请求的服务“Ploeh.Samples.MenuModel.SauceBéarnaise”尚未注册。 为避免此异常,请注册一个组件以提供服务,使用IsRegistered()检查服务注册,或使用ResolveOptional()方法解决可选的依赖项。

Autofac不仅可以使用无参数构造函数来解析具体类型,还可以自动连接具有其他依赖关系的类型。 所有这些依赖项都需要注册。 在大多数情况下,您需要对接口进行编程,因为这会引入松散耦合(Loose Coupling)。 为此,Autofac允许您将抽象映射到具体类型。

将抽象映射到具体类型(Mapping Abstractions to concrete types)

应用程序的根类型通常由具体类型来解决,而松散耦合(Loose Coupling)则要求您将抽象映射到具体类型。 基于此类映射创建实例是任何DI容器(DI Container)提供的核心服务,但是您仍然必须定义映射。 在此示例中,您将IIngredient接口映射到具体的SauceBéarnaise类,这使您能够成功解析IIngredient

var builder = new ContainerBuilder();

builder.RegisterType<SauceBéarnaise>()
.As<IIngredient>();  <-- 将具体类型映射到抽象

IContainer container = builder.Build();

ILifetimeScope scope = container.BeginLifetimeScope();

IIngredient sauce = scope.Resolve<IIngredient>();  <--解决了SauceBéarnaise类

As <T>方法允许将具体类型映射到特定的抽象。 由于先前有As <IIngredient>()调用,因此SauceBéarnaise现在可以解析为IIngredient

您可以使用ContainerBuilder实例来注册类型和定义映射。 RegisterType方法使您可以注册具体类型。

如清单13.1所示,如果您只想注册SauceBéarnaise类,则可以就此停下来。 您还可以继续使用As方法来定义应如何注册具体类型。

警告Simple InjectorMicrosoft.Extensions.DependencyInjection相反,RegisterTypeAs方法定义的类型之间没有有效的通用类型约束。 这意味着可以映射不兼容的类型。 代码将编译,但是当ContainerBuilder生成容器时,您会在运行时遇到异常。

在许多情况下,您只需要通用API。 尽管它不能提供与某些其他DI容器(DI Container)相同的类型安全性,但它仍然是一种易于配置的容器配置方式。 但是,在某些情况下,您需要使用更弱类型的方法来解析服务。 使用Autofac,这也是可能的。

解决弱类型服务(Resolving weakly typed services)

有时您无法使用通用API,因为在设计时您不知道合适的类型。 您所拥有的只是一个Type实例,但您仍然希望获得该类型的实例。 您在7.3节中看到了一个示例,其中我们讨论了ASP.NET Core MVCIControllerActivator类。 相关方法是这样的:

object Create(ControllerContext context);

如清单7.8所示,ControllerContext捕获控制器的Type,您可以使用ActionDescriptor属性的ControllerTypeInfo属性提取该控制器的Type:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();

由于您只有Type实例,因此无法使用通用的Resolve <T>方法,而必须使用弱类型的APIAutofac提供了Resolve方法的弱类型重载,使您可以像下面这样实现Create方法:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
return scope.Resolve(controllerType);

Resolve的弱类型重载使您可以将controllerType变量直接传递给Autofac。 通常,这意味着您必须将返回值转换为某种抽象,因为弱类型的Resolve方法返回object。 但是,对于IControllerActivator,则不需要这样做,因为ASP.NET Core MVC不需要控制器来实现任何接口或基类。

无论您使用哪种Resolve重载,Autofac都会保证它会返回所请求类型的实例,或者如果存在不能满足的依赖关系,则将引发异常。 正确配置了所有必需的依赖项后,Autofac可以自动连线所请求的类型。

在前面的示例中,scopeAutofac.ILifetimeScope的实例。 为了能够解析请求的类型,必须预先配置所有松散耦合(Loose Coupling)的依赖项。 有很多方法可以配置Autofac,下一节将介绍最常用的方法。

配置ContainerBuilder

正如我们在12.2节中讨论的那样,您可以用几种概念上不同的方式配置DI容器(DI Container)。 图12.5审阅了以下选项:配置文件,“配置为代码”和“自动注册”。 图13.2再次显示了这些选项。

图13.2针对显式维度和绑定程度配置DI容器(DI Container)的最常用方法
image

核心配置API以代码为中心,并支持“代码配置”和基于约定的自动注册。 可以使用Autofac.Configuration NuGet软件包插入对配置文件的支持。 Autofac支持所有三种方法,并允许您将它们全部混合在同一容器中。 在本部分中,您将看到如何使用这三种类型的配置源。

使用配置作为代码配置ContainerBuilder

在13.1节中,您简要了解了Autofac的强类型配置API。 在这里,我们将对其进行更详细的研究。

尽管您使用的大多数方法都是扩展方法,但是Autofac中的所有配置都使用ContainerBuilder类公开的API。 最常用的方法之一是您已经看到的RegisterType方法:

builder.RegisterType<SauceBéarnaise>().As<IIngredient>();

将SauceBéarnaise注册为IIngredient隐藏了具体类,因此您无法再通过此注册来解析SauceBéarnaise。 但是您可以通过使用As方法的重载轻松地解决此问题,该方法使您可以指定具体类型映射到多个注册类型:

builder.RegisterType<SauceBéarnaise>().As<SauceBéarnaise, IIngredient>();

您可以将其注册为本身及其实现的接口,而不是仅将类注册为IIngredient。 这使容器能够解决对SauceBéarnaise和IIngredient的请求。 或者,您也可以将调用链接到As方法:

builder.RegisterType<SauceBéarnaise>() .As<SauceBéarnaise>() .As<IIngredient>();

这将产生与先前示例相同的结果。 两种注册之间的区别仅是样式问题。

As方法的三个泛型重载使您可以指定一种,两种或三种类型。 如果您需要指定更多类型,则还可以使用非通用重载来指定任意多个类型。

残缺的生命周期

在本节中,我们展示了如何多次调用As来将组件注册为多种服务类型。 以下示例再次显示了这一点:

builder.RegisterType<SauceBéarnaise>() .As<SauceBéarnaise>() .As<IIngredient>();

您可能会觉得这等效于以下代码:

builder.RegisterType<SauceBéarnaise>(); 
builder.RegisterType<SauceBéarnaise>().As<IIngredient>();

但是,前一个示例并不等同于后者。 当您将“生活方式”从“瞬态”更改为“单例”时,这一点变得显而易见。

builder.RegisterType<SauceBéarnaise>().SingleInstance(); 
builder.RegisterType<SauceBéarnaise>().As<IIngredient>() .SingleInstance();

尽管您可能希望在容器的生存期内只有一个SauceBéarnaise实例,但是拆分注册会使Autofac为每个RegisterType调用创建一个单独的实例。 因此,SauceBéarnaise的生活方式被认为已被破坏。

警告 向As方法提供多种服务类型与进行多个RegisterType调用不同。 每次调用会得到它自己的缓存,这可能会导致被撕裂的寿命时,所选择的生命周期比其他瞬态。

在实际的应用程序中,始终要映射多个抽象,因此必须配置多个映射。 这是通过多次调用RegisterType来完成的:

builder.RegisterType<SauceBéarnaise>().As<IIngredient>(); 
builder.RegisterType<Course>().As<ICourse>();

此示例将IIngredient映射到SauceBéarnaise,将ICourse映射到Course。 类型没有重叠,因此应该很清楚地说明发生了什么。 但是您也可以多次注册相同的抽象:

builder.RegisterType<SauceBéarnaise>().As<IIngredient>(); 
builder.RegisterType<Steak>().As<IIngredient>();

在这里,您注册了IIngredient两次。 如果解决IIngredient,您将获得Steak的实例。 上次注册获胜,但不会忘记以前的注册。 Autofac可以很好地处理同一个抽象的多种配置,但我们将在13.4节中回到本主题。

有更多高级选项可用于配置Autofac,但是您可以使用此处显示的方法配置整个应用程序。 但是,为了避免过多地显式维护容器配置,可以改用使用自动注册的基于约定的方法。

使用自动注册配置ContainerBuilder

在许多情况下,注册将是相似的。 如我们在第12.3.3节中所讨论的那样,此类注册非常繁琐,并且显式注册每个组件可能不是最有效的方法。

考虑一个包含许多IIngredient实现的库。 您可以分别配置每个类,但这将导致对RegisterType方法的许多外观相似的调用。 更糟糕的是,每次添加新的IIngredient实现时,如果您希望它可以使用,还必须在ContainerBuilder中显式注册它。 声明在给定程序集中找到的IIngredient的所有实现都应进行注册会更有效率。

使用RegisterAssemblyTypes方法可以做到这一点。 此方法使您可以指定程序集,并将该程序集中所有选定的类配置为单个语句。 要获取Assembly实例,可以使用一个代表性的类(在本例中为Steak):

Assembly ingredientsAssembly = typeof(Steak).Assembly; builder.RegisterAssemblyTypes(ingredientsAssembly).As<IIngredient>();

RegisterAssemblyTypes方法返回与RegisterType方法相同的接口,因此可以使用许多相同的配置选项。 这是一项强大的功能,因为它意味着您无需学习新的API即可使用自动注册。

在前面的示例中,我们使用As方法将程序集中的所有类型注册为IIngredient服务。 前面的示例还无条件地配置了IIngredient接口的所有实现,但是您可以提供允许仅选择一个子集的过滤器。 这是一个基于约定的扫描,其中您仅添加名称以Sauce开头的类:

Assembly ingredientsAssembly = typeof(Steak).Assembly; builder.RegisterAssemblyTypes(ingredientsAssembly) .Where(type => type.Name.StartsWith("Sauce")) .As<IIngredient>();

在装配中注册所有类型时,可以使用谓词定义选择条件。 与前面的代码示例的唯一区别是其中包含了Where方法,您可以在其中仅选择名称以Sauce开头的那些类型。

还有许多其他方法可让您提供各种选择条件。 Where方法为您提供了一个过滤器,该过滤器仅允许那些类型与谓词相匹配,而Except方法也可以采用其他方法。

除了从程序集中选择正确的类型之外,自动注册的另一部分是定义正确的映射。 在前面的示例中,我们将As方法与特定接口一起使用,以针对该接口注册所有选择的类型。 但是有时候您会想要使用不同的约定。

假设您使用抽象基类而不是接口,并希望在名称以Policy结尾的程序集中注册所有类型。 为此,As方法还有其他几种重载,包括使用Func <Type,Type>作为输入的重载:

Assembly policiesAssembly = typeof(DiscountPolicy).Assembly; builder.RegisterAssemblyTypes(policiesAssembly) .Where(type => type.Name.EndsWith("Policy")) .As(type => type.BaseType);

提示 将RegisterAssemblyTypes视为RegisterType的复数形式。

您可以为名称以Policy结尾的每个类型使用提供给As方法的代码块。 这样可以确保所有带有Policy后缀的类都将针对其基类进行注册,以便在请求基类时,容器会将其解析为该约定所映射的类型。 使用Autofac的基于约定的注册非常容易,并且使用的API紧密反映了由奇异的RegisterType方法公开的API。

使用AsClosedTypesOf自动注册泛型抽象

在第10章中,您将大型,令人讨厌的IProductService接口重构为清单10.12的ICommandService 接口。 这又是抽象:

public interface ICommandService<TCommand> { 
    void Execute(TCommand command);
}

如第10章所述,每个命令Parameter Object都代表一个用例,除了实现交叉切割关注点的任何Decorator之外,每个用例都将有一个实现。 以清单10.8的AdjustInventoryService为例。 它实施了“调整库存”用例。 下一个清单再次显示了该类。

清单13.2第10章中的AdjustInventoryService

public class AdjustInventoryService : ICommandService<AdjustInventory>
{
    private readonly IInventoryRepository repository;
    public AdjustInventoryService(IInventoryRepository repository)
    {
        this.repository = repository;
    }
    public void Execute(AdjustInventory command)
    {
        var productId = command.ProductId;
        ...
    }
}

任何相当复杂的系统都将轻松实现数百个用例。 这是使用自动注册的理想选择。 如下面的清单所示,有了Autofac,这再简单不过了。

清单13.3 ICommandService 实现的自动注册

Assembly assembly = typeof(AdjustInventoryService).Assembly; 
builder.RegisterAssemblyTypes(assembly) .AsClosedTypesOf(typeof(ICommandService<>));

与前面的清单一样,您可以使用RegisterAssemblyTypes方法从提供的程序集中选择类。 但是,您可以调用AsClosedTypesOf并提供开放通用的ICommandService <TCommand>接口,而不是调用As

使用提供的开放通用接口,Autofac遍历程序集类型列表,并注册实现ICommandService <TCommand>的封闭通用版本的所有类型。 例如,这意味着注册AdjustInventoryService,因为它实现了ICommandService ,它是ICommandService <TCommand>的封闭版本。

RegisterAssemblyTypes方法采用Assembly实例的params数组,因此您可以根据需要为单个约定提供尽可能多的程序集。 扫描文件夹中的程序集并将其全部提供以实现加载项功能并非易事。 这样,可以在不重新编译核心应用程序的情况下添加加载项。 这是实现后期绑定(Late binding)的一种方法。 另一个是使用配置文件。

使用配置文件配置ContainerBuilder

当您需要更改容器的注册而不重新编译应用程序时,配置文件是一个可行的选择。 如我们在第12.2.1节中所述,应仅将DI文件用于需要后期绑定(Late binding)的那些类型的配置文件:在所有其他类型和配置的所有其他部分中,将Configuration优先选择为Code或Auto-Registration。

使用配置文件的最自然的方法是将那些文件嵌入到标准.NET应用程序配置文件中。 这是可能的,但是如果需要独立于标准.config文件来更改Autofac配置,也可以使用独立配置文件。 无论您要做一个还是另一个,API都差不多。

Autofac的配置支持在单独的程序集中实现。 若要使用此功能,您必须添加对Autofac.Configuration程序集(https://mng.bz/1Q4V)的引用。

一旦有了对Autofac.Configuration的引用,就可以要求ContainerBuilder从standard.config文件中读取组件注册,如下所示:

var configuration = new ConfigurationBuilder()
    .AddJsonFile("autofac.json")
    .Build();  <---使用.NET Core的配置系统加载autofac.json配置文件。 默认情况下,配置文件将位于应用程序的根目录中。

builder.RegisterModule(
    new ConfigurationModule(configuration));  <---将创建的配置包装在处理配置文件并在Autofac中映射基于文件的注册的Autofac模块中。 该模块使用RegisterModule添加到构建器中。

这是一个将IIngredient接口映射到Steak类的简单示例:

{
    "defaultAssembly": "Ploeh.Samples.MenuModel",  <--defaultAssembly构造使您可以以较短的方式编写类型。 如果您未在类型或接口引用中指定程序集限定的类型名称,则将其假定为默认程序集。
    "components": [
        {
            "services": [{
                "type": "Ploeh.Samples.MenuModel.IIngredient"
            }],
            "type": "Ploeh.Samples.MenuModel.Steak"
        }] <--从IIngredient到Steak的简单映射。 使用完全限定的类型名称来指定类型,但是,如果在默认程序集中定义了该类型,则可以省略程序集名称,在这种情况下就是这样。
}

类型名称必须包含名称空间,以便Autofac可以找到该类型。 因为这两种类型都位于默认程序集Ploeh.Samples.MenuModel中,所以在这种情况下可以省略程序集名称。 尽管defaultAssembly属性是可选的,但它是一项不错的功能,如果您在同一个程序集中定义了许多类型,则可以避免进行大量键入操作。

组件元素是组件元素的JSON数组。 前面的示例包含一个组件,但是您可以根据需要添加任意数量的组件元素。 在每个元素中,必须使用type属性指定一个具体类型。 这是唯一必需的属性。 要将Steak类映射到IIngredient,可以使用可选的services属性。

当需要在不重新编译应用程序的情况下更改一个或多个组件的配置时,配置文件是一个不错的选择,但是由于它通常很脆弱,因此仅应将其保留。 使用自动注册或“配置作为代码”作为容器配置的主要部分。

提示 请记住,类型的最后一个配置将获胜! 您可以使用此行为来用XML配置覆盖硬编码配置。 为此,必须记住在配置任何其他组件之后读取XML配置。

本节介绍了Autofac DI容器(DI Container),并演示了这些基本机制:如何配置ContainerBuilder,以及随后如何使用构造的容器来解析服务。 只需调用Resolve方法即可轻松完成解析服务,因此复杂性涉及配置容器。 这可以通过几种不同的方式来完成,包括命令性代码和配置文件。

到目前为止,我们仅研究了最基本的API,因此还有一些更高级的领域需要介绍。 最重要的主题之一是如何管理组件的寿命。

管理生命周期

在第8章中,我们讨论了生命周期管理,包括最常见的概念性生活方式,例如Singleton,Scoped和Transient。 Autofac支持几种不同的生活方式,使您可以配置所有服务的生命周期。 表13.2中显示的生活方式可作为API的一部分获得。

在Autofac中,生命周期称为实例作用域。

表13.2 Autofac实例范围(生活方式)

Autofac名称 模式名称 注释
Per-dependency Transient 这是默认实例范围。 容器跟踪实例。
Single instance Singleton 当处置容器时,处置实例。
Per-lifetime scope Scoped 将组件的生存期与生存期范围联系在一起(请参阅第13.2.1节)。

提示 默认的“临时生命周期”是最安全的,但并非总是最有效的。 Singleton是线程安全服务的更有效选择,但是您必须记住明确注册那些服务。

Autofac的Transient和Singleton的实现与第8章中描述的常规生活方式等效,因此我们在本章中不会在它们上花费太多时间。 相反,在本节中,您将看到如何在代码中和配置文件中为组件定义生活方式。 我们还将介绍Autofac的有效期范围概念以及如何将其用于实施“有效范围的生活方式”。 在本节结束之前,您应该可以在自己的应用程序中使用Autofac的Lifestyles。 让我们首先回顾一下如何为组件配置实例范围。

配置实例范围

在本节中,我们将回顾如何使用Autofac管理组件实例范围。 实例作用域被配置为注册组件的一部分,您可以使用代码和配置文件来定义它们。 我们将依次研究每个。

用代码配置实例范围

实例范围定义为您在ContainerBuilder实例上进行的注册的一部分。 就这么简单:

builder.RegisterType<SauceBéarnaise>().SingleInstance();

这会将具体的SauceBéarnaise类配置为Singleton,以便每次请求SauceBéarnaise时都返回相同的实例。 如果要将Abstraction映射到具有特定生存期的具体类,则可以使用常规的As方法并将SingleInstance方法调用放置在任意位置。 这两个注册在功能上是等效的:

builder.RegisterType<SauceBéarnaise>().As<IIngredient>().SingleInstance();
=
builder.RegisterType<SauceBéarnaise>().SingleInstance().As<IIngredient>();

请注意,唯一的区别是我们交换了As和SingleInstance方法调用。 就个人而言,我们更喜欢左侧的序列,因为RegisterType和As方法调用形成了具体类和Abstraction之间的映射。 将它们保持在一起可以使注册更具可读性,然后您可以将实例范围声明为对映射的修改。

尽管Transient是默认的实例范围,但是您可以显式声明它。 这两个示例是等效的:

builder.RegisterType<SauceBéarnaise>();
=
  builder.RegisterType<SauceBéarnaise>().InstancePerDependency();

使用与单一注册相同的方法为基于约定的注册配置实例范围:

Assembly ingredientsAssembly = typeof(Steak).Assembly; 
builder.RegisterAssemblyTypes(ingredientsAssembly).As<IIngredient>() .SingleInstance();

您可以使用SingleInstance和其他相关方法来为约定中的所有注册定义实例范围。 在上一个示例中,您将所有IIngredient注册定义为Singleton。 与可以在代码和配置文件中注册组件的方式相同,还可以在两个位置都配置实例范围。

使用配置文件配置实例范围

当您需要在配置文件中定义组件时,您可能希望在同一位置配置它们的实例作用域。 否则,将导致所有组件使用相同的默认“生活方式”。 作为在13.1.2节中看到的配置模式的一部分,可以轻松完成此操作。 您可以使用可选的instance-scope属性来声明Lifestyle。

清单13.4 使用可选的instance-scope属性

{
    "defaultAssembly": "Ploeh.Samples.MenuModel",
    "components": [
        {
            "services": [{
                "type": "Ploeh.Samples.MenuModel.IIngredient"
            }],
            "type": "Ploeh.Samples.MenuModel.Steak",
            "instance-scope": "single-instance"  <--添加实例范围以配置Singleton
        }]
}

与13.1.2节中的示例相比,唯一的区别是添加了instancescope属性,该属性将实例配置为Singleton。 省略instance-scope属性时,将使用基于依赖关系,即Autofac等效于Transient。

无论是在代码中还是在文件中,都可以轻松配置组件的实例范围。 在所有情况下,都是以声明性的方式完成的。 尽管配置很容易,但您一定不要忘记,某些生活方式涉及寿命长的对象,这些对象只要在资源周围就可以使用资源。

解析组件(Releasing components)

如第8.2.2节所述,在完成对象的释放后,释放它们很重要。 Autofac没有显式的Release方法,而是使用称为生存期作用域的概念。 生存期范围可以视为容器的一次性复制品。 如图13.3所示,它定义了可以重用组件的边界。

图13.3 Autofac的生命周期作用域充当可以在有限的持续时间或目的下共享组件的容器。
image

生存期范围定义了可以用于特定持续时间或用途的派生容器; 最明显的例子是Web请求。 您从容器中生成作用域,以便该作用域继承父容器跟踪的所有Singleton,但该作用域还充当本地Singleton的容器。 当从生存期范围请求生存期范围的组件时,您始终会收到相同的实例。 与真正的Singletons的区别在于,如果查询第二个作用域,则将获得另一个实例。

无论您是从根容器还是生存期范围解析临时组件,它们都仍然按其应有的方式运行。

生命周期作用域的重要功能之一是,它们使您可以在作用域完成后正确释放组件。 您可以使用BeginLifetimeScope方法创建一个新范围,并通过调用其Dispose方法来释放所有适当的组件,如下所示:

using (var scope = container.BeginLifetimeScope())  <--从根容器创建范围
{
    IMeal meal = scope.Resolve<IMeal>();  <--解决新创建的范围中的meal
    meal.Consume();
}  <--通过结束using块释放meal

您可以通过调用BeginLifetimeScope方法从容器中创建一个新的作用域。 返回值实现IDisposable,因此您可以将其包装在using块中。 因为它也实现了与容器本身相同的接口,所以您可以使用作用域来解析组件,方式与容器本身完全相同。

终生作用域完成后,就可以对其进行处置。 当您退出一个using块时,这会自动发生,但是您也可以选择通过调用Dispose方法来显式处理它。 处置范围时,您还将释放由生存期范围创建的所有组件。 在该示例中,这意味着您释放了膳食对象图。

组件的依赖关系始终在组件的生存期范围内或以下得到解决。 例如,如果您需要向Singleton中注入一个Transient Dependency,则即使您是从嵌套生命周期范围解析Singleton的,该Transient Dependency也来自根容器。 这将跟踪根容器中的Transient,并防止在废弃生存期作用域时将其丢弃。 否则,Singleton使用者会崩溃,因为它依赖于已处置的组件而在根容器中保持活动状态。

请记住,释放一次性组件与处置它并不相同。 这是向容器发出的信号,表明该组件有资格退役。 如果该组件为“合并范围”,则将其处置; 否则,如果是Singleton,则它将一直处于活动状态,直到处理完根容器为止。

在本节的前面,您了解了如何将组件配置为Singletons或Transients。 配置组件以使其实例范围绑定到生存期范围的方法类似:

builder.RegisterType<SauceBéarnaise>() .As<IIngredient>() .InstancePerLifetimeScope(); 
<-----------------------与SingleInstance和InstancePerDependency方法类似,您可以使用InstancePerLifetimeScope方法声明组件的生存期应遵循创建实例的生存期范围。

重要 Autofac会跟踪大多数组件,甚至是一次性的瞬态瞬变,因此从使用寿命范围内解析所有组件并在使用后将其丢弃是很重要的。 从根容器解析范围内的实例将导致始终返回同一实例。 当这样的作用域组件不是线程安全的时,这将导致并发错误。 当使用根容器解析一次性Transient时,即使在每次对Resolve的调用上都创建了新实例,实例也保持活动状态,以便在处置容器时将其处置。 由于在应用程序停止运行之前不会丢弃根容器,因此会导致内存泄漏。

由于其性质,在容器本身的整个生命周期内都不会释放单例。 不过,如果您不再需要该容器,则可以释放那些组件。 这是通过处理容器本身来完成的:

container.Dispose();

在实践中,这并不像处理示波器那么重要,因为容器的寿命往往与其所支持的应用程序的寿命紧密相关。 通常,只要应用程序运行,就一直在保存该容器,因此只有在应用程序关闭时,才将其丢弃。 在这种情况下,操作系统将回收内存。

到此,我们完成了使用Autofac进行生命周期管理的旅程。 可以使用混合实例范围配置组件,即使您注册同一Abstraction的多个实现,也是如此。 但是直到现在,您已经隐含地假设所有组件都使用自动装配,从而允许容器装配“依赖关系”。 并非总是如此。 在下一节中,我们将介绍如何处理必须以特殊方式实例化的类。

注册困难的API (Registering difficult APIs)

到目前为止,我们已经考虑了如何配置使用构造函数注入(Constructor Injection)的组件。 构造函数注入(Constructor Injection)的许多好处之一是,像Autofac这样的DI容器(DI Container)可以轻松理解如何在依赖关系图中组合和创建所有类。 当API表现不佳时,这变得不太清楚。

在本节中,您将看到如何处理原始的构造函数参数和静态工厂。 这些都需要特别注意。 首先,我们看一下采用原始类型(例如字符串或整数)作为构造函数参数的类。

配置原始依赖项(Configuring primitive Dependencies)

只要将抽象注入消费者,一切都会好起来的。 但是,当构造函数依赖于基本类型(例如字符串,数字或枚举)时,将变得更加困难。 对于将连接字符串作为构造函数参数的数据访问实现,尤其如此,但这是一个更普遍的问题,适用于所有字符串和数字。

从概念上讲,将字符串或数字注册为容器中的组件并不总是很有意义。 但是使用Autofac,这至少是可行的。 以该构造函数为例:

public ChiliConCarne(Spiciness spiciness)

在此示例中,Spiciness是一个枚举:

public enum Spiciness { Mild, Medium, Hot }

提示 根据经验,枚举是代码的味道,应将其重构为多态类。3但是,在本示例中,它们很好地为我们服务。

如果希望所有Spiciness消费者使用相同的值,则可以彼此独立注册Spiciness和ChiliConCarne。 此代码段显示了如何:

builder.Register<Spiciness>(c => Spiciness.Medium); 
builder.RegisterType<ChiliConCarne>().As<ICourse>();

当您以后解决ChiliConCarne时,其Spiciness值将为Medium,所有其他依赖于Spiciness的组件也将具有Spiciness值。 如果您想更好地控制ChiliConCarne和Spiciness之间的关系,则可以使用WithParameter方法。 因为要为Spiciness参数提供具体的值,所以可以使用WithParameter重载,该重载采用参数名和值:

builder.RegisterType<ChiliConCarne>().As<ICourse>()
    .WithParameter( 
    "spiciness",   <---------------------------参数名称
    Spiciness.Hot); <------------------------  注入的值

此处介绍的两个选项均使用“自动装配”为组件提供具体值。 如第13.4节所述,这具有优点和缺点。 但是,更方便的解决方案是将原始依赖项提取到参数对象中。

在10.3.3节中,我们讨论了引入参数对象如何减轻IProductService引起的“开放/关闭原则”冲突。 但是,参数对象也是减轻歧义的好工具。

例如,一道菜的辣味可以用更笼统的术语调味来描述。 调味料可能包括其他特性,例如咸味。 换句话说,您可以将Spiciness和ExtraSalty包装在Flavoring类中:

public class Flavoring
{
    public readonly Spiciness Spiciness;
    public readonly bool ExtraSalty;
    public Flavoring(Spiciness spiciness, bool extraSalty)
    {
        this.Spiciness = spiciness;
        this.ExtraSalty = extraSalty;
    }
}

提示 正如我们在10.3.3节中提到的,参数对象最好有一个参数。 目标是消除歧义,但不仅限于技术层面。 像Flavouring类一样,这样的Parameter Object的名称可能在描述代码在功能级别上的功能方面做得更好。

随着风味参数对象的引入,现在可以很容易地自动连线任何需要风味的ICourse实现:

var flavoring = new Flavoring(Spiciness.Medium, extraSalty: true);
builder.RegisterInstance<Flavoring>(flavoring);
builder.RegisterType<ChiliConCarne>().As<ICourse>();

现在,您有了Flavoring类的单个实例。 调味成为ICourses的配置对象。 由于将只有一个调味实例,因此您可以使用RegisterInstance在Autofac中进行注册。

避免注入用作应用程序范围的配置对象的参数对象。 取而代之的是,选择狭窄的,集中的,仅包含特定使用者所需值的参数对象。 这样可以更清楚地传达组件使用的配置值,并简化测试。 一般而言,注入应用程序范围的配置对象是违反接口隔离原理的。

与以前讨论的选项相比,将原始依赖项提取到参数对象中应该是您的首选,因为参数对象在功能和技术层面上都消除了歧义。 但是,它确实需要更改组件的构造函数,但这可能并不总是可行的。 在这种情况下,使用WithParameter是第二好的选择。

用代码块注册对象

创建具有原始值的组件的另一种方法是使用Register方法。 它使您可以提供创建组件的委托:

builder.Register<ICourse>(c => new ChiliConCarne(Spiciness.Hot));

当我们在13.3.1节中讨论了Spiciness的注册时,您已经看到了Register方法。 在这里,每次ICourse服务被解析时,都会以Spiciness值Hot调用ChiliConCarne构造函数。

Register方法是类型安全的,但它禁用了自动装配。

对于ChiliConCarne类,您可以选择自动装配还是使用代码块。 其他类可能更具限制性:它们不能通过公共构造函数实例化。 相反,您必须使用某种工厂来创建该类型的实例。 这对于DI容器(DI Container)总是很麻烦,因为默认情况下,它们会照顾公共构造函数。 考虑公共JunkFood类的以下示例构造函数:

internal JunkFood(string name)

即使JunkFood类可能是公共的,构造函数也是内部的。 在此示例中,应该通过静态JunkFoodFactory类创建JunkFood的实例:

public static class JunkFoodFactory
{
    public static JunkFood Create(string name)
    {
        return new JunkFood(name);
    }
}

从Autofac的角度来看,这是一个有问题的API,因为在静态工厂周围没有明确且完善的约定。 它需要帮助-您可以通过提供可以执行以创建实例的代码块来提供帮助:

builder.Register<IMeal>(c => JunkFoodFactory.Create("chicken meal"));

这次,您使用Register方法通过在代码块内调用静态工厂来创建组件。 有了这个,每次IMeal被解析并返回结果时,都会调用JunkFoodFactory.Create。

当最终编写代码创建实例时,这比直接调用代码更好吗? 通过在Register方法调用中使用代码块,您仍然可以获得一些好处:

  • 您从IMeal映射到JunkFood。 这允许消耗类保持松散耦合(Loose Coupling)。
  • 实例范围仍然可以配置。 尽管将调用代码块来创建实例,但可能不会在每次请求实例时都调用该代码块。 它是默认设置,但是如果将其更改为Singleton,则该代码块将仅被调用一次,其结果将被缓存并在以后重用。

在本部分中,您已经了解了如何使用Autofac处理更困难的创建API。 您可以使用WithParameter方法将构造函数与服务连接起来,以保持自动装配的外观,也可以将Register方法与代码块一起使用,以实现更加类型安全的方法。 我们尚未研究如何使用多个组件,因此现在让我们将注意力转向该方向。

使用多个组件

正如第12.1.2节所提到的,DI容器(DI Container)在独特性上很兴旺,但在含糊不清的情况下却很难。 使用构造函数注入(Constructor Injection)时,单个构造函数比重载的构造函数更可取,因为很明显,在别无选择时要使用哪个构造函数。 从抽象到具体类型的映射时也是如此。 如果尝试将多个具体类型映射到同一Abstraction,则会引入歧义。

尽管模棱两可的特性令人不快,但您通常仍需要使用单个Abstraction的多个实现。5在以下情况下可能会出现这种情况:

  • 不同的消费者使用不同的混凝土类型。
  • 依赖关系是序列。
  • 装饰器或复合材料正在使用中。

在本部分中,我们将研究每种情况,并了解Autofac如何依次解决每种情况。 完成后,即使正在进行同一Abstraction的多个实现,您也应该能够注册和解析组件。 首先,让我们看看如何提供比“自动装配”更细粒度的控制。

在多个候选人中选择

自动接线既方便又强大,但几乎无法控制。 只要将所有Abstractions明确映射到具体类型,就不会有问题。 但是,一旦您引入了相同接口的更多实现,歧义就会浮出水面。 首先,让我们回顾一下Autofac如何处理同一个抽象的多个注册。

配置同一服务的多种实现

正如在13.1.2节中所看到的,您可以像这样注册同一接口的多个实现:

builder.RegisterType<Steak>().As<IIngredient>(); 
builder.RegisterType<SauceBéarnaise>().As<IIngredient>();

本示例将Steak和SauceBéarnaise类都注册为IIngredient服务。 最后一次注册获胜,因此,如果使用scope.Resolve ()解析IIngredient,则将获得SauceBéarnaise实例。

提示 给定服务的最后注册定义了该类型的默认实例。 如果您不希望您的注册优先于以前的注册,则可以使用.PreserveExistingDefaults()注册类型。

您也可以要求容器解析所有IIngredient组件。 Autofac没有专用的方法来执行此操作,而是依靠关系类型(https:// mng.bz/P429)。 关系类型是指示容器可以解释的关系的类型。 例如,可以使用IEnumerable 来指示您想要给定类型的所有服务:

IEnumerable<IIngredient> ingredients = scope.Resolve<IEnumerable<IIngredient>>();

请注意,我们使用常规的Resolve方法,但是我们请求IEnumerable 。 Autofac将其解释为惯例,并向我们提供了它的所有IIngredient组件。

提示 作为IEnumerable 的替代方法,您还可以请求一个数组。 结果是等效的; 在这两种情况下,您都将获得请求类型的所有组件。

注册组件时,可以为每个注册赋予一个名称,以后可以在不同的组件中选择该名称。 此代码段显示了该过程:

builder.RegisterType<Steak>().Named<IIngredient>("meat"); 
builder.RegisterType<SauceBéarnaise>().Named<IIngredient>("sauce");

与往常一样,您从RegisterType方法开始,而不是跟进As方法,而是使用Named方法指定服务类型和名称。 这使您可以通过为ResolveNamed方法提供相同的名称来解析命名服务:

IIngredient meat = scope.ResolveNamed<IIngredient>("meat"); 
IIngredient sauce = scope.ResolveNamed<IIngredient>("sauce");

命名组件不算作默认组件。 如果仅注册命名组件,则无法解析该服务的默认实例。 但是,没有什么可以阻止您也使用As方法注册默认(未命名)组件。 您甚至可以通过方法链接在同一条语句中执行此操作。

用字符串命名组件是DI容器(DI Container)的一个相当普遍的功能。 但是,Autofac还允许您使用任意键来标识组件:

object meatKey = new object(); 
builder.RegisterType<Steak>().Keyed<IIngredient>(meatKey);

该键可以是任何对象,您可以随后使用它来解析该组件:

IIngredient meat = scope.ResolveKeyed<IIngredient>(meatKey);

鉴于您应该始终在单个“组合根(Composition Root)”中解析服务,因此通常不应期望在此级别上处理此类歧义。 如果您确实发现自己使用特定名称或键来调用Resolve方法,请考虑是否可以更改方法以减少歧义。 在为给定服务配置依赖项时,还可以使用命名或键控实例在多个替代项之间进行选择。

注册命名的依赖项

与自动装配一样有用,有时您需要覆盖常规行为以提供对哪些依赖项位于何处的精细控制。 也可能是您需要解决一个模棱两可的API。 例如,考虑以下构造函数:

public ThreeCourseMeal(ICourse entrée, ICourse mainCourse, ICourse dessert)

在这种情况下,您具有三个相同类型的依赖项,每个依赖项代表一个不同的概念。 在大多数情况下,您希望将每个依赖项映射到一个单独的类型。 以下清单显示了如何选择注册ICourse映射。

清单13.5注册命名课程

builder.RegisterType<Rillettes>().Named<ICourse>("entrée");
builder.RegisterType<CordonBleu>().Named<ICourse>("mainCourse");
builder.RegisterType<MousseAuChocolat>().Named<ICourse>("dessert");

在这里,您注册了三个命名的组件,分别将Rilettes映射到名为entrée的实例,CordonBleu映射到名为mainCourse的实例,并将MousseAuChocolat映射到名为甜点的实例。 有了此配置,您现在可以使用指定的注册来注册ThreeCourseMeal类。

事实证明这非常复杂。 在下面的清单中,我们将首先向您展示它的外观,然后我们将挑选出该示例以了解发生了什么。

清单13.6覆盖自动装配

builder.RegisterType<ThreeCourseMeal>().As<IMeal>()
    .WithParameter(  <--WithParameter方法为ThreeCourseMeal构造函数提供参数值。 其重载之一带有两个参数。
    (p, c) => p.Name == "entrée",
    (p, c) => c.ResolveNamed<ICourse>("entrée"))
    .WithParameter(
    (p, c) => p.Name == "mainCourse",  <--将构造函数参数与特定名称匹配的谓词; 在这种情况下,mainCourse
    (p, c) => c.ResolveNamed<ICourse>("mainCourse"))
    .WithParameter(
    (p, c) => p.Name == "dessert",
    (p, c) => c.ResolveNamed<ICourse>("dessert")); <--解析要注入到构造函数参数中的值; 在这种情况下,dessert

让我们仔细看看这里发生了什么。 WithParameter方法重载包装了具有以下构造函数的ResolvedParameter类:

public ResolvedParameter(
    Func<ParameterInfo, IComponentContext, bool> predicate,
    Func<ParameterInfo, IComponentContext, object> valueAccessor);

谓词参数是一个测试,用于确定是否将调用valueAccessor委托。 当谓词返回true时,将调用valueAccessor为参数提供值。 两个委托都接受相同的输入:有关参数的信息,形式为ParameterInfo对象和IComponentContext,可用于解析其他组件。 当Autofac使用ResolvedParameter实例时,它在调用委托时会提供这两个值。

如清单13.6所示,产生的注册相当冗长。 但是,借助两种自写的辅助方法,您可以大大简化注册过程:

builder.RegisterType<ThreeCourseMeal>().As<IMeal>()
    .WithParameter(Named("entrée"), InjectWith<ICourse>("entrée"))
    .WithParameter(Named("mainCourse"), InjectWith<ICourse>("mainCourse"))
    .WithParameter(Named("dessert"), InjectWith<ICourse>("dessert"));

通过引入Named和InjectWith 辅助方法,您简化了注册过程,降低了详细程度,同时使读取正在发生的事情变得更加容易。 它几乎开始读起来像诗歌(或一瓶陈年的葡萄酒):

​ 使用名为entrée的参数InjectedWith名为entrée的ICourse创建您的ThreeCourseMeal。

以下代码显示了两个新方法:

Func<ParameterInfo, IComponentContext, bool> Named(string name)
{
    return (p, c) => p.Name == name;
}
Func<ParameterInfo, IComponentContext, object> InjectWith<T>(string name)
{
    return (p, c) => c.ResolveNamed<T>(name);
}

调用时,这两种方法都会创建一个新的委托,该委托包装提供的name参数。 有时除了对每个构造函数参数使用WithParameter方法外,别无其他方法,但是在其他情况下,您可以利用约定。

通过约定解析命名的组件

如果仔细查看清单13.6,您会发现一个重复的模式。 每次对WithParameter的调用都只处理一个构造函数参数,但是每个valueAccessor都做同样的事情:它使用IComponentContext解析与该参数同名的ICourse组件。

并没有要求您必须在构造函数参数后命名组件的要求,但是在这种情况下,您可以利用此约定并以更简单的方式重写清单13.6。 以下清单演示了如何进行。

清单13.7用约定覆盖自动装配

builder.RegisterType<ThreeCourseMeal>().As<IMeal>()
    .WithParameter(
        (p, c) => true,
        (p, c) => c.ResolveNamed(p.Name, p.ParameterType));

可能有些令人惊讶,但是您可以使用相同的WithParameter调用来处理ThreeCourseMeal类的所有三个构造函数参数。 通过声明此实例将处理Autofac可能向其抛出的任何参数来执行此操作。 因为仅使用此方法来配置ThreeCourseMeal类,所以约定仅在此有限范围内适用。

由于谓词始终返回true,因此将为所有三个构造函数参数调用第二个代码块。 在这三种情况下,都将要求IComponentContext解析名称和类型与参数相同的组件。 这在功能上与清单13.6中的操作相同。

警告 通过名称来标识参数很方便,但是重构起来并不安全。 如果重命名参数,则可以中断配置(取决于重构工具)。

与清单13.6一样,您可以创建清单13.7的简化版本。 但是,我们将其留给读者练习。

通过将参数显式映射到命名组件来覆盖自动装配是一种普遍适用的解决方案。 即使您在“组合根(Composition Root)”的一部分中配置了命名的组件,而在一个完全不同的部分中配置了使用者,也可以执行此操作,因为将命名组件和参数联系在一起的唯一标识就是名称。 这总是可能的,但如果要管理的名称很多,可能会很脆弱。 当提示您使用命名组件的原始原因是要处理歧义时,更好的解决方案是设计自己的API以消除歧义。 它通常会导致更好的总体设计。

在下一节中,您将看到如何使用含糊不清,更灵活的方法,即在一餐中允许任意数量的课程。 为此,您必须学习Autofac如何处理列表和序列。

接线顺序

在第6.1.1节中,我们讨论了构造函数注入(Constructor Injection)如何作为违反单一责任原则的警告系统。 那时的教训是,与其将构造函数过度注入视为构造函数注入(Constructor Injection)模式的弱点,您不应该为它使问题设计变得如此明显而高兴。

当谈到DI Containers和歧义性时,我们看到了类似的关系。 DI容器(DI Container)通常不会以优雅的方式处理歧义。 尽管您可以使用Autofac这样的优质DI容器(DI Container)来处理它,但它似乎很尴尬。 这通常表明您可以改进代码的设计。

提示 如果使用Autofac很难配置API的特定部分,请退后一步,对照本书中介绍的模式和原理重新评估您的设计。 通常,配置困难是由不遵循这些模式或违反这些原则的应用程序设计引起的。 使您的总体设计更好,不仅可以提高应用程序的可维护性,还可以使配置Autofac更加容易。

而不是受Autofac的束缚,您应该接受它的约定,并让它引导您朝着更好,更一致的设计方向发展。 在本节中,我们将看一个示例,该示例演示如何重构以消除歧义,并展示Autofac如何处理序列,数组和列表。

通过消除歧义来重构到更好的过程

在13.4.1节中,您看到了ThreeCourseMeal及其固有的歧义如何迫使您放弃自动装配,而是使用WithParameter。 这应该提示您重新考虑API设计。 例如,一个简单的概括就朝着实现IMeal的实现迈进,该实现采用任意数量的ICourse实例,而不是像ThreeCourseMeal类那样恰好是三个实例:

public Meal(IEnumerable<ICourse> courses)

请注意,与IEnumerable 实例的单个依赖关系使您无需为构造函数中的三个独立的ICourse实例提供任何数量的课程-从零到...很多! 这可以解决含糊不清的问题,因为现在只有一个依赖项。 此外,它还提供了一个通用的类,可以对不同类型的餐点进行建模,从而改善了API和实现,从简单的单道菜餐到精致的12道菜晚餐。

在本节中,我们将研究如何配置Autofac来连接具有适当ICourse依赖关系的Meal实例。 完成后,当您需要配置具有依存关系序列的实例时,应该对可用选项有一个很好的了解。

自动接线顺序

Autofac注入所有组件的默认策略通常是正确的策略,但是如图13.4所示,在某些情况下,您可能只想从较大的所有已注册组件集中选择某些已注册组件。

注入一个完整集合的子集的需求并不常见,但这确实说明了如何解决您可能遇到的更复杂的需求。

以前让Autofac Auto-Wire自动配置所有已配置的实例时,它对应于图右侧所示的情况。 如果要按左侧所示注册组件,则必须明确定义应使用的组件。 为了实现这一点,可以像清单13.6和13.7一样使用WithParameter方法。 这次,您正在处理仅包含一个参数的Meal构造函数。 下面的清单演示了如何实现WithParameter的值提供部分,以从IComponentContext中显式选择命名的组件。

图13.4从更大的所有已注册组件集中选择组件
image

清单13.8将命名组件注入序列

builder.RegisterType<Meal>().As<IMeal>() 
    .WithParameter(
    (p, c) => true, 
    (p, c) => new[] { 
        c.ResolveNamed<ICourse>("entrée"), 
        c.ResolveNamed<ICourse>("mainCourse"), 
        c.ResolveNamed<ICourse>("dessert") 
    });

如在13.4.1节中所见,WithParameter方法将两个委托作为输入参数。 第一个谓词用于确定是否应调用第二个委托。 在这种情况下,您决定变得有点懒,然后返回true。 您知道Meal类只有一个构造函数参数,因此可以使用。 但是,如果您稍后将Meal类重构为采用第二个构造函数参数,则可能无法正常工作。 为参数类型定义显式检查可能更安全。

第二个委托提供参数的值。 您使用IComponentContext将三个命名的组件解析为一个数组。 结果是一组ICourse实例,该实例与IEnumerable 兼容。

Autofac可以自然地理解序列; 除非您需要从给定类型的所有服务中仅明确选择某些组件,否则Autofac会自动执行正确的操作。 自动装配不仅适用于单个实例,而且适用于序列,并且容器将序列映射到相应类型的所有已配置实例。 Decorators设计模式是使用多个具有相同Abstraction的实例的一种不太直观的用法,我们将在下面讨论。

接线装饰器

在第9.1.1节中,我们讨论了在实现“跨页面关注”时装饰器设计模式如何有用。 根据定义,装饰器会引入同一抽象的多种类型。 至少,您有两种抽象的实现:装饰器本身和装饰类型。 如果堆叠装饰器,则可以拥有更多。 这是具有相同服务的多个注册的另一个示例。 与前面的部分不同,这些注册在概念上不是相等的,而是彼此之间的依赖关系。

在Autofac中应用装饰器有多种策略,例如使用前面讨论的WithParameter或使用代码块,如我们在13.3.2节中讨论的那样。 但是,在本节中,我们将重点介绍RegisterDecorator和RegisterGenericDecorator方法的使用,因为它们使配置Decorators变得轻而易举。

用RegisterDecorator装饰非泛型抽象

Autofac通过RegisterDecorator方法为Decorators提供了内置支持。 以下示例显示了如何使用此方法将面包屑应用于VealCutlet:

builder.RegisterType<VealCutlet>().As<IIngredient>(); <------------------将VealCutlet注册为默认IIngredient
    
builder.RegisterDecorator<Breading, IIngredient>(); <--------将Breading注册为IIngredient的装饰器。 解决IIngredient时,Autofac返回包装在Breading中的VealCutlet。

正如在第9章中学到的那样,当您在小牛肉肉饼上切开一个小袋并在小面包饼上添面包时将火腿,奶酪和大蒜放入小袋中,便会得到蓝带。 以下示例显示了如何在VealCutlet和面包屑装饰器之间添加HamCheeseGarlic装饰器:

builder.RegisterType<VealCutlet>() .As<IIngredient>();
builder.RegisterDecorator<HamCheeseGarlic,IIngredient>(); <--------- 添加一个新的装饰器
builder.RegisterDecorator<Breading, IIngredient>();

通过将此新注册放置在Breading注册之前,HamCheeseGarlic Decorator会被首先包装。 这将导致对象图等于以下Pure DI版本:

new Breading( new HamCheeseGarlic( new VealCutlet())); <----VealCutlet由HamCheeseGarlic包装,Breading包装。

Autofac按注册顺序应用装饰器。

在Autofac中,使用RegisterDecorator方法链接装饰器很容易。 同样,您可以应用通用装饰器,如下所示。

用RegisterGenericDecorator装饰泛型抽象

在第10章的过程中,我们定义了多个通用的Decorator,它们可以应用于任何ICommandService <TCommand>实现。 在本章的其余部分,我们将搁置我们的成分和课程,并了解如何使用Autofac注册这些通用装饰器。 下面的清单演示了如何向10.3节中介绍的三个Decorator注册所有ICommandService <TCommand>实现。

清单13.9 装饰通用的自动注册的抽象

builder.RegisterAssemblyTypes(assembly) .AsClosedTypesOf(typeof(ICommandService<>)); 

builder.RegisterGenericDecorator( typeof(AuditingCommandServiceDecorator<>), typeof(ICommandService<>));

builder.RegisterGenericDecorator( typeof(TransactionCommandServiceDecorator<>), typeof(ICommandService<>));

builder.RegisterGenericDecorator( typeof(SecureCommandServiceDecorator<>), typeof(ICommandService<>));

如清单13.3所示,清单13.9使用RegisterAssemblyTypes注册任意ICommandService 实现。 但是,要注册通用装饰器,Autofac提供了另一种方法-RegisterGenericDecorator。 清单13.9的配置结果如图13.5所示,我们在前面的10.3.4节中进行了讨论。

您可以用不同的方式配置Decorator,但是在本节中,我们重点介绍为该任务明确设计的Autofac方法。 Autofac允许您以几种不同的方式处理多个实例:您可以将组件注册为彼此的替代品,可以将对等体注册为序列,也可以注册为分层Decorators。 在许多情况下,Autofac会弄清楚该怎么做,但是如果您需要更明确的控制,则始终可以明确定义服务的组成方式。

尽管依赖项序列的使用者可以最直观地使用同一抽象的多个实例,但装饰器是另一个很好的例子。 但是,在第三种情况下,也许会有一些令人惊讶的情况,其中有多个实例在起作用,这就是Composite设计模式。

接线组合组件

在本书的学习过程中,我们多次讨论了复合设计模式。 例如,在6.1.2节中,您创建了一个CompositeNotificationService(清单6.4),它既实现了INotificationService,又包装了一系列INotificationService实现。

接线非通用组合组件

让我们看一下如何注册Autofac中第6章中的Composites之类的CompositeNotificationService。 以下清单再次显示了该类。

清单13.10 第6章中的CompositeNotificationService复合

public class CompositeNotificationService : INotificationService
{
    private readonly IEnumerable<INotificationService> services;
    public CompositeNotificationService(
        IEnumerable<INotificationService> services)
    {
        this.services = services;
    }
    public void OrderApproved(Order order)
    {
        foreach (INotificationService service in this.services)
        {
            service.OrderApproved(order);
        }
    }
}

注册一个Composite要求将它添加为默认注册,同时向其注入一系列命名实例:

builder.RegisterType<OrderApprovedReceiptSender>() .Named<INotificationService>("service"); 

builder.RegisterType<AccountingNotifier>() .Named<INotificationService>("service"); 

builder.RegisterType<OrderFulfillment>() .Named<INotificationService>("service"); 

builder.Register(c => new CompositeNotificationService( c.ResolveNamed<IEnumerable<INotificationService>>("service"))) .As<INotificationService>();

在这里,使用Autofac的Auto-Wiring API以相同的名称,服务注册了三个INotificationService实现。 另一方面,CompositeNotificationService是使用委托注册的。 在委托内部,手动更新了Composite,并注入了IEnumerable <INotificationService>。 通过指定服务名称,可以解析先前的命名注册。

由于通知服务的数量可能会随着时间的推移而增加,因此您可以通过应用自动注册来减轻“组合根(Composition Root)”的负担。 使用RegisterAssemblyTypes方法,您可以在简单的单行代码中打开以前的注册列表。

清单13.11 注册CompositeNotificationService

builder.RegisterAssemblyTypes(assembly)
    .Named<INotificationService>("service");

builder.Register(c =>
                 new CompositeNotificationService(
                     c.ResolveNamed<IEnumerable<INotificationService>>("service")))
    .As<INotificationService>();

这看起来相当简单,但是看起来很欺骗。 RegisterAssemblyTypes将注册任何实现INotificationService的非通用实现。 当您尝试运行前面的代码时,根据您的Composite所在的程序集,Autofac可能会引发以下异常:

检测到圆形组件依赖性:CompositeNotificationService-> INotificationService []-> CompositeNotificationService-> INotificationService []-> CompositeNotificationService。

Autofac检测到循环依赖性。 (我们在6.3节中详细讨论了依赖性循环。)幸运的是,它的异常消息非常清楚。 它描述了CompositeNotificationService依赖于INotificationService []。 CompositeNotificationService包装了一个INotificationService序列,但是该序列本身再次包含CompositeNotificationService。 这意味着CompositeNotificationService是注入CompositeNotificationService的序列的元素。 这是无法构建的对象图。

由于Autofac的RegisterAssemblyTypes注册了找到的所有非通用INotificationService实现,因此CompositeNotificationService成为了该序列的一部分。 在这种情况下,CompositeNotificationService与所有其他实现放在同一程序集中。

有多种解决方法。 最简单的解决方案是将“复合材料”移动到其他组件。 例如,包含“组合根(Composition Root)”的程序集。 这可以防止RegisterAssemblyTypes选择类型,因为它是随特定的Assembly实例一起提供的。 另一个选择是将CompositeNotificationService过滤出列表。 执行此操作的一种优雅方法是使用Except方法:

builder.RegisterAssemblyTypes(assembly) .Except<CompositeNotificationService>() .Named<INotificationService>("service");

但是,复合类并不是唯一可能需要删除的类。 对于任何装饰器,您都必须执行相同的操作。 这并不是特别困难,但是由于通常会有更多的Decorator实现,因此最好查询类型信息以找出该类型是否表示Decorator。 下面的示例演示如何使用自定义的IsDecoratorForHelper方法也可以滤除Decorators:

builder.RegisterAssemblyTypes(assembly) 
    .Except<CompositeNotificationService>()
    .Where(type => !IsDecoratorFor<INotificationService>(type)) 
    .Named<INotificationService>("service");

下面的示例显示IsDecoratorFor方法:

private static bool IsDecoratorFor<T>(Type type) {
    return typeof(T).IsAssignableFrom(type) && 
        type.GetConstructors()[0].GetParameters()
        .Any(p => p.ParameterType == typeof(T)); 
}

IsDecoratorFor方法期望一个类型具有单个构造函数。 当一个类型都实现给定的T抽象并且其构造函数也需要一个T时,该类型被视为Decorator。

接线通用复合组件

在第13.4.3节中,您了解了如何使用Autofac的RegisterGenericDecorator方法注册通用Decorators子剧集。 在本节中,我们将介绍如何为常规抽象注册Composites。

在6.1.3节中,您指定了CompositeEventHandler 类(清单6.12)作为IEventHandler 实现序列上的Composite实现。 让我们看看是否可以将Composite与其封装的事件处理程序实现一起注册。

让我们从事件处理程序的自动注册开始。 如您先前所见,这是使用RegisterAssemblyTypes方法完成的:

builder.RegisterAssemblyTypes(assembly) 
    .As(type => from interfaceType in type.GetInterfaces() 
        where interfaceType.IsClosedTypeOf(typeof(IEventHandler<>)) 
        select new KeyedService("handler", interfaceType)
       );

此示例利用As重载允许提供一系列Autofac.Core.KeyedService实例。 KeyedService类是一个小型数据对象,它结合了密钥和服务类型。

Autofac通过As方法运行在装配体中找到的任何类型。 您可以使用LINQ查询来查找类型的已实现接口,该接口是IEventHandler 的封闭版本。 对于程序集中的大多数类型,此查询不会产生任何结果,因为大多数类型未实现IEventHandler 。 对于这些类型,没有注册添加到ContainerBuilder。

即使这很复杂,也不必将通用的Composites和Decorator过滤掉。 RegisterAssemblyTypes仅选择非通用实现。 通用类型(例如CompositeEventHandler )不会引起任何问题,也不必过滤掉或移至其他程序集。 这很幸运,因为必须编写一个可以处理通用抽象的IsDecoratorFor版本根本没有意思。

剩下的就是CompositeEventHandler 的注册。 由于此类型是通用类型,因此您不能使用带谓词的Register重载。 而是使用RegisterGeneric。 此方法允许在通用实现及其抽象之间进行映射,类似于使用RegisterGenericDecorator看到的内容。 要获得要注入到Composite的构造函数参数中的命名注册的序列,您可以再次使用通用的WithParameter方法:

builder.RegisterGeneric(typeof(CompositeEventHandler<>)) 
    .As(typeof(IEventHandler<>)) .WithParameter( 
    (p, c) => true, 
    (p, c) => c.ResolveNamed("handler", p.ParameterType)
);

因为CompositeEventHandler <TEvent>包含单个构造函数参数,所以通过让谓词返回true,可以简化注册以应用于所有参数。

当请求关闭的IEventHandler <TEvent>时,将调用WithParameter委托。 因此,在调用时,可以通过调用p.ParameterType获得构造函数参数的类型。 例如,如果请求IEventHandler <OrderApproved>,则参数类型将为IEnumerable <IEventHandler <OrderApproved >>。 通过使用序列名称处理程序将此类型传递给ResolveNamed方法,Autofac可以解析先前注册的实现IEventHandler <OrderApproved>的命名实例的序列。

尽管Decorators的注册很简单,但不幸的是,这不适用于Composites。 考虑到“合成”设计模式,尚未设计Autofac。 这可能会在将来的版本中更改。

这样就完成了我们对Autofac DI容器(DI Container)的讨论。 在下一章中,我们将注意力转向简单注入器。

总结

  • Autofac DI容器(DI Container)提供了相当全面的API,可解决使用DI容器(DI Container)时通常遇到的许多棘手情况。
  • Autofac的一个重要的总体主题似乎是明确性之一。 它不会试图猜测您的意思,而是提供易于使用的API,该API为您提供显式启用功能的选项。
  • Autofac强制在配置和使用容器之间更加严格地分离关注点。 您使用ContainerBuilder实例配置组件,但是ContainerBuilder无法解析组件。 完成ContainerBuilder的配置后,就可以使用它来构建一个IContainer,该IContainer可用于解析组件。
  • 使用Autofac,直接从根容器进行解析是一种不好的做法。 这很容易导致内存泄漏或并发错误。 相反,您应该始终从生命周期范围内进行解析。
  • Autofac支持标准的生活方式:瞬态,单例和作用域。
  • 通过提供允许提供代码块的API,Autofac允许使用不明确的构造函数和类型。 这允许执行任何创建服务的代码。
posted @ 2022-09-04 10:29  F(x)_King  阅读(290)  评论(0编辑  收藏  举报