第十四章-Simple Injector DI容器

在上一章中,我们介绍了由Nicholas Blumhardt于2007年创建的Autofac DI容器(DI Container)。三年后,Steven创建了Simple Injector,我们将在本章中进行研究。 与上一章中的Autofac一样,我们将给予Simple Injector相同的处理方式。 您将看到如何使用Simple Injector来应用第1-3部分中介绍的原理和模式。

本章分为四个部分。 您可以独立阅读每个部分,尽管第一部分是其他部分的前提条件,而第四部分则依赖于第三部分中介绍的某些方法和类。 您可以将本章与第4部分的其余各章分开阅读,以专门了解Simple Injector,也可以将其与其他各章一起阅读以比较DI容器(DI Container)。

尽管本章并不完全介绍Simple Injector容器,但它提供了足够的信息,您可以开始使用它。 本章包括有关如何处理使用简单进样器时可能出现的最常见问题的信息。 有关此容器的更多信息,请参见https://simpleinjector.org上的Simple Injector主页。

简单注射器介绍#

在本部分中,您将学习从何处获得Simple Injector,获得了什么以及如何开始使用它。 我们还将介绍常见的配置选项。

总体而言,使用Simple Injector与使用其他DI容器(DI Container)没有什么不同。 与Autofac DI容器(DI Container)(在第13章中介绍)和Microsoft一样。 Extensions.DependencyInjection DI容器(DI Container)(在第15章中介绍了),用法是一个两步过程,如图14.1所示。

您可能从第13章可能还记得,为了简化两步过程,Autofac使用了一个产生IContainer的ContainerBuilder类。 另一方面,简单注入器将注册和解析集成在同一Container实例中。 尽管如此,它通过禁止在解析第一个服务之后进行任何显式注册,将注册强制为两步过程。

尽管分辨率没有什么不同,但是Simple Injector的注册API与大多数DI容器(DI Container)的工作方式确实有很大不同。 在其设计和实现中,它消除了导致错误的常见原因的许多陷阱。 在整本书中,我们讨论了大多数这些陷阱,因此,在本章中,我们将讨论Simple Injector与其他DI容器(DI Container)之间的以下区别:

图14.1使用简单注入器的模式。 首先,您配置一个容器,然后使用相同的容器实例来解析其中的组件。
image

本书着重介绍了这一点,因此在本章中,我们将讨论Simple Injector与其他DI容器(DI Container)之间的以下区别:

  • 作用域是环境性的,允许始终从容器本身解析对象图,以防止出现内存不足和并发错误。
  • 序列通过不同的API进行注册,以防止意外的重复注册彼此覆盖。
  • 无法直接注册基本类型,以防止注册变得模棱两可。
  • 可以验证对象图以发现常见的配置错误,例如专属依赖关系。

在完成本节后,您应该对Simple Injector的整体使用模式有很好的了解,并且应该能够在行为举止良好的场景中开始使用它-所有组件都遵循正确的DI模式,例如Constructor 注射。 让我们从最简单的场景开始,看看如何使用“简单注入器”容器解析对象。

解析对象#

任何DI容器(DI Container)的核心服务是组成对象图。 在本节中,我们将介绍使您能够使用Simple Injector组成对象图的API。

如果您记得有关使用Autofac解决组件的讨论,您可能会记得Autofac要求您注册所有相关组件,然后才能解决它们。 Simple Injector并非如此; 如果您请求带有无参数构造函数的具体类型,则无需进行配置。 以下清单显示了Simple Injector的最简单的用途之一。

清单14.1 最简单的使用Simple Injector

var container = new Container();  <------------------- 创建容器
SauceBéarnaise sauce = container.GetInstance<SauceBéarnaise>(); <-------- 解决具体实例

给定SimpleInjector.Container的实例,可以使用通用的GetInstance方法获取具体的SauceBéarnaise类的实例。 由于此类具有无参数的构造函数,因此Simple Injector会自动创建其实例。 不需要容器的显式配置。

GetInstance 方法等效于Autofac的Resolve 方法。

如您在第12.1.2节中所了解的那样,“自动装配”是一种通过使用类型信息自动组成对象图的功能。 因为Simple Injector支持自动装配,所以即使在没有无参数构造函数的情况下,只要所涉及的构造函数参数都是具体类型,并且整个树中的所有参数都具有带有无参数构造函数的叶类型,它就可以创建没有配置的实例。 例如,考虑以下蛋黄酱构造函数:

public Mayonnaise(EggYolk eggYolk, SunflowerOil oil)

尽管蛋黄酱的配方有些简化,但假设EggYolk和SunflowerOil都是具有无参数构造函数的具体类。 尽管Mayonnaise本身没有无参数构造函数,但是Simple Injector无需任何配置即可创建它:

var container = new Container(); 
Mayonnaise mayo = container.GetInstance<Mayonnaise>();

这是可行的,因为Simple Injector能够弄清楚如何创建所有必需的构造函数参数。 但是,一旦您引入了松散耦合(Loose Coupling),就必须通过将抽象映射到具体类型来配置简单注入器。

将抽象映射到具体类型

尽管Simple Injector有时可以自动连接具体类型的功能会派上用场,但松散耦合(Loose Coupling)需要您将抽象映射到具体类型。 基于此类映射创建实例是任何DI容器(DI Container)提供的核心服务,但是您仍然必须定义映射。 在此示例中,您将IIngredient接口映射到具体的SauceBéarnaise类,这使您能够成功解析IIngredient:

var container = new Container();
container.Register<IIngredient, SauceBéarnaise>();  <-------------- 将抽象映射到特定实现
IIngredient sauce = container.GetInstance<IIngredient>(); <----------将SauceBéarnaise解析为IIredredient

您可以使用Container实例注册类型并定义映射。 在这里,通用的Register方法允许将Abstraction映射到特定的实现。 这使您可以注册一个具体的类型。 由于先前的Register调用,因此SauceBéarnaise现在可以解析为IIngredient。

Register <TService,TImplementation>方法包含通用类型约束。 这意味着编译器将捕获不兼容的类型映射。

在许多情况下,您只需要通用API。 但是,在某些情况下,您需要使用更弱类型的方法来解析服务。 这也是可能的。

解决弱类型服务

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

object Create(ControllerContext context);

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

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

因为只有Type实例,所以不能使用通用的GetInstance 方法,而必须使用弱类型的API。 Simple Injector提供了GetInstance方法的弱类型重载,使您可以像下面这样实现Create方法:

Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
return container.GetInstance(controllerType);

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

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

为了能够解析请求的类型,必须预先配置所有松散耦合(Loose Coupling)的Dependencies。 您可以通过多种方式配置Simple Injector。 下一节将回顾最常见的内容。

配置容器#

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

图14.2针对显式尺寸和绑定程度显示了配置DI容器(DI Container)的最常用方法。
image

Simple Injector的核心配置API以代码为中心,并支持“代码配置”和基于约定的自动注册。 基于文件的配置被完全忽略了。 这不应成为使用Simple Injector的障碍,因为正如我们在第12章中讨论的那样,通常应避免这种配置方法。 不过,如果您的应用程序需要后期绑定(Late binding),那么您可以自己添加基于文件的配置,这很容易,我们将在本节的后面进行讨论。

使用简单注入器,您可以混合使用所有三种方法。 在本部分中,您将看到如何使用这三种类型的配置源。

使用“配置作为代码”配置容器

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

Simple Injector中的所有配置都使用Container类公开的API。 最常用的方法之一是您已经看到的Register方法:

container.Register<IIngredient, SauceBéarnaise>();

因为要对接口进行编程,所以大多数组件都将依赖于Abstractions。 这意味着大多数组件将通过其相应的抽象进行注册。 当组件是对象图中最顶层的类型时,通常会通过其具体类型而不是其抽象来解析它。 例如,MVC控制器通过其具体类型进行解析。

提示 即使Simple Injector允许解析具体的未注册类型,也请确保您明确注册最顶层的类型。 这使Simple Injector可以验证完整的对象图,包括这些根类型。 这通常会导致检测到其他隐藏的配置错误。 第14.2.4节详细介绍了验证。

通常,您可以通过抽象或具体类型来注册类型,但不能同时注册两者。 但是,该规则也有例外。 在Simple Injector中,通过组件的具体类型和抽象来注册组件,只需添加一个额外的注册即可:

container.Register<IIngredient, SauceBéarnaise>(); 
container.Register<SauceBéarnaise>();

您可以将其注册为本身及其实现的接口,而不是仅将类注册为IIngredient。 这使容器能够解决对SauceBéarnaise和IIngredient的请求。

在13.1.2节中,当为Autofac中的相同组件两次调用RegisterType时,我们警告过Torn Lifestyles。 但是,使用Simple Injector,这不是问题。 在幕后,Simple Injector会自动对SauceBéarnaise的注册进行重复数据删除,并防止SauceBéarnaise的Lifestyle遭到破坏。

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

container.Register<IIngredient, SauceBéarnaise>(); 
container.Register<ICourse, Course>();

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

container.Register<IIngredient, SauceBéarnaise>(); 
container.Register<IIngredient, Steak>(); <-----------  引发异常

在这里,您注册了IIngredient两次,这导致第二行引发异常并显示以下消息:

				类型IIngredient已被注册。 如果您打算解决IIngredient实现的集合,请使用Collection.Register重载。 有关更多信息,请参见https://simpleinjector.org/coll1。

与大多数其他DI容器(DI Container)相反,Simple Injector不允许堆叠注册来建立一系列类型,如先前的代码片段所示。 它的API明确地将序列的注册与单个抽象映射分开。 1 Simple Injector不会多次调用Register,而是强制您使用Collection属性的注册方法,例如Collection.Register:

container.Collection.Register<IIngredient>( typeof(SauceBéarnaise), typeof(Steak));

Simple Injector在使用序列的地方使用术语集合。 在大多数情况下,您可以互换使用这些术语。

本示例在一次调用中注册了所有成分。 或者,您可以使用Collection.Append将实现添加到一系列成分中:

container.Collection.Append<IIngredient, SauceBéarnaise>(); 
container.Collection.Append<IIngredient, Steak>();

在以前的注册中,任何依赖于IEnumerable 的组件都将注入一系列的成分。 Simple Injector可以很好地处理同一个抽象的多种配置,但我们将在14.4节中回到本主题。

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

使用自动注册配置容器

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

考虑一个包含许多IIngredient实现的库。 您可以分别配置每个类,但这会导致提供给Collection.Register方法的Type实例列表不断变化。 更糟糕的是,每次添加新的IIngredient实现时,如果希望可用,也必须在Container中显式注册它。 声明在给定程序集中找到的IIngredient的所有实现都应进行注册会更有效率。

使用某些Register和Collection.Register方法重载是可能的。 这些特殊的重载使您可以在单个语句中指定程序集并配置该程序集中所有选定的类。 要获得Assembly实例,可以使用一个代表性的类。 在这种情况下,牛排:

Assembly ingredientsAssembly = typeof(Steak).Assembly; 
container.Collection.Register<IIngredient>(ingredientsAssembly);

前面的示例无条件地配置了IIngredient接口的所有实现,但是您可以提供允许仅选择一个子集的过滤器。 这是一个基于约定的扫描,其中您仅添加名称以Sauce开头的类:

Assembly assembly = typeof(Steak).Assembly;
var types = container.GetTypesToRegister<IIngredient>(assembly)
    .Where(type => type.Name.StartsWith("Sauce"));
container.Collection.Register<IIngredient>(types);

该扫描利用GetTypesToRegister方法,该方法搜索类型而不注册它们。 这使您可以使用谓词过滤选择。 现在,您无需使用程序集列表提供Collection.Register,而是向其提供Type实例列表。

除了从程序集中选择正确的类型之外,自动注册的另一部分是定义正确的映射。 在前面的示例中,您将Collection.Register方法与特定接口一起使用,以针对该接口注册所有选定的类型。 但是,有时您可能想使用不同的约定。 假设您使用抽象基类而不是接口,并且希望在程序集中注册所有类型,该程序集的名称以Policy的基本类型结尾:

Assembly policiesAssembly = typeof(DiscountPolicy).Assembly; 
var policyTypes = from type in policiesAssembly.GetTypes() <------ 获取装配体中的所有类型
    where type.Name.EndsWith("Policy")    <------ 按策略后缀过滤
    select type; 
foreach (Type type in policyTypes) { 
    container.Register(type.BaseType, type);   <----- 按其基类注册每个策略组件
}

在此示例中,您几乎没有使用Simple Injector API的任何部分。 相反,您可以使用.NET框架提供的反射和LINQ API来筛选和获取所需的类型。

尽管Simple Injector基于约定的API受到限制,但通过利用现有的.NET框架API,基于约定的注册仍然非常容易。 Simple Injector基于约定的API主要集中在序列和泛型类型的注册上。 在泛型方面,这变成了另一种局面。这就是为什么简单注入器明确支持基于泛型抽象注册类型的原因,我们将在下面讨论。

通用抽象的自动注册

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

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

如第10章所述,每个命令Parameter Object代表一个用例,每个用例只有一个实现。 以清单10.8的AdjustInventoryService为例。 它实施了“调整库存”用例。 下一个清单再次显示了该类。

清单14.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;
        ...
    }
}

任何相当复杂的系统都可以轻松实现数百个用例,这些都是使用自动注册的理想选择。 使用Simple Injector,这再简单不过了。

清单14.3 ICommandService 实现的自动注册

Assembly assembly = typeof(AdjustInventoryService).Assembly; container.Register(typeof(ICommandService<>), assembly);

与之前使用Collection.Register的清单相反,您再次使用了Register。 这是因为,请求的命令服务始终只有一种实现; 您不想注入一系列命令服务。

使用提供的开放通用接口,Simple Injector遍历程序集类型列表并注册实现ICommandService 的封闭通用版本的类型。 举例来说,这意味着AdjustInventoryService已注册,因为它实现了ICommandService ,它是ICommandService 的封闭版本。

但是,并非所有ICommandService 实现都将被注册。 Simple Injector跳过开放式实现,Decorator和Composites,因为它们通常需要特殊的注册。 我们将在第14.4节中对此进行讨论。

Register方法采用Assembly实例的params数组,因此您可以根据需要为一个约定提供任意数量的程序集。 扫描文件夹中的程序集并将其全部提供以实现可在不重新编译核心应用程序的情况下添加附加组件的附加组件功能并不是一个不切实际的想法。 (有关示例,请参见https://simpleinjector.org/registering-plugins-dynamically。)这是实现后期绑定(Late binding)的一种方法。 另一个是使用配置文件。

使用配置文件配置容器

当您需要能够在不重新编译应用程序的情况下更改配置时,配置文件是一个不错的选择。 使用配置文件的最自然的方法是将它们嵌入到标准.NET应用程序配置文件中。 这是可能的,但是如果您需要能够独立于标准.config文件而更改简单注入器配置,则也可以使用独立配置文件。

提示 如我们在第12.2.1节中所述,您应仅对DI配置中需要后期绑定(Late binding)的那些类型使用配置文件。 在配置的所有其他类型和所有其他部分中,将“配置”视为“代码”或“自动注册”。

如本节开头所述,Simple Injector中没有针对基于文件的配置的明确支持。 但是,通过使用.NET Core的内置配置系统,从配置文件加载注册非常简单。 为此,您可以定义自己的配置结构,以将抽象映射到实现。 这是一个将IIngredient接口映射到Steak类的简单示例。

清单14.4使用配置文件从IIngredient到Steak的简单映射

{
    "registrations": [
        {
            "service":
            "Ploeh.Samples.MenuModel.IIngredient, Ploeh.Samples.MenuModel",
            "implementation":
            "Ploeh.Samples.MenuModel.Steak, Ploeh.Samples.MenuModel"
        }
    ]
}

此配置示例的结构非常接近Autofac的结构,因为它是一种非常自然的格式。

registrations元素是一个注册元素的JSON数组。 前面的示例包含一个注册,但是您可以根据需要添加任意多个注册元素。 在每个元素中,必须使用实现属性指定一个具体类型。 要将Steak类映射到IIngredient,可以使用service属性。

使用.NET Core的内置配置系统,您可以加载配置文件并对其进行遍历。 然后,将定义的注册附加到容器:

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

var registrations = config
    .GetSection("registrations").GetChildren();  <--从配置文件加载注册列表

foreach (var reg in registrations)
{
    container.Register(
        Type.GetType(reg["service"]),
        Type.GetType(reg["implementation"]));
}  <--遍历注册列表。 使用配置文件中定义的提供的服务和实现类型,将每个注册添加到Simple Injector。

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

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

到目前为止,我们只研究了最基本的API; 我们尚未涵盖更先进的领域。 最重要的主题之一是如何管理组件的寿命。

管理生命周期#

在第8章中,我们讨论了生命周期管理,包括最常见的概念性生活方式,例如Transient,Singleton和Scoped。 Simple Injector的生活方式支持映射到这三种生活方式。 表14.2中显示的生活方式是API的一部分。

表14.2简单喷油器的生活方式

Simple Injector名称 模式名称 注释
Transient Transient 这是默认的生活方式。 容器不会跟踪瞬态实例,因此永远不会对其进行处理。 通过使用其诊断服务,如果您将一次性组件注册为瞬态,Simple Injector会发出警告。
Singleton Singleton 当处置容器时,处置实例。
Scoped Scoped 生活方式模板,可用于确定实例范围。 ScopedLifestyle由ScopedLifestyle基类定义,并且有几种ScopedLifestyle实现。 .NET Core应用程序最常用的Lifestyle是AsyncScopedLifestyle。 跟踪实例的整个生命周期,并在处置示波器时将其处置。

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

Simple Injector的Transient和Singleton的实现与第8章中描述的常规生活方式等效,因此我们在本章中不会在它们上花费太多时间。 相反,在本节中,您将看到如何为代码中的组件定义生活方式。 我们还将研究Simple Injector的环境范围界定概念,以及它如何简化容器的使用。 然后,我们将介绍Simple Injector如何验证和诊断其配置以防止常见的配置错误。 在本节的最后,您应该可以在自己的应用程序中使用Simple Injector的Lifestyles。 让我们首先回顾一下如何为组件配置Lifestyles。

配置生命周期#

在本部分中,我们将回顾如何使用Simple Injector管理生活方式。 生活方式被配置为注册组件的一部分。 就这么简单:

container.Register<SauceBéarnaise>(Lifestyle.Singleton);

本示例将具体的SauceBéarnaise类配置为Singleton,以便每次请求SauceBéarnaise时都返回相同的实例。 如果要将Abstraction映射到具有特定生存期的具体类,则可以将带有两个泛型参数的常规Register重载使用,同时向其提供Lifestyle .Singleton:

container.Register<IIngredient, SauceBéarnaise>(Lifestyle.Singleton);

尽管Transient是默认的Lifestyle,但是您可以显式声明它。 在默认配置下,这两个示例是等效的:

container.Register<IIngredient, SauceBéarnaise>( Lifestyle.Transient); <-------明确提供Transient到注册
container.Register<IIngredient, SauceBéarnaise>();  <----- 瞬态是默认的生活方式,可以省略。

配置生活方式以进行基于约定的注册可以通过多种方式完成。 例如,在注册序列时,一个选项是为Collection.Register方法提供一个Registration实例列表:

Assembly assembly = typeof(Steak).Assembly; 
var types = container.GetTypesToRegister<IIngredient>(assembly); container.Collection.Register<IIngredient>(
    from type in types 
    select Lifestyle.Singleton.CreateRegistration(type, container));

您可以使用Lifestyle.Singleton为公约中的所有注册定义Lifestyle。 在此示例中,通过将所有IIngredient注册作为注册实例提供给Collection.Register重载,将所有IIngredient注册定义为Singleton。

注册是用于构造表达式树的“简单注入器”类,该树描述基于其“生活方式”的类型的创建。 当您调用大多数Register重载时,注册对象是由Simple Injector在后台构造的,但是您也可以直接创建它们。 这在这种情况下很有用。

在为组件配置生活方式时,有很多选项。 在所有情况下,都是以声明性的方式完成的。 尽管配置通常很容易,但您一定不要忘记,某些生活方式涉及长期存在的对象,这些对象只要存在就可以使用资源。

释放组件#

如第8.2.2节所述,在完成对象的释放后,释放它们很重要。 与Autofac相似,Simple Injector没有显式的Release方法,而是使用一种称为范围的概念。 范围可以视为特定于请求的缓存。 如图14.3所示,它定义了可以重用组件的边界。

范围定义了可以用于特定持续时间或用途的缓存; 最明显的例子是Web请求。 从合并范围请求合并范围的组件时,您始终会收到相同的实例。 与真正的Singletons的区别在于,如果查询第二个作用域,则将获得另一个实例。

图14.3 Simple Injector的作用域充当特定于请求的缓存,可以在有限的持续时间或目的下共享组件。
image

范围的重要功能之一是,它们使您可以在范围完成时适当地释放组件。 您可以使用特定ScopedLifestyle实现的BeginScope方法创建一个新范围,并通过调用其Dispose方法来释放所有适当的组件:

using (AsyncScopedLifestyle.BeginScope(container)) {  <--------为容器创建范围
    IMeal meal = container.GetInstance<IMeal>();  <-----在创建的范围内从容器中解析meal
    meal.Consume(); 
}  <-------通过结束using块释放meal

本示例说明如何从Container实例而不是从Scope实例解析IMeal。 这不是错字-容器会自动“知道”它在哪个活动范围内运行。 下一节将对此进行更详细的讨论。

提示 Simple Injector包含多个NuGet软件包,可帮助将其与常见的应用程序框架集成。 其中一些软件包会自动确保范围包装Web请求。 要找出将Simple Injector与所选框架集成的最佳方法,请参阅集成指南(https://simpleinjector.org/integration)。

在前面的示例中,通过在相应的Scoped Lifestyle上调用BeginScope方法来创建新的范围。 返回值实现IDisposable,因此您可以将其包装在using块中。

合并范围完成后,可以使用using块将其处理。 当您退出该块时,这种情况会自动发生,但是您也可以选择通过调用Dispose方法来显式处理它。 处置范围时,您还将释放在该范围内创建的所有组件。 在该示例中,这意味着您释放了膳食对象图。

请记住,释放一次性组件与处置它并不相同。 这是向容器发出的信号,表明该组件有资格退役。 如果该组件具有作用域,则将对其进行处理; 如果是Singleton,它将一直处于活动状态,直到容器被处理掉为止。

在本节的前面,您了解了如何将组件配置为Singletons或Transients。 通过类似的方式配置组件以使其“生活方式”与范围相关联:

container.Register<IIngredient, SauceBéarnaise>(Lifestyle.Scoped);

与Lifestyle.Singleton和Lifestyle.Transient相似,您可以使用Lifestyle .Scoped值声明组件的生命周期应该在创建实例的作用域内有效。 但是,此调用本身将导致容器引发以下异常:

​ 为了能够使用Lifestyle.Scoped属性,请通过为您的应用程序类型设置Container.Options.DefaultScopedLifestyle属性和所需的作用域生活方式,以确保为容器配置了默认的作用域生活方式。 有关更多信息,请参见https://simpleinjector.org/scoped。

在使用Lifestyle.Scoped值之前,Simple Injector要求您设置Container.Options.DefaultScopedLifestyle属性。 Simple Injector具有多个ScopedLifestyle实现,这些实现有时特定于某个框架。 这意味着您必须明确配置最适合您的应用程序类型的ScopedLifestyle实现。 对于ASP.NET Core应用程序,正确的Scoped Lifestyle是AsyncScopedLifestyle,您可以按以下方式进行配置:

var container = new Container(); 
container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); 
<------------- 在对容器进行任何注册之前,您需要将AsyncScopedLifestyle注册为默认的Scoped Lifestyle。
container.Register<IIngredient, SauceBéarnaise>( Lifestyle.Scoped); <-------------现在,您可以使用Lifestyle.Scoped值进行范围注册。

提示 由于Simple Injector的环境范围,您始终可以直接从Container进行解析,而不必担心使用DI Container(例如Autofac)执行此操作会发生意外的内存泄漏。 如果您在没有有效作用域的情况下从容器中解析了作用域依赖性,则Simple Injector会抛出描述性异常。

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

container.Dispose();

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

正如我们在本节前面提到的,使用简单注入器,您总是从容器而不是作用域中解析对象。 之所以可行,是因为在简单进样器中示波器是周围的。 接下来让我们看看环境范围。

环境范围#

使用Simple Injector,创建和处置范围的先前示例显示了如何始终从Container中解析实例,即使您解析了作用域实例。 以下示例再次显示了这一点:

using (AsyncScopedLifestyle.BeginScope(container)) 
{ 
    IMeal meal = container.GetInstance<IMeal>();  <-----使用Simple Injector,您始终可以从容器中进行解析
    meal.Consume(); 
}

这揭示了Simple Injector的一个有趣功能,即作用域实例是环境实例,并且在它们运行时的上下文中全局可用。 下面的清单显示了此行为。

清单14.5 Simple Injector中的环境范围

var container = new Container();

container.Options.DefaultScopedLifestyle =
    new AsyncScopedLifestyle();

Scope scope1 = Lifestyle.Scoped
    .GetCurrentScope(container);  <--请求配置的Scoped Lifestyle的当前活动范围; 在这种情况下,为AsyncScopedLifestyle。 由于尚无活动范围,因此此方法返回值null
using (Scope scope2 =
       AsyncScopedLifestyle.BeginScope(container))
{
    Scope scope3 = Lifestyle.Scoped
        .GetCurrentScope(container);
} <--- 当存在活动范围时调用GetCurrentScope时,它将返回该范围。 在这种情况下,范围3的值将等于范围2
Scope scope4 = Lifestyle.Scoped
    .GetCurrentScope(container); <--处置范围后,它就不再列出。 这导致对GetCurrentScope的调用再次返回null

此行为类似于.NET的TransactionScope类的行为。 当使用TransactionScope包装操作时,在该操作中打开的所有数据库连接将自动成为同一事务的一部分。

重要 环境范围不应与环境上下文混淆。 Ambient Context为组合根外部的应用程序代码提供对Volatile Dependency或其行为的全局访问。 由于DI容器(DI Container)的作用域仅在组合根(Composition Root)内部使用,因此环境作用域不会像环境上下文那样暴露出相同的问题。

通常,您根本不会大量使用GetCurrentScope方法。 当您开始解析实例时,Container会在您的名义下使用它。 尽管如此,它仍然很好地证明了Scope实例可以被检索并且可以从容器中访问。

诸如以前的AsyncScopedLifestyle之类的ScopedLifestyle实现存储其创建的Scope实例供以后使用,从而允许在同一上下文中对其进行检索。 特定的ScopedLifestyle实现定义了代码在相同上下文中运行的时间。 例如,AsyncScopedLifestyle在内部将Scope存储在System.Threading.AsyncLocal .5中,即使异步方法在另一个线程上继续运行,这也允许范围在方法之间流动,如本示例所示:

using (AsyncScopedLifestyle.BeginScope(container))
{
    IMeal meal = container.GetInstance<IMeal>();
    await meal.Consume();  <--此异步调用可能导致该方法的其余代码在另一个线程上继续运行。
    meal = container.GetInstance<IMeal>();  <--保证对象图可以在同一范围内解析。
}

尽管起初环境示波器可能会令人困惑,但它们的使用通常会简化使用简单进样器的工作。 例如,您无需担心从容器中进行解析时会发生内存泄漏,因为Simple Injector可以代表您透明地进行管理。 作用域实例将永远不会缓存在根容器中,这对于本书中介绍的其他容器需要谨慎。 Simple Injector擅长的另一个领域是能够检测常见的错误配置。

诊断容器是否存在常见的使用寿命问题#

与Pure DI相比,DI容器(DI Container)中的注册和构建对象图更加隐含。 这样很容易意外地误配置容器。 因此,许多DI容器(DI Container)具有允许迭代所有注册以验证是否可以解决所有注册的功能,并且简单注入器也不例外。

但是,如第8.4.1节中的“强制依赖陷阱”所示,不能解析对象图不能保证配置的正确性。 强制依赖性是组件生命周期的错误配置。 实际上,与使用DI容器(DI Container)有关的大多数错误都与生命周期配置错误有关。

由于DI容器(DI Container)的错误配置非常普遍且通常难以跟踪,因此Simple Injector允许您验证其配置,这超出了大多数DI容器(DI Container)支持的对象图的简单实例化。 最重要的是,Simple Injector会扫描对象图以查找常见的错误配置-强制依赖是其中之一。

因此,如图14.1所示,Simple Injector的两步过程的配置步骤包含两个子步骤。 图14.4显示了此过程。

图14.4使用简单注入器的模式是对其进行配置(包括对其进行验证)然后解析组件。
image

让Simple Injector诊断和检测配置错误的最简单方法是调用Container的Verify方法,如下面的清单所示。

清单14.6验证容器

var container = new Container();    
container.Register<IIngredient, Steak>();    <-----------注册所有组件以实现全功能的应用程序
container.Verify();     <-----------  上次注册后,请致电验证以确保所有注册均已通过验证
让容器检测强制依赖性

强制依赖错误配置是Simple Injector检测到的错误配置。 现在,让我们看看如何使用14.1.1节中的蛋黄酱成分使Verify陷入依赖依赖状态。 其构造函数包含两个依赖项:

public Mayonnaise(EggYolk eggYolk, SunflowerOil oil)

以下清单将蛋黄酱注册为两个依存关系。 但是它将蛋黄酱错误地配置为Singleton,而将其EggYolk依赖关系注册为瞬态。

清单14.7导致容器检测强制依赖性

var container = new Container();

container.Register<EggYolk>(Lifestyle.Transient);   <--------由于EggYolk的有效期短,因此已将其注册为瞬态。
container.Register<Mayonnaise>(Lifestyle.Singleton);  
container.Register<SunflowerOil>(Lifestyle.Singleton);  <-------- 蛋黄酱依赖于EggYolk,但意外地注册为Singleton

container.Verify(); <-----  由于之前的配置错误,此方法引发异常。

当您调用Register时,Simple Injector仅执行一些基本的验证。 这包括检查类型是否不是抽象,是否具有公共构造函数等。 由于该注册可以任意顺序进行,因此它不会在该阶段检查诸如“强制依赖”之类的问题。 例如,在清单14.7中,向日葵油是在蛋黄酱之后注册的,即使它是蛋黄酱的依存关系。 这样做是完全有效的。 只有完成配置后,才能进行验证。 当您运行此代码示例时,对Verify的调用失败,并显示以下异常消息:

配置无效。 报告了以下诊断警告:

​ -[Lifestyle Mismatch] Mayonnaise (Singleton) depends on EggYolk (Transient). See the Error property for detailed information about the warnings. Please see https:// simpleinjector.org/diagnostics how to fix problems and how to suppress individual warnings.

Simple Injector称“专属依赖性生活方式不匹配”。

这里有趣的发现是,简单注入器不允许将瞬态依赖项注入到Singleton使用者中。 这与Autofac相反。 使用Autofac,隐式地预期瞬态对象的生存时间与它们的使用者一样长,这意味着在Autofac中,这种情况永远不会被认为是强制依赖。 因此,Autofac调用了Transient InstancePerDependency,它几乎描述了其行为:配置为Transient的每个使用者的Dependency都将获得自己的实例。 因此,Autofac仅将作用域组件注入到Singleton中作为俘获依赖项。

尽管有时候这可能正是您所需要的行为,但在大多数情况下不是。 瞬态组件通常会在很短的时间内运行,而将它们注入Singleton使用者会使组件在应用程序生存期间一直存在。 因此,Simple Injector的座右铭是:“安全胜于遗憾”,这就是它引发异常的原因。 有时,在您最了解的情况下,可能需要禁止显示此类警告。

禁止个人注册的警告

如果您想忽略EggYolk的到期日期,可以使用Simple Injector取消对该特定注册的检查。

清单14.8抑制诊断警告

var container = new Container();

Registration reg = Lifestyle.Transient
    .CreateRegistration<EggYolk>(container);  <------ 为EggYolk创建一个临时注册

reg.SuppressDiagnosticWarning(
    DiagnosticType.LifestyleMismatch,
    justification: "I like to eat rotten eggs.");  <------- 禁止使用“强制性依赖项”诊断警告,并说明为什么需要这样做

container.AddRegistration(typeof(EggYolk), reg);  <---- 将注册添加到容器

container.Register<Mayonnaise>(Lifestyle.Singleton);
container.Register<SunflowerOil>(Lifestyle.Singleton);

container.Verify();

SuppressDiagnosticWarning包含必需的对正参数。 SuppressDiagnosticWarning根本没有使用它,但它只是一个提醒,因此您不会忘记记录为什么禁止警告。

至此,我们完成了使用Simple Injector进行生命周期管理的旅程。 可以使用混合生活方式配置组件,当您注册同一Abstraction的多个实现时,甚至也是如此。

到目前为止,通过隐式假定所有组件都使用构造函数注入(Constructor Injection),您已经允许容器关联依赖项。 但这并非总是如此。 在下一节中,我们将介绍如何处理必须以特殊方式实例化的类。

注册困难的API#

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

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

配置原始依赖项#

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

从概念上讲,将字符串或数字注册为容器中的组件没有任何意义。 特别是,当使用自动装配时,基本类型的注册会引起歧义。 以字符串为例。 其中一个组件可能需要数据库连接字符串,而另一个组件可能需要文件路径。 两者在概念上是不同的,但是由于自动装配可以通过根据其类型选择依赖项来工作,因此它们变得模棱两可。 因此,Simple Injector会阻止原始依赖项的注册。 以该构造函数为例:

public ChiliConCarne(Spiciness spiciness)

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

public enum Spiciness { Mild, Medium, Hot }

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

您可能会很想注册ChiliConCarne,如以下示例所示。 那行不通!

container.Register<ICourse, ChiliConCarne>();

此行会导致异常,并显示以下消息:

The constructor of type ChiliConCarne contains parameter 'spiciness' of type Spiciness, which cannot be used for constructor injection because it’s a value type.

如果您想以中等程度的辣味解决ChiliConCarne,则必须离开自动装配,而要使用委托:

container.Register<ICourse>(() => new ChiliConCarne(Spiciness.Medium));

此Register方法具有类型安全性,但会禁用自动装配。

使用委托的缺点是,当ChiliConCarne构造函数更改时,必须更改注册。 例如,当您向ChiliConCarne构造函数添加IIngredient依赖性时,必须更新注册:

container.Register<ICourse>(() =>    <-------- 注册一个在调用时创建ChiliConCarne的委托
                            new ChiliConCarne( 
    				   Spiciness.Medium,
                                	container.GetInstance<IIngredient>())); <------- 回调到容器中以获取IIngredient并将其手动注入到构造函数中

除了在“组合根(Composition Root)”中进行额外的维护外,并且由于缺少自动装配,使用委托不能使Simple Injector验证ChiliConCarne及其IIngredient依赖性之间关系的有效性。 委托人隐藏了此依赖项存在的事实。 这并不总是一个问题,但是会使由于配置错误而引起的诊断问题复杂化。 由于存在这些缺点,一种更方便的解决方案是将原始依赖项提取到参数对象中。

提取对参数对象的原始依赖关系#

在10.3.3节中,我们讨论了引入参数对象如何减轻IProductService引起的“开放/关闭原则”冲突。 但是,参数对象也是减轻歧义的好工具。 例如,一道菜的辣味可以用更一般的术语描述为调味料。 调味品可能还包含其他属性,例如咸味,因此您可以在调味品类中包装辣味和咸味:

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);
container.RegisterInstance<Flavoring>(flavoring);

container.Register<ICourse, ChiliConCarne>();

这段代码创建了Flavoring类的单个实例。 调味成为课程的配置对象。 因为只有一个调味实例,所以您可以使用RegisterInstance在Simple Injector中注册它。

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

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

用代码块注册对象#

正如我们在上一节中讨论的那样,使用原始值创建组件的一种选择是使用Register方法。 这样就可以提供创建组件的委托。 再次是该注册:

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

每当解决ICourse服务时,就以Hot Spiciness调用ChiliConCarne构造函数。 但是,您可以使用代码块自己编写构造函数调用,而不是使用Simple Injector弄清楚构造函数的参数。

对于应用程序类,通常可以在自动装配或使用代码块之间进行选择。 但是其他类的限制更大:它们不能通过公共构造函数实例化。 相反,您必须使用某种工厂来创建该类型的实例。 这对于DI容器(DI Container)总是很麻烦,因为默认情况下,它们会照顾公共构造函数。

提示 默认情况下,只要将Instructor的构造函数定义为public,Simple Injector就能实例化它们。 但是,可以通过替换默认的IConstructorResolutionBehavior实现(https://simpleinjector.org/xtpcr)来覆盖此行为。

考虑公共JunkFood类的以下示例构造函数:

internal JunkFood(string name)

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

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

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

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

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

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

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

在本部分中,您已经了解了如何使用Simple Injector处理更困难的API。 您可以将Register方法与代码块一起使用,以实现更加类型安全的方法。 我们尚未研究如何使用多个组件,因此现在让我们将注意力转向该方向。

使用多个组件#

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

大多数情况下,当一个类具有多个重载的构造函数时,大多数容器都包含一些用于选择正确的构造函数的试探法,默认情况下,Simple Injector会引发异常,这说明该类型的定义不明确。 尽管如第4.2.3节所述,可以忽略此行为(请参阅https://simpleinjector.org/xtpcr),但我们的建议是防止使用多个构造函数创建组件。

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

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

在本节中,我们将研究每种情况,并查看Simple Injector如何依次解决每种情况。 完成后,即使正在进行同一Abstraction的多个实现,您也应该能够注册和解析组件。 首先,让我们看看如何在模棱两可的情况下提供细粒度的控制。

在多个候选人中选择#

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

配置同一服务的多种实现

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

container.Collection.Register<IIngredient>( typeof(SauceBéarnaise), typeof(Steak));

本示例将Steak和SauceBéarnaise类注册为IIngredient服务序列。 您可以要求容器解析所有IIngredient组件。 Simple Injector具有专用的方法来执行此操作:GetAllInstances获取具有所有已注册成分的IEnumerable。 这是一个例子:

IEnumerable<IIngredient> ingredients = container.GetAllInstances<IIngredient>();

您也可以要求容器使用GetInstance解析所有IIngredient组件:

IEnumerable<IIngredient> ingredients = container.GetInstance<IEnumerable<IIngredient>>();

请注意,您请求IEnumerable <IIngredient>,但是您使用常规的GetInstance方法。 Simple Injector将此解释为惯例,并为您提供了它具有的所有IIngredient组件。

提示 作为IEnumerable 的替代方法,您还可以请求IList ,ICollection ,IReadOnlyList 和IReadOnlyCollection 之类的抽象。 结果是等效的:在所有情况下,您都获得了所请求类型的所有组件。

当某个抽象有多种实现时,通常会有一个依赖序列的使用者。 但是,有时组件需要与固定集合或相同抽象的依赖项子集一起使用,这将在下面讨论。

使用条件注册消除歧义

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

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

在这种情况下,您具有三个相同类型的依赖项,每个依赖项代表一个不同的概念。 在大多数情况下,您希望将每个依赖项映射到一个单独的类型。 对于大多数DI容器(DI Container),解决此类问题的典型方法是使用键注册或命名注册,就像在上一章中使用Autofac所看到的那样。 使用Simple Injector,解决方案通常是更改依赖关系的注册,而不是使用方的注册。 以下清单显示了如何选择注册ICourse映射。

清单14.9根据构造函数的参数名称注册课程

container.Register<IMeal, ThreeCourseMeal>();  <--ThreeCourseMeal是使用通常的自动接线注册完成的。

container.RegisterConditional<ICourse, Rillettes>(
c => c.Consumer.Target.Name == "entrée");

container.RegisterConditional<ICourse, CordonBleu>(
c => c.Consumer.Target.Name == "mainCourse");
container
    
.RegisterConditional<ICourse, MousseAuChocolat>(
c => c.Consumer.Target.Name == "dessert"); 
<--根据消费类型目标的名称,有条件地注册这三门课程。 目标可以是属性,也可以是构造函数参数。 在这种情况下,目标是ThreeCourseMeal的构造函数参数。

让我们仔细看看这里发生了什么。 RegisterConditional方法接受Predicate 值,该值使它可以确定是否应将注册注入使用者。 它具有以下签名:

public void RegisterConditional<TService, TImplementation>( Predicate<PredicateContext> predicate) where TImplementation : class, TService where TService : class;

System.Predicate 是.NET委托类型。 谓词值将由Simple Injector调用。 如果谓词返回true,则使用给定使用者的注册。 否则,Simple Injector期望另一个条件注册具有一个返回true的委托。 如果找不到注册,则会引发异常,因为在这种情况下,无法构造对象图。 同样,当有多个适用的注册时,它也会引发异常。

正如我们之前讨论的有关具有多个构造函数的组件一样,Simple Injector十分严格,并且从不假定您知道要选择的内容。 但是,这确实意味着,简单注入器始终调用所有适用条件注册的所有谓词以查找可能的重叠注册。 这似乎效率不高,但是只有在首次解析组件时才调用这些谓词。 以下任何解决方案均具有所有可用信息,这意味着其他解决方案非常快捷。

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

通过使用条件注册的组件覆盖自动装配,您可以使Simple Injector生成整个对象图,而不必还原为注册代码块,正如我们在14.3.3节中讨论的那样。 由于先前讨论的诊断功能,在使用Simple Injector时这很有用。 使用代码块会使容器蒙蔽,这可能会导致配置错误在很长一段时间内未被发现。

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

接入顺序#

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

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

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

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

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

在14.4.1节中,您看到了ThreeCourseMeal及其固有的含糊性如何迫使您使注册复杂化。 这应该提示您重新考虑API设计。 一个简单的概括就趋向于实现IMeal的实现,该实现采用任意数量的ICourse实例,而不是像ThreeCourseMeal类那样恰好是三个实例:

public Meal(IEnumerable<ICourse> courses)

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

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

自动接线顺序

Simple Injector对序列有很好的了解,因此,如果您要使用给定服务的所有已注册组件,则自动装配就可以了。 例如,给定一组已配置的ICourse实例,您可以像这样配置IMeal服务:

container.Register<IMeal, Meal>();

请注意,这是从抽象到具体类型的完全标准的映射。 Simple Injector会自动了解Meal构造函数,并确定正确的操作步骤是解析所有ICourse组件。 解决IMeal时,您将获得带有ICourse组件的Meal实例。 这仍然需要您注册ICourse组件的序列,例如,使用自动注册:

container.Collection.Register<ICourse>(assembly);

Simple Injector自动处理序列,除非您另外指定,否则它会执行您期望的操作:它为该Abstraction的所有注册解析一个依赖项序列。 仅当您需要显式地从较大的集合中仅选择某些组件时,才需要执行更多操作。 让我们看看如何做到这一点。

仅从较大的集合中选择一些组件

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

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

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

先前让Simple Injector自动注册和自动连接所有已配置的实例时,它对应于图右侧所示的情况。 如果要按左侧所示注册组件,则必须明确定义应使用的组件。 为了实现此目的,可以使用Collection.Create方法,该方法允许创建序列的子集。 以下清单显示了如何将序列的子集注入使用者。

清单14.10将序列子集注入使用者

IEnumerable<ICourse> coursesSubset1 = container.Collection.Create<ICourse>( 
    typeof(Rillettes), 
    typeof(CordonBleu), t
    ypeof(MousseAuChocolat)
);   <-------------------创建三个课程的序列

IEnumerable<ICourse> coursesSubset2 = container.Collection.Create<ICourse>( 
    typeof(CeasarSalad), 
    typeof(ChiliConCarne), 
    typeof(MousseAuChocolat)
);    <------------------------- 创建具有不同课程子集的另一个序列

container.RegisterInstance<IMeal>( new Meal(sourcesSubset1));  <-----------通过注入第一个序列创建一个Meal实例,并将其映射到IMeal

使用Collection.Create方法可以创建给定抽象的序列。 序列本身不会在容器中注册-可以使用Collection.Register完成。 通过为相同的抽象调用Collection.Create多次,您可以创建都是不同子集的多个序列,如清单14.10所示。

清单14.10可能令人惊讶的是,对Collection.Create的调用当时并未创建课程。 而是,序列是一个流。 仅当您开始迭代序列时,它才会开始解析实例。 由于这种行为,序列子集可以安全地注入到Singleton Meal中,而不会造成任何伤害。 我们将在第14.4.5节中详细介绍流。

Simple Injector本身了解序列。 除非您需要从给定类型的所有服务中明确选择某些组件,否则Simple Injector会自动执行正确的操作。

自动装配不仅适用于单个实例,而且适用于序列。 容器将序列映射到相应类型的所有已配置实例。 Decorator设计模式可能是不太直观的使用具有相同Abstraction的多个实例的方法,我们将在下面讨论。

接入装饰器#

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

Simple Injector具有使用RegisterDecorator方法注册装饰器的内置支持。 并且,在本节中,我们将讨论非通用和通用抽象的注册。 让我们从前者开始。

装饰非泛型抽象

使用RegisterDecorator方法,可以优雅地注册Decorator。 以下示例显示了如何使用此方法将面包屑应用于VealCutlet:

var c = new Container(); 
c.Register<IIngredient, VealCutlet>();  <---------  将VealCutlet注册为IIngredient
c.RegisterDecorator<IIngredient, Breading>(); <------ 将Breading注册为IIngredient的装饰器。 解决IIngredient时,Simple Injector会返回包裹在Breading中的VealCutlet。

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

var c = new Container();
c.Register<IIngredient, VealCutlet>(); 
c.RegisterDecorator<IIngredient, HamCheeseGarlic>();  <----- 添加一个新的装饰器
c.RegisterDecorator<IIngredient, Breading>();

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

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

装饰器按注册顺序应用。

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

装饰泛型抽象

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

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

container.Register( typeof(ICommandService<>), assembly);  <-------- 注册任意ICommandService <TCommand>实现

container.RegisterDecorator( 
    typeof(ICommandService<>), 
    typeof(AuditingCommandServiceDecorator<>)); 

container.RegisterDecorator( 
    typeof(ICommandService<>), 
    typeof(TransactionCommandServiceDecorator<>)); 

container.RegisterDecorator( 
    typeof(ICommandService<>), 
    typeof(SecureCommandServiceDecorator<>));        <----3-13行 注册通用装饰器

如清单14.3所示,您可以使用Register重载来通过扫描程序集来注册任意ICommandService 实现。 若要注册通用装饰器,请使用RegisterDecorator方法,该方法接受两个Type实例。 清单14.11的配置结果如图14.6所示,我们之前在10.3.4节中进行了讨论。

图14.6通过事务,审计和安全性方面丰富实际的命令服务
image

关于Simple Injector对装饰器的支持,这只是冰山一角。 几个RegisterDecorator重载允许有条件地进行Decorator,就像前面讨论的清单14.9的RegisterConditional重载一样。 但是,对此功能和其他功能的讨论不在本书的讨论范围之内。

使用简单注入器,您可以通过几种不同的方式使用多个Decorator实例。 您可以将组件注册为彼此的替代品,也可以将对等体注册为序列,也可以注册为分层装饰器。 在许多情况下,Simple Injector会弄清楚该怎么做。 如果需要更明确的控制,则始终可以明确定义服务的组成方式。

在本节中,我们重点介绍为配置装饰器而明确设计的Simple Injector的方法。 尽管依赖项序列的使用者可以最直观地使用同一抽象的多个实例,但装饰器是另一个很好的例子。 但是,在第三种情况下,也许会有一些令人惊讶的情况,其中有多个实例在起作用,这就是Composite设计模式。

接入组合#

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

接入非通用组合

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

清单14.12第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);
        }
    }
}

由于Simple Injector API将序列的注册与非序列的注册分开,因此Composites的注册再简单不过了。 您可以将组合注册为单个注册,同时将其依存关系注册为一个序列:

container.Collection.Register<INotificationService>(
    typeof(OrderApprovedReceiptSender),
    typeof(AccountingNotifier), 
    typeof(OrderFulfillment)
);

container.Register<INotificationService, CompositeNotificationService>();

在上一个示例中,使用Collection.Register将三个INotificationService实现注册为序列。 另一方面,CompositeNotificationService被注册为单个非序列注册。 所有类型均通过简单喷油器自动接线。 使用先前的注册,当解析INotificationService时,将导致类似于下面的Pure DI表示形式的对象图:

return new CompositeNotificationService(
    new INotificationService[] { 
        new OrderApprovedReceiptSender(),
        new AccountingNotifier(), 
        new OrderFulfillment()
});

由于通知服务的数量可能会随着时间的推移而增加,因此您可以通过使用接受程序集的Collection.Register重载来应用自动注册,从而减轻“组合根(Composition Root)”的负担。 这使您可以将前面的类型列表转换为简单的单行代码:

container.Collection.Register<INotificationService>(assembly); 
container.Register<INotificationService, CompositeNotificationService>();

您可能会从第13章中回想起,Autofac中的类似构造无效,因为Autofac的自动注册功能会注册Composite以及序列的一部分。 但是,对于简单喷射器而言,情况并非如此。 它的Collection.Register方法会自动过滤掉所有Composite类型,并阻止将它们注册为序列的一部分。

但是,复合类并不是唯一由Simple Injector自动从列表中删除的类。 简单注入器也以相同的方式检测装饰器。 此行为使在简单喷射器中使用“装饰器”和“复合材料”变得轻而易举。 使用通用Composites的情况也是如此。

接入通用组合

在14.4.2节中,您看到了Simple Injector的RegisterDecorator方法如何使注册通用Decorators看起来像孩子的游戏。 在本节中,我们将介绍如何为常规抽象注册Composites。

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

container.Collection.Register(typeof(IEventHandler<>), assembly);

与清单14.3中ICommandService 实现的注册相反,您现在使用Collection.Register而不是Register。 那是因为特定事件类型可能会有多个处理程序。 这意味着您必须明确声明您知道单一事件类型将有更多实现。 如果您不小心调用了Register而不是Collection.Register,Simple Injector将引发类似于以下的异常:

​ In the supplied list of types or assemblies, there are 3 types that represent the same closed-generic type IEventHandler. Did you mean to register the types as a collection using the Collection.Register method instead? Conflicting types: OrderApprovedReceiptSender, AccountingNotifier, and OrderFulfillment.

关于此消息的一件好事是,它已经表明您很可能应该使用Collection.Register而不是Register。 但是也有可能您不小心添加了一个无效的类型,该类型已被拾取。 如前所述,当涉及到歧义时,Simple Injector会强制您保持明确,这有助于检测错误。

剩下的就是CompositeEventHandler 的注册。 由于CompositeEventHandler 是通用类型,因此您必须使用接受Type参数的Register重载:

container.Register( 
    typeof(IEventHandler<>), 
      typeof(CompositeEventHandler<>)); <------ 由于Composite的目标是隐藏序列的存在,因此Composite被注册为单个非序列映射。

使用此注册,当请求特定的关闭的IEventHandler 抽象(例如IEventHandler )时,简单注入器将确定要创建的确切CompositeEventHandler 类型。 在这种情况下,这非常简单,因为请求IEventHandler 会导致CompositeEventHandler 得到解决。 在其他情况下,确定确切的封闭类型可能是一个相当复杂的过程,但是Simple Injector可以很好地处理这一问题。

在Simple Injector中,处理序列非常简单。 但是,当涉及到解析和注入序列时,Simple Injector的表现方式与其他DI容器(DI Container)相比则更具吸引力。 正如我们前面提到的,Simple Injector将序列作为流处理。

序列是流#

在第14.1节中,您注册了以下成分序列:

container.Collection.Register<IIngredient>( typeof(SauceBéarnaise), typeof(Steak));

如前所示,您可以要求容器使用GetAllInstances或GetInstance方法解析所有IIngredient组件。 这是再次使用GetInstance的示例:

IEnumerable<IIngredient> ingredients = container.GetInstance<IEnumerable<IIngredient>>();

您可能希望对GetInstance <IEnumerable >()的调用会创建两个类的实例,但这离事实还远。 在解析或注入IEnumerable 时,Simple Injector不会立即将所有成分预先填充到序列中。 而是,IEnumerable 的行为类似于流。10这意味着返回的IEnumerable 是一个对象,可以在迭代时生成新的IIngredient实例。 这类似于使用System.IO.FileStream从磁盘流式传输数据或使用System.Data .SqlClient.SqlDataReader从数据库流式传输数据,其中数据以小块的形式到达,而不是一次性预取所有数据。

就我们所知,Simple Injector是唯一一个流化抽象序列的DI容器(DI Container)。

以下示例显示了如何多次迭代流可以产生新的实例:

IEnumerable<IIngredient> stream =
    container.GetAllInstance<IIngredient>();

IIngredient ingredient1 = stream.First(); <---使用LINQ的Enumerable,迭代配料流以解析第一个配料SauceBéarnaise。第一种扩展方法
IIngredient ingredient2 = stream.First(); <--再次迭代配料流

object.ReferenceEquals(ingredient1, ingredient2); <--返回false,因为每次迭代该流时,都会请求该容器解析一个实例

在对流进行迭代时,它会回调到容器中,以根据其适当的Lifestyle解析序列中的元素。 这意味着,如果类型注册为Transient,则总是生成新的实例,如前面的示例所示。 但是,当类型为Singleton时,每次都会返回相同的实例:

返回第一个实例后,对First的调用将停止迭代流,这意味着仅创建SauceBéarnaise,而未创建Steak实例。 不过,令人惊讶的是,对Last的调用不会同时导致第一个和最后一个元素的创建,而只会导致最后一个元素的创建,这在使用流时不会期望的。 这是由Enumerable.Last中的优化与Simple Injector返回的对象组合引起的。

返回的序列实现IList 。 当您将序列视为流时,这可能看起来很奇怪,但这是可能的,因为在配置阶段结束之后,序列中的项数是固定的。 Enumerable .Last对IList 进行了优化,允许它仅使用List 的索引器请求最后一个元素,而不必迭代整个列表。

尽管流并不是DI容器(DI Container)的共同特征,但它具有一些有趣的优点。 首先,将流注入使用者时,流本身的注入实际上是免费的,因为在那个时间点没有实例创建。11当元素列表很大并且在执行过程中不需要所有元素时,这很有用。 消费者的生命周期。 以下面的Composite ILogger实现为例。 它是代码清单8.22的“组合”的一种变体,但是在这种情况下,“组合”在其中一个包装好的记录器成功后立即停止记录。

清单14.13 处理一部分注入流的Composite

public class CompositeLogger : ILogger
{
    private readonly IEnumerable<ILogger> loggers;
    public CompositeLogger(
        IEnumerable<ILogger> loggers)
    {
        this.loggers = loggers;
    }
    public void Log(LogEntry entry)
    {
        foreach (ILogger logger in this.loggers)
        {
            try
            {
                logger.Log(entry);
                break;
            }
            catch { }
        }
    }
}

正如在14.4.4 节中所看到的,您可以注册CompositeLogger和ILogger实现的序列,如下所示:

container.Collection.Register<ILogger>(assembly); 
container.Register<ILogger, CompositeLogger>(Lifestyle.Singleton);

在这种情况下,您将CompositeLogger注册为Singleton,因为它是无状态的,并且它的唯一依赖项IEnumerable 本身就是Singleton。 CompositeLogger和ILogger序列作为Singletons的作用是CompositeLogger的注入实际上是免费的。 即使使用者调用其Dependency的Log方法,通常也只会导致创建序列的第一个ILogger实现-并非全部。

序列为流的第二个优点是,只要只存储对IEnumerable 的引用(如清单14.13所示),序列的元素就永远不会偶然成为Captive Dependencies。 前面的示例已经显示了这一点。 Singleton CompositeLogger可以安全地依赖IEnumerable ,因为它也是Singleton,即使它提供的服务可能不是。

在本节中,您已经了解了如何处理多个组件,例如序列,装饰器和合成器。 到此结束我们对简单注入器的讨论。 在下一章中,我们将注意力转向Microsoft.Extensions.DependencyInjection。

总结#

  • Simple Injector是一种现代的DI容器(DI Container),提供了相当全面的功能集,但其API与大多数DI容器(DI Container)完全不同。 以下是其一些特征属性:
    • 范围是环境
    • 使用Collection.Register来注册序列,而不是附加同一Abstraction的新注册。
    • 序列表现为流。
    • 可以诊断该容器以找到常见的配置陷阱。
  • 简单喷射器的一个重要总体主题是严格性。 它不会试图猜测您的意思,而是会尝试通过其API和诊断工具来防止和检测配置错误。
  • Simple Injector强制将注册和分辨率严格分开。 尽管您使用相同的Container实例进行注册和解析,但是Container在首次使用后被锁定。
  • 由于Simple Injector的环境范围很大,因此直接从根容器中进行解析是一种很好的做法,而且值得鼓励:它不会导致内存泄漏或并发错误。
  • Simple Injector支持标准的生活方式:瞬态,单例和作用域。
  • Simple Injector对序列,Decorator,Composite和Generic的注册提供了出色的支持。
posted @   F(x)_King  阅读(350)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示
主题色彩