第十五章- Microsoft.Extensions.DependencyInjection DI容器
Microsoft.Extensions.DependencyInjection DI容器(DI Container)
随着ASP.NET Core的引入,Microsoft引入了自己的DI容器(DI Container)Microsoft.Extensions.DependencyInjection,作为Core框架的一部分。 在本章中,我们将该名称简称为MS.DI。
Microsoft构建了MS.DI,以简化与ASP.NET Core一起使用的框架和第三方组件开发人员的依赖关系管理。 微软的意图是定义一个具有最小,最低公分母功能集的DI容器(DI Container),所有其他DI容器(DI Container)都可以遵循该特征集。
在本章中,我们将为MS.DI提供与Autofac和Simple Injector相同的处理方式。 您将看到MS.DI可以在多大程度上应用第1-3部分中阐述的原理和模式。 即使MS.DI集成在ASP.NET Core中,也可以单独使用,这就是为什么在本章中我们将其按原样对待。
但是,在本章的过程中,您会发现MS.DI的功能如此有限,以至于我们认为它不适合开发任何大小适中的应用程序,这些应用程序会进行松耦合并遵循本书中所述的原理和模式。 如果不适合使用MS.DI,那么为什么要在本书中使用整章介绍它呢? 最重要的原因是,MS.DI乍一看就像其他DI容器(DI Container)一样,您需要花一些时间来了解它与成熟的DI容器(DI Container)之间的区别。 由于它是.NET Core的一部分,因此如果您不了解其局限性,可能会尝试使用此内置容器。 本章的目的是揭示这些限制,以便您可以做出明智的决定。
注如果您对MS.DI不感兴趣,并且您已经决定使用其他DI容器(DI Container),则可以跳过本章。
本章分为四个部分。 您可以独立阅读每个部分,尽管第一部分是其他部分的前提条件,而第四部分则依赖于第三部分中介绍的某些方法和类。 您可以将本章与第4部分的其余部分分开阅读,以专门了解MS.DI,也可以将其与其他章一起阅读以比较DI容器(DI Container)。 本章的重点是说明MS.DI如何关联和实现第1-3部分中描述的模式和原理。
引入Microsoft.Extensions.DependencyInjection
在本部分中,您将学习在何处获取MS.DI,获得的内容以及如何开始使用它。 我们还将介绍常见的配置选项。
总体而言,使用MS.DI与Autofac并没有什么不同(在第13章中进行了讨论)。 如图15.1所示,它的使用分为两个步骤。 但是,与Simple Injector相比,对于MS.DI来说,此两步过程非常明确:首先,配置ServiceCollection,然后完成此操作,然后使用它来构建可用于解析组件的ServiceProvider。
图15.1使用Microsoft.Extensions.DependencyInjection的模式是首先配置它,然后解析组件。 |
---|
完成本节后,您应该对MS.DI的总体使用模式有很好的了解,并且应该能够在行为规范的场景中开始使用它-所有组件都遵循正确的DI模式,例如 构造函数注入(Constructor Injection)。 让我们从最简单的场景开始,看看如何使用MS.DI容器(DI Container)解析对象。
解析对象
任何DI容器(DI Container)的核心服务是组成对象图。 在本部分中,我们将研究使您能够使用MS.DI组成对象图的API。 MS.DI要求您注册所有相关组件,然后才能解决它们。 下面的清单显示了MS.DI的最简单的用法之一。
清单15.1最简单地使用MS.DI
var services = new ServiceCollection();
services.AddTransient<SauceBéarnaise>();
ServiceProvider container = services.BuildServiceProvider(validateScopes: true);
IServiceScope scope = container.CreateScope();
SauceBéarnaise sauce = scope.ServiceProvider.GetRequiredService<SauceBéarnaise>();
正如图15.1所暗示的那样,您需要一个ServiceCollection实例来配置组件。 MS.DI的ServiceCollection相当于Autofac的ContainerBuilder。
在这里,您将在服务中注册具体的SauceBéarnaise类,以便当您要求它构建容器时,将使用SauceBéarnaise类配置生成的容器。 这再次使您能够从容器中解析SauceBéarnaise类。 如果您未注册SauceBéarnaise组件,则尝试对其进行解析将引发InvalidOperationException并显示以下消息:
No service for type 'Ploeh.Samples.MenuModel.SauceBéarnaise' has been registered.
注 创建ASP.NET Core应用程序时,托管环境将为您创建ServiceCollection。 在那种情况下,您只需要使用它,如清单7.7所示。 但是,在本章中,我们会将MS.DI视为其他DI容器(DI Container),这意味着我们将展示如何在集成度较低的环境中使用它。
如清单15.1所示,对于MS.DI,您永远不会从根容器本身进行解析,而只能从IServiceScope进行解析。 15.2.1节将详细介绍IServiceScope是什么。
警告 使用MS.DI,避免从根容器中进行解析。 这很容易导致内存泄漏或并发错误。 相反,您应该始终从作用域进行解析,如清单15.1所示。
作为安全措施,请始终使用BuildServiceProvider重载来构建ServiceProvider,并将validateScopes参数设置为true,如清单15.1所示。 这样可以防止从根容器中意外解析作用域实例。 随着ASP.NET Core 2.0的引入,当应用程序在开发环境中运行时,框架会自动将validateScopes设置为true,但是最好甚至在开发环境之外也启用验证。 这意味着您必须手动调用BuildServiceProvider(true)。
MS.DI不仅可以使用无参数构造函数来解析具体类型,还可以自动连接具有其他依赖关系的类型。 所有这些依赖项都需要注册。 在大多数情况下,您希望对接口进行编程,因为这会引入松散耦合(Loose Coupling)。 为此,MS.DI允许您将抽象映射到具体类型。
将抽象映射到具体类型
如清单15.1所示,我们应用程序的根类型通常由其具体类型解析,而松散耦合(Loose Coupling)则需要您将抽象映射到具体类型。 基于此类映射创建实例是任何DI容器(DI Container)提供的核心服务,但是您仍然必须定义映射。 在此示例中,您将IIngredient接口映射到具体的SauceBéarnaise类,这使您能够成功解析IIngredient:
var services = new ServiceCollection();
services.AddTransient<IIngredient, SauceBéarnaise>(); <----- 将具体类型映射到特定的抽象
var container = services.BuildServiceProvider(true);
IServiceScope scope = container.CreateScope();
IIngredient sauce = scope.ServiceProvider .GetRequiredService<IIngredient>(); <--- 将SauceBéarnaise解析为IIredredient
在这里,AddTransient方法允许使用“临时生活方式”将具体类型映射到特定的抽象。 由于先前有AddTransient调用,因此SauceBéarnaise现在可以解析为IIngredient。
在许多情况下,您只需要通用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实例,因此无法使用泛型,而必须使用弱类型的API。 MS.DI提供了GetRequiredService方法的弱类型重载,该方法使您可以实现Create方法:
Type controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
return scope.ServiceProvider.GetRequiredService(controllerType);
通过GetRequiredService的弱类型重载,可以将controllerType变量直接传递给MS.DI。 通常,这意味着您必须将返回的值强制转换为某种抽象,因为弱类型的GetRequiredService方法将返回object。 但是,对于IControllerActivator,则不需要这样做,因为ASP.NET Core MVC不需要控制器来实现任何接口或基类。
无论您使用的是哪种GetRequiredService的重载,MS.DI都会保证它会返回所请求类型的实例,或者在存在无法满足的依赖关系时引发异常。 正确配置了所有必需的依赖项后,MS.DI可以自动连接所请求的类型。
注 除了GetRequiredService之外,还有一种GetService方法。 当无法解析所请求的类型时,GetRequiredService会引发异常,而GetService返回null。 当您希望返回实例时,您应该首选GetRequiredService,这几乎总是如此。
为了能够解析请求的类型,必须预先配置所有松散耦合(Loose Coupling)的Dependencies。 让我们研究一下配置MS.DI的方法。
配置ServiceCollection
正如我们在12.2节中讨论的那样,您可以用几种概念上不同的方式配置DI容器(DI Container)。 图12.5审阅了以下选项:配置文件,“配置为代码”和“自动注册”。 图15.2再次显示了这些选项。
图15.2针对显式维度和绑定程度配置DI容器(DI Container)的最常用方法 |
---|
注 MS.DI是围绕“配置为代码”而设计的,不包含支持配置文件或自动注册的API。
尽管没有自动注册API,但在一定程度上,您可以借助.NET的LINQ和反射API来实现程序集扫描。 在讨论此问题之前,我们将首先讨论MS.DI的“配置为代码API”。
使用配置作为代码配置ServiceCollection
在15.1.1节中,您简要了解了MS.DI的强类型配置API。 在这里,我们将对其进行更详细的研究。
尽管大多数方法是扩展方法,但MS.DI中的所有配置都使用ServiceCollection类公开的API。 最常用的方法之一是您已经看到的AddTransient方法:
services.AddTransient<IIngredient, SauceBéarnaise>();
将SauceBéarnaise注册为IIngredient隐藏了具体类,因此您无法再通过此注册来解析SauceBéarnaise。 但是您可以通过以下方式替换注册来解决此问题:
services.AddTransient<SauceBéarnaise>();
services.AddTransient<IIngredient>(
c => c.GetRequiredService<SauceBéarnaise>()); <-------- 注册一个代表,该代表回调到容器中以解析先前注册的具体类型
您不必注册使用AddTransient的Auto-Wiring重载进行IIngredient的注册,而是注册一个代码块,该代码块在被调用时将调用转发到具体的SauceBéarnaise的注册。
Torn Lifestyles
在本节中,我们展示了如何多次调用AddTransient来将组件注册为多种服务类型。 以下示例再次显示了这一点:
services.AddTransient<SauceBéarnaise>(); services.AddTransient<IIngredient>( c => c.GetRequiredService<SauceBéarnaise>());
但是,您可能会想起这等效于以下代码:
services.AddTransient<SauceBéarnaise>(); services.AddTransient<IIngredient, SauceBéarnaise>();
但是,前一个示例并不等同于后者。 当您将“生活方式”从“瞬态”更改为“单例”时,这一点变得显而易见。
services.AddSingleton<SauceBéarnaise>(); services.AddSingleton<IIngredient, SauceBéarnaise>();
尽管您可能希望在容器的生存期内只有一个SauceBéarnaise实例,但是拆分注册会使MS.DI为每个AddSingleton调用创建一个单独的实例。 因此,SauceBéarnaise的生活方式被认为已被破坏。
警告 每次对AddScoped和AddSingleton方法之一的调用都会导致其自己的唯一缓存。 因此,具有多个Add ...调用可能会导致每个作用域或每个容器有多个实例。 为避免这种情况,请注册一个解析具体实例的委托。
在实际的应用程序中,始终要映射多个抽象,因此必须配置多个映射。 这是通过多次调用“添加...”之一来完成的。 方法:
services.AddTransient<IIngredient, SauceBéarnaise>();
services.AddTransient<ICourse, Course>();
这将IIngredient映射到SauceBéarnaise,将ICourse映射到Course。 类型没有重叠,因此应该很清楚地说明发生了什么。 但是您也可以多次注册相同的抽象:
services.AddTransient<IIngredient, SauceBéarnaise>();
services.AddTransient<IIngredient, Steak>();
在这里,您注册了IIngredient两次。 如果解决IIngredient,您将获得Steak的实例。 上次注册获胜,但不会忘记以前的注册。 MS.DI可以为同一个抽象处理多种配置,但是我们将在15.4节中回到本主题。
尽管有更多高级选项可用于配置MS.DI,但是您可以使用此处显示的方法配置整个应用程序。 但是,为了避免过多地显式维护容器配置,可以改用使用自动注册的基于约定的方法。
使用自动注册配置ServiceCollection
在许多情况下,注册将是相似的。 如我们在第12.3.3节中所讨论的那样,此类注册非常繁琐,并且显式注册每个组件可能不是最有效的方法。
考虑一个包含许多IIngredient实现的库。 您可以分别配置每个类,但是这会导致提供给Add ...方法的Type实例列表不断变化。 更糟糕的是,每次添加新的IIngredient实现时,如果希望可用,还必须在容器中显式注册它。 声明应该在给定程序集中找到的所有IIngredient实现都将被注册会更有效率。
如前所述,MS.DI不包含自动注册API。 这意味着您必须自己做。 在某种程度上这是可能的,并且在本节中,我们将通过一个简单的示例演示如何实现,但是将有关可能性和局限性的详细讨论推迟到15.4节。 让我们看一下如何注册IIngredient注册序列:
Assembly ingredientsAssembly = typeof(Steak).Assembly;
var ingredientTypes = from type in ingredientsAssembly.GetTypes()
where !type.IsAbstract
where typeof(IIngredient).IsAssignableFrom(type)
select type; <-------------------------基于约定的扫描
foreach (var type in ingredientTypes)
{
services.AddTransient(typeof(IIngredient), type); <-------------- 根据IIngredient接口注册每种类型
}
前面的示例无条件地配置了IIngredient接口的所有实现,但是您可以提供允许仅选择一个子集的过滤器。 这是一个基于约定的扫描,其中您仅添加名称以Sauce开头的类:
Assembly ingredientsAssembly = typeof(Steak).Assembly;
var ingredientTypes =
from type in ingredientsAssembly.GetTypes()
where !type.IsAbstract
where typeof(IIngredient).IsAssignableFrom(type)
where type.Name.StartsWith("Sauce") <----- 删除名称不是以Sauce开头的类
select type;
foreach (var type in ingredientTypes)
{
services.AddTransient(typeof(IIngredient), type);
}
除了从程序集中选择正确的类型之外,自动注册的另一部分是定义正确的映射。 在前面的示例中,您将AddTransient方法与特定接口一起使用,以针对该接口注册所有选定的类型。
但是有时候您会想要使用不同的约定。 假设您使用抽象基类而不是接口,并且希望在程序集中注册所有类型,该程序集的名称以Policy的基本类型结尾:
Assembly policiesAssembly = typeof(DiscountPolicy).Assembly;
var policyTypes = from type in policiesAssembly.GetTypes() <------- 获取装配体中的所有类型
where type.Name.EndsWith("Policy") <------- 按策略后缀过滤
select type;
foreach (var type in policyTypes)
{
services.AddTransient(type.BaseType, type); <-------按其基类注册每个策略组件
}
即使MS.DI不包含基于约定的API,通过使用现有的.NET Framework API,也可以进行基于约定的注册。 关于泛型,这将变成另一种球类游戏,我们将在下面讨论。
通用抽象的自动注册
在第10章中,您将大型,令人讨厌的IProductService接口重构为清单10.12的ICommandService
public interface ICommandService<TCommand>
{
void Execute(TCommand command);
}
如第10章所述,每个命令Parameter Object代表一个用例,每个用例只有一个实现。 以清单10.8的AdjustInventoryService为例。 它实施了“调整库存”用例。 以下清单再次显示了该类。
清单15.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;
...
}
}
任何相当复杂的系统都将轻松实现数百个用例,这是使用自动注册的理想选择。 但是,由于MS.DI缺少自动注册支持,因此您必须编写大量代码才能运行此代码。 下一个清单提供了一个示例。
清单15.3 ICommandService
实现的自动注册
Assembly assembly = typeof(AdjustInventoryService).Assembly;
var mappings =
from type in assembly.GetTypes()
where !type.IsAbstract <------- 选择具体类型
where !type.IsGenericType <------ 选择非通用类型
from i in type.GetInterfaces() where i.IsGenericType where i.GetGenericTypeDefinition() == typeof(ICommandService<>) select new { service = i, type }; <----- 选择实现ICommandService <TCommand>的类型
foreach (var mapping in mappings)
{
services.AddTransient( mapping.service, mapping.type); <------ 通过其接口注册类型
}
与之前的清单一样,您可以充分利用.NET的LINQ和Reflection API,以便从提供的程序集中选择类。 使用提供的开放通用接口,可以遍历程序集类型列表,并注册实现ICommandService
警告 清单15.1中的代码存在许多缺点。 例如,如果您不小心在多个类上实现了相同的封闭通用接口,则注册将静默失败。 该代码将愉快地注册所有实现。 在请求命令服务的情况下,如果存在该类型的多个实现,则会解决最后的注册。 但是,一个主要问题是,尚不确定哪个注册是最后一次注册,甚至在应用程序重新启动后可能会改变!
本节介绍了MS.DI DI容器(DI Container),并演示了这些基本机制:如何配置ServiceCollection,以及随后如何使用构造的ServiceProvider解析服务。 只需调用GetRequiredService方法即可完成服务解析,因此复杂性涉及配置容器。 该API主要支持以代码形式进行配置,尽管在某种程度上可以在其之上构建自动注册。 但是,正如您稍后将看到的那样,缺少对自动注册的支持将导致非常复杂且难以维护的代码。 到目前为止,我们仅研究了最基本的API,但是还有另一个领域需要我们解决:如何管理组件的寿命。
管理生命周期
在第8章中,我们讨论了生命周期管理,包括最常见的概念生命周期样式,例如Transient,Singleton和Scoped。 MS.DI支持这三种生活方式,并允许您配置所有服务的生命周期。 表15.2中显示的生活方式可作为API的一部分获得。
Microsoft 名称 | 模式名称 | 解释 |
---|---|---|
Transient | Transient | 容器跟踪实例并进行处理。 |
Singleton | Singleton | 当处置容器时,处置实例。 |
Scoped | Scoped | 实例在同一IServiceScope中重用。 跟踪实例的整个生命周期,并在处置示波器时将其处置。 |
MS.DI的Transient和Singleton的实现与第8章中描述的常规生活方式等效,因此我们在本章中不会在它们上花费太多时间。 相反,在本节中,您将看到如何为代码中的组件定义生活方式。 在本节的最后,您应该可以在自己的应用程序中使用MS.DI的Lifestyles。 让我们首先回顾一下如何为组件配置实例范围。
配置生命方式
在本部分中,我们将回顾如何使用MS.DI管理生活方式。 生活方式被配置为注册组件的一部分。 就这么简单:
services.AddSingleton<SauceBéarnaise>();
这会将具体的SauceBéarnaise类配置为Singleton,以便每次请求SauceBéarnaise时都返回相同的实例。 如果要将抽象映射到具有特定生活方式的具体类,则可以将AddSingleton重载与两个通用参数一起使用:
services.AddSingleton<IIngredient, SauceBéarnaise>();
与其他DI容器(DI Container)相比,在为组件配置Lifestyle时,MS.DI中没有太多选择。 它以声明性的方式完成。 尽管配置通常很容易,但您一定不要忘记,某些Lifestyle涉及寿命长的对象,这些对象只要在资源周围就可以使用资源。
释放组件
如第8.2.2节所述,在完成对象的释放后,释放它们很重要。 与Autofac和Simple Injector相似,MS.DI没有显式的Release方法,而是使用一种称为范围的概念。 范围可以视为特定于请求的缓存。 如图15.3所示,它定义了可以重用组件的边界。
IServiceScope定义了可以用于特定持续时间或特定目的的缓存。 最明显的例子是Web请求。 当从IServiceScope请求范围对象的组件时,您总是会收到相同的实例。 与真正的Singletons的区别在于,如果查询第二个作用域,则将获得另一个实例。
图15.3 Microsoft.Extensions.DependencyInjection的作用域充当可以在有限的持续时间内或特定目的共享组件的容器。 |
---|
范围的重要功能之一是,它们使您可以在范围完成时适当地释放组件。 您可以使用特定IServiceProvider实现的CreateScope方法创建一个新范围,并通过调用其Dispose方法来释放所有适当的组件:
using (IServiceScope scope = container.CreateScope()) <---- 从根容器创建范围
{
IMeal meal = scope.ServiceProvider .GetRequiredService<IMeal>(); <---- 解决新创建的范围中的一餐
meal.Consume();
} <---- 通过结束using块释放meal
通过调用CreateScope方法从容器中创建一个新的作用域。 返回值实现IDisposable,因此您可以将其包装在using块中。 因为IServiceScope包含一个ServiceProvider属性,该属性实现与容器本身实现的接口相同的接口,所以您可以使用该范围来解析组件,其方式与容器本身完全相同。
示波器完成后,就可以处置它。 对于using块,退出该块时会自动发生,但是您也可以选择通过调用Dispose方法来显式处理它。 处置范围时,还释放了由范围创建的所有组件。 在这里,这意味着您释放了膳食对象图。
请注意,组件的依赖关系始终在组件范围内或以下解析。 例如,如果您需要向Singleton中注入一个Transient Dependency,则即使您是从嵌套作用域解析Singleton,该Transient Dependency也将来自根容器。 这将跟踪根容器中的瞬变,并防止在丢弃示波器时将其丢弃。 否则,Singleton使用者会崩溃,因为它依赖于已处置的组件而在根容器中保持活动状态。
重要 对于MS.DI,瞬态组件是指预期能够在注入它的使用者使用后才能使用的组件。 这就是为什么MS.DI允许将Transients注入到Singleton中的原因,尽管阻止了将Scoped实例注入到Singletons中。2尽管将Transient注入到Singleton中可能确实是所需的行为,但实际上并非如此。 您需要格外小心,以确保“瞬态”不会成为偶然的“圈养依赖性”。
在本节的前面,您了解了如何将组件配置为Singletons或Transients。 通过类似的方式将组件配置为具有“范围化生活方式”:
services.AddScoped<IIngredient, SauceBéarnaise>();
与AddTransient和AddSingleton方法类似,您可以使用AddScoped方法来声明组件的生存期应遵循创建实例的作用域。
警告 MS.DI跟踪大多数组件,甚至是一次性瞬态。 当您从根容器而不是作用域进行解析时,这将导致问题。 从根容器解析时,每次调用GetService时仍会创建新的实例,但是那些一次性的Transients仍保持活动状态,以便在处置容器时将其丢弃。 由于在应用程序停止运行之前不会丢弃根容器,这会导致内存泄漏,因此务必记住要从作用域中解析所有组件,并在使用后处置该作用域。
由于其性质,在容器本身的整个生命周期内都不会释放单例。 不过,如果您不再需要该容器,则可以释放那些组件。 这是通过处理容器本身来完成的:
container.Dispose()
在实践中,这并不像处理示波器那么重要,因为容器的寿命往往与其所支持的应用程序的寿命紧密相关。 通常,只要应用程序运行,就一直在保存该容器,因此只有在应用程序关闭时,才将其丢弃。 在这种情况下,操作系统将回收内存。
至此,我们完成了使用MS.DI进行生命周期管理的旅程。 可以使用混合生活方式配置组件,即使您注册同一Abstraction的多个实现,也是如此。 到目前为止,通过隐式假定所有组件都使用构造函数注入(Constructor Injection),您已经允许容器关联依赖项。 但这并非总是如此。 在下一节中,我们将介绍如何处理必须以特殊方式实例化的类。
注册不同的API
到目前为止,我们已经考虑了如何配置使用构造函数注入(Constructor Injection)的组件。 构造函数注入(Constructor Injection)的许多好处之一是,诸如MS.DI之类的DI容器(DI Container)可以轻松理解如何在依赖关系图中组成和创建所有类。 当API表现不佳时,这变得不太清楚。
在本节中,您将看到如何处理原始的构造函数参数和静态工厂。 这些都需要您的特别注意。 首先,我们看一下采用原始类型(例如字符串或整数)作为构造函数参数的类。
配置原始依赖项
只要将抽象注入消费者,一切都会好起来的。 但是,当构造函数依赖于基本类型(例如字符串,数字或枚举)时,将变得更加困难。 对于将连接字符串作为构造函数参数的数据访问实现,尤其如此,但这是一个更普遍的问题,适用于所有字符串和数字。
从概念上讲,将字符串或数字注册为容器中的组件并不总是很有意义。 使用通用类型约束,MS.DI甚至从其通用API阻止值类型的注册,例如数字和枚举。 另一方面,使用非通用API,这仍然是可能的。 以该构造函数为例:
public ChiliConCarne(Spiciness spiciness)
在此示例中,Spiciness是一个枚举:
public enum Spiciness { Mild, Medium, Hot }
提示 根据经验,枚举是代码的味道,应将其重构为多态类。3但是,在本示例中,它们很好地为我们服务。
如果希望所有Spiciness消费者使用相同的值,则可以彼此独立地注册Spiciness和ChiliConCarne:
services.AddSingleton( typeof(Spiciness), Spiciness.Medium);
<------ 使用非通用的AddSingleton重载,该重载接受预先创建的对象; 在这种情况下,枚举的值
services.AddTransient<ICourse, ChiliConCarne>(); <--- 自动连接带有辣味的ChiliConCarne
当您以后解决ChiliConCarne时,它就会具有中等的辣味,而其他所有依赖于辣味的成分也会有类似的辣味。 如果您想更好地控制ChiliConCarne和Spiciness之间的关系,则可以使用代码块,我们将在15.3.3节中稍作介绍。
此处描述的选项使用自动装配为组件提供具体值。 但是,更方便的解决方案是将原始依赖项提取到参数对象中。
提取对参数对象的原始依赖关系
在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);
services.AddSingleton<Flavoring>(flavoring);
container.AddTransient<ICourse, ChiliConCarne>();
这段代码创建了Flavoring类的单个实例。 调味成为课程的配置对象。 因为只有一个调味实例,所以您可以使用接受预先创建的实例的AddSingleton
与原始讨论的选项相比,将原始依赖项提取到参数对象中应该是您的首选,因为参数对象在功能和技术层面上都消除了歧义。 但是,它确实需要更改组件的构造函数,但这可能并不总是可行的。 在这种情况下,注册代表是您的第二选择。
用代码块注册对象
创建具有原始值的组件的另一种方法是使用Add ...方法之一,该方法可让您提供创建该组件的委托:
services.AddTransient<ICourse>(c => new ChiliConCarne(Spiciness.Hot));
之前在15.1.2节中讨论过撕裂的Lifestyles时,您已经看到了AddTransient方法的重载。 每当解决ICourse服务时,就会以热辣的方式调用ChiliConCarne构造函数。 下面的示例显示此AddTransient
public static IServiceCollection AddTransient<TService>( this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class;
如您所见,此AddTransient方法接受类型为Func <IServiceProvider,TService>的参数。 关于先前的注册,当解决了ICourse时,MS.DI将调用提供的委托,并将其提供给属于当前IServiceScope的IServiceProvider。 使用它,您的代码块可以解析源自同一IServiceScope的实例。 我们将在下一部分中对此进行演示。
对于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);
}
}
从MS.DI的角度来看,这是一个有问题的API,因为在静态工厂周围没有明确且完善的约定。 它需要帮助-您可以通过提供可以执行以创建实例的代码块来提供帮助:
services.AddTransient<IMeal>(c => JunkFoodFactory.Create("chicken meal"));
这次,您使用AddTransient方法通过在代码块内调用静态工厂来创建组件。 每次解决IMeal时,都会调用JunkFoodFactory.Create,并将返回结果。
如果您必须编写代码来创建实例,那么与直接调用代码相比,这有什么好处呢? 通过在AddTransient方法调用内使用代码块,您仍然可以获得一些好处:
- 您从IMeal映射到JunkFood。 这允许消耗类保持松散耦合(Loose Coupling)。
- 生活方式仍然可以配置。 尽管将调用代码块来创建实例,但可能不会在每次请求实例时都调用该代码块。 它是默认设置,但是如果将其更改为Singleton,则该代码块将仅被调用一次,结果将被缓存并在以后重用。
在本部分中,您已经了解了如何使用MS.DI处理更困难的创建API。 到目前为止,代码示例非常简单。 当您开始使用多个组件时,这将迅速改变,因此现在让我们将注意力转向该方向。
使用多个组件
正如第12.1.2节所提到的,DI容器(DI Container)在独特性上很兴旺,但在含糊不清的情况下却很难。 使用构造函数注入(Constructor Injection)时,单个构造函数比重载的构造函数更可取,因为很明显,在别无选择时要使用哪个构造函数。 从抽象到具体类型的映射时也是如此。 如果尝试将多个具体类型映射到同一Abstraction,则会引入歧义。
尽管模棱两可的特性令人不快,但您通常仍需要使用单个Abstraction的多个实现。 在以下情况下可能是这种情况:
- 不同的消费者使用不同的混凝土类型。
- 依赖关系是序列
- 装饰器或复合材料正在使用中。
在本节中,我们将研究每种情况,并了解如何使用MS.DI处理每种情况。 完成后,您应该对使用MS.DI可以做的事情有一个很好的感觉,并且在进行同一个Abstraction的多个实现时边界在哪里。 首先,让我们看看如何提供比“自动装配”更细粒度的控制。
在多个候选人中选择
自动接线既方便又强大,但几乎无法控制。 只要将所有Abstractions明确映射到具体类型,就不会有问题。 但是,一旦您引入了相同接口的更多实现,歧义就会浮出水面。 让我们首先回顾一下MS.DI如何处理相同抽象的多个注册。
配置同一服务的多种实现
正如在15.1.2节中所看到的,您可以注册同一接口的多个实现:
services.AddTransient<IIngredient, SauceBéarnaise>();
services.AddTransient<IIngredient, Steak>();
本示例将Steak和SauceBéarnaise类都注册为IIngredient服务。 最后一次注册获胜,因此,如果使用GetRequired- Service
您也可以要求容器解析所有IIngredient组件。 MS.DI有一个专用的方法来执行此操作,称为GetServices。 这是一个例子:
IEnumerable<IIngredient> ingredients = scope.ServiceProvider.GetServices<IIngredient>();
<------ 获取所有已注册成分的序列
在幕后,GetServices委托给GetRequiredService,同时请求IEnumerable
IEnumerable<IIngredient> ingredients = scope.ServiceProvider .GetRequiredService<IEnumerable<IIngredient>>();
请注意,您使用常规的GetRequiredService方法,但是您请求IEnumerable
当某个抽象有多种实现时,通常会有一个依赖序列的使用者。 但是,有时组件需要与固定集合或相同抽象的依赖项子集一起使用,这将在下面讨论。
使用代码块消除歧义
与自动装配一样有用,有时您需要覆盖常规行为以提供对依赖项到达何处的细粒度控制,但也可能是您需要解决模棱两可的API。 例如,考虑以下构造函数:
public ThreeCourseMeal(ICourse entrée, ICourse mainCourse, ICourse dessert)
在这种情况下,您具有三个相同类型的依赖项,每个依赖项代表一个不同的概念。 在大多数情况下,您希望将每个依赖项映射到一个单独的类型。
如前所述,与Autofac和Simple Injector相比,MS.DI的功能受到限制。 在Autofac提供密钥注册,而Simple Injector提供条件注册来处理这种歧义的地方,MS.DI在这方面不足。 没有任何内置功能可以执行此操作。 要使用MS.DI连接这样一个模棱两可的API,您必须还原为使用代码块。
services.AddTransient<IMeal>(c => new ThreeCourseMeal( <----- 使用lambda表达式注册IMeal
entrée: c.GetRequiredService<Rillettes>(),
mainCourse: c.GetRequiredService<CordonBleu>(),
dessert: c.GetRequiredService<CrèmeBrûlée>())); <----通过从容器请求注入三个构造函数参数
此注册从“自动装配”还原,并使用委托构造“ ThreeCourseMeal”。 幸运的是,这三个ICourse实现本身仍然是自动连线的。 要使Three-CourseMeal恢复自动装配,请使用MS.DI的ActivatorUtilities类。
使用ActivatorUtilities消除歧义
在此示例中,缺少Three-CourseMeal的自动装配并不成问题,因为在这种情况下,您将覆盖所有构造函数参数。 如果ThreeCourseMeal包含更多的依赖项,则可能会有所不同:
public ThreeCourseMeal(
ICourse entrée,
ICourse mainCourse,
ICourse dessert,
... <------------ 更多依赖
)
MS.DI包含一个称为ActivatorUtilities的实用程序类,该类允许自动装配类的依赖项,同时通过显式提供其依赖项来覆盖其他依赖项。 使用ActivatorUtilities,您可以重写以前的注册。
清单15.5使用ActivatorUtilities连线ThreeCourseMeal
services.AddTransient<IMeal>(
c => ActivatorUtilities.CreateInstance<ThreeCourseMeal>( <------ 要求创建ThreeCourseMeal
c, <------ 提供IServiceProvider
new object[] {
c.GetRequiredService<Rillettes>(),
c.GetRequiredService<CordonBleu>(),
c.GetRequiredService<MousseAuChocolat>() })); <--- 提供要覆盖的三个路线作为对象数组
本示例使用了ActivatorUtilities的CreateInstance
public static T CreateInstance<T>( IServiceProvider provider, params object[] parameters);
CreateInstance
由于所有三个已解决的课程都实施了ICourse,因此通话中仍然存在歧义。 CreateInstance
注 如果您过度指定了参数(例如,通过提供第四个ICourse值),则CreateInstance
与清单15.4相比,清单15.5有很大的缺点。 清单15.4由编译器验证。 对构造函数的任何重构都将允许该代码继续工作,或者由于编译错误而失败。
清单15.5则相反。 如果重新排列了三个ICourse构造函数参数,则代码将继续编译,并且ActivatorUtilities甚至可以构造一个新的ThreeCourseMeal。 但是,除非清单15.5根据该重新排列而更改,否则课程将以不正确的顺序注入,这很可能导致应用程序行为不正确。 不幸的是,没有任何重构工具会发出信号,表明注册也必须更改。
甚至Autofac和Simple Injector的相关注册(列表13.7和14.9)在防止错误方面也做得更好。 尽管这两个列表都不是类型安全的,但是由于两个列表都在确切的参数名称上匹配,所以在解析类时,对ThreeCourseMeal进行更改至少会导致异常。 这总比静默地失败更好,在清单15.5的情况下可能会发生这种情况。
通过将参数显式映射到组件来覆盖自动装配是一种普遍适用的解决方案。 如果您将命名注册与Autofac一起使用,将条件注册与Simple Injector和MS.DI一起使用,则可以通过传递手动解析的具体类型来覆盖参数。 如果要管理的类型很多,这可能会很脆弱。 更好的解决方案是设计您自己的API,以消除这种歧义。 它通常会导致更好的总体设计。
在下一节中,您将看到如何使用含糊不清,更灵活的方法来允许一餐中任意数量的课程。 为此,您必须学习MS.DI如何处理序列。
接线顺序
在第6.1.1节中,我们讨论了构造函数注入(Constructor Injection)如何作为违反单一责任原则的警告系统。 那时的教训是,与其将构造函数过度注入视为构造函数注入(Constructor Injection)模式的弱点,您不应该为它使问题设计变得如此明显而高兴。
当谈到DI Containers和歧义性时,我们看到了类似的关系。 DI容器(DI Container)通常不会以优雅的方式处理歧义。 尽管您可以使用它来处理DI容器(DI Container),但看起来似乎很尴尬。 这通常表明您可以改进代码的设计。
在本节中,我们将看一个示例,该示例演示了如何重构以消除歧义。 它还将显示MS.DI如何处理序列。
通过消除歧义来重构到更好的过程
在15.4.1节中,您看到了ThreeCourseMeal及其固有的歧义如何迫使您要么放弃自动装配,要么使用对ActivatorUtilities的冗长调用。 一个简单的概括就趋向于实现IMeal的实现,该实现采用任意数量的ICourse实例,而不是像ThreeCourseMeal类那样恰好是三个实例:
public Meal(IEnumerable<ICourse> courses)
请注意,与IEnumerable
在本部分中,我们将研究如何配置MS.DI来连接具有适当ICourse依赖关系的Meal实例。 完成后,您需要对需要配置具有依存关系序列的实例时可用的选项有一个很好的了解。
自动接线顺序
MS.DI可以理解序列,因此,如果您要使用给定服务的所有已注册组件,则自动装配就可以使用。 例如,您可以像这样配置IMeal服务及其课程:
services.AddTransient<ICourse, Rillettes>();
services.AddTransient<ICourse, CordonBleu>();
services.AddTransient<ICourse, MousseAuChocolat>();
services.AddTransient<IMeal, Meal>();
请注意,这是从抽象到具体类型的完全标准映射。 MS.DI自动了解Meal构造函数,并确定正确的操作步骤是解析所有ICourse组件。 解决IMeal时,您将获得带有ICourse组件Rillettes,CordonBleu和MousseAuChocolat的Meal实例。
MS.DI自动处理序列,除非您另外指定,否则它会执行您期望的操作:它将一系列依赖关系解析为该类型的所有已注册组件。 仅当您需要显式地从较大的集合中仅选择某些组件时,才需要执行更多操作。 让我们看看如何做到这一点。
仅从较大的集合中选择一些组件
MS.DI的注入所有组件的默认策略通常是正确的策略,但是如图15.4所示,在某些情况下,您可能只想从较大的所有已注册组件集中仅选择某些已注册组件。
图15.4从更大的所有已注册组件集中选择组件 |
---|
注 注入完整集合的子集的需求并不常见,但这确实说明了如何解决您可能遇到的一些更复杂的需求。
先前让MS.DI Auto-Wire自动配置所有已配置的实例时,它对应于图右侧所示的情况。 如果要按左侧所示注册组件,则必须明确定义应使用的组件。 为了实现此目的,可以使用接受委托的AddTransient方法。 这次,您正在处理Meal构造函数,该构造函数仅需要一个参数。
清单15.6将ICourse子集注入到Meal中
services.AddScoped<Rillettes>();
services.AddTransient<LobsterBisque>();
services.AddScoped<CordonBleu>();
services.AddScoped<OssoBuco>();
services.AddSingleton<MousseAuChocolat>();
services.AddTransient<CrèmeBrûlée>();
<------ 1-7 rows 按照具体类型而不是界面注册所有课程。 在这种情况下,将使用多种生活方式。
services.AddTransient<ICourse>(
c => c.GetRequiredService<Rillettes>());
services.AddTransient<ICourse(
c => c.GetRequiredService<LobsterBisque>());
services.AddTransient<ICourse>(
c => c.GetRequiredService<CordonBleu>());
services.AddTransient<ICourse(
c => c.GetRequiredService<OssoBuco>());
services.AddTransient<ICourse>(
c => c.GetRequiredService<MousseAuChocolat>());
services.AddTransient<ICourse(
c => c.GetRequiredService<CrèmeBrûlée>());
<--------- 通过其ICourse接口注册所有课程,该课程允许将每个课程解析为IEnumerable <ICourse>。 通过注册代表来防止生活方式破烂
services.AddTransient<IMeal>(c = new Meal(
new ICourse[]
{
c.GetRequiredService<Rillettes>(),
c.GetRequiredService<CordonBleu>(),
c.GetRequiredService<MousseAuChocolat>()
})); <-------- 根据具体类型解析三门特定课程,并将其注入到Meal构造函数中
MS.DI本机理解序列; 除非您只需要从给定类型的所有服务中明确选择某些组件,否则MS.DI会自动执行正确的操作。 自动装配不仅适用于单个实例,而且适用于序列,并且容器将序列映射到相应类型的所有已配置实例。 Decorators设计模式是使用多个具有相同Abstraction的实例的一种不太直观的用法,我们将在下面讨论。
接入装饰器
在第9.1.1节中,我们讨论了在实现“跨页面关注”时装饰器设计模式如何有用。 根据定义,装饰器会引入同一抽象的多种类型。 至少,您有两种抽象的实现:装饰器本身和装饰类型。 如果堆叠装饰器,则可以拥有更多。 这是具有相同服务的多个注册的另一个示例。 与前面的部分不同,这些注册在概念上不是相等的,而是彼此之间的依赖关系。
装饰非泛型抽象
MS.DI没有对Decorator的内置支持,这是MS.DI的局限性可能会影响生产率的领域之一。 尽管如此,我们将向您展示如何在一定程度上解决这些限制。
您可以再次通过使用ActivatorUtilities类来解决这一遗漏。 以下示例显示了如何使用此类将面包屑应用于VealCutlet:
services.AddTransient<IIngredient>(c => < ----- 注册一个代码块,该代码块调用CreateInstance来使用自动装配构造面包装饰器
ActivatorUtilities.CreateInstance<Breading>(
c,
ActivatorUtilities
.CreateInstance<VealCutlet>(c))); <---- 通过将VealCutlet实例提供给params数组,用VealCutlet注入Breading。 使用标准自动装配创建VealCutlet。
正如在第9章中所学到的那样,当您在小牛肉饼上切开一个口袋并在口袋里加火腿,奶酪和大蒜时,您会得到小牛肉蓝带。 以下示例显示了如何在VealCutlet和面包屑装饰器之间添加HamCheeseGarlic装饰器:
services.AddTransient<IIngredient>(c => ActivatorUtilities.CreateInstance<Breading>(
c,
ActivatorUtilities
.CreateInstance<HamCheeseGarlic>( <------ 添加一个新的装饰器
c,
ActivatorUtilities .CreateInstance<VealCutlet>(c))));
通过使HamCheeseGarlic成为面包屑的依赖关系,并使VealCutlet成为HamCheeseGarlic的依赖关系,HamCheeseGarlic装饰器将成为对象图中的中产阶级。 这将导致对象图等于以下Pure DI版本:
new Breading(
new HamCheeseGarlic(
new VealCutlet())); <---------VealCutlet由HamCheeseGarlic包装,Breading包装。
您可能会猜到,用MS.DI链接Decorators既麻烦又冗长。 让我们看看如果尝试将Decorators应用于通用Abstractes会发生什么,这会增加伤害的侮辱。
装饰泛型抽象
在第10章的过程中,我们定义了多个通用的Decorator,它们可以应用于任何ICommandService
清单15.7装饰通用的自动注册的抽象
Assembly assembly = typeof(AdjustInventoryService).Assembly;
var mappings =
from type in assembly.GetTypes()
where !type.IsAbstract
where !type.IsGenericType
from i in type.GetInterfaces()
where i.IsGenericType
where i.GetGenericTypeDefinition()
== typeof(ICommandService<>)
select new { service = i, implementation = type }; <---- ICommandService <TCommand>实现
foreach (var mapping in mappings)
{
Type commandType =
mapping.service.GetGenericArguments()[0]; <---- 从关闭的ICommandService <TCommand>抽象中提取具体的TCommand类型
Type secureDecoratoryType =
typeof(SecureCommandServiceDecorator<>)
.MakeGenericType(commandType);
Type transactionDecoratorType =
typeof(TransactionCommandServiceDecorator<>)
.MakeGenericType(commandType);
Type auditingDecoratorType =
typeof(AuditingCommandServiceDecorator<>)
.MakeGenericType(commandType);
<--------------- 17-25rows 使用提取的commandType构建需要应用的Decorators的封闭通用实现
services.AddTransient(mapping.service, c =>
ActivatorUtilities.CreateInstance(
c,
secureDecoratoryType,
ActivatorUtilities.CreateInstance(
c,
transactionDecoratorType,
ActivatorUtilities.CreateInstance(
c,
auditingDecoratorType,
ActivatorUtilities.CreateInstance(
c,
mapping.implementation)))));
} <---- 为关闭的ICommandService <TCommand>抽象添加委托注册。 该委托多次调用ActivatorUtilities的CreateInstance方法,以自动连接所有Decorators和扫描的实现作为最内部的组件。
清单15.7的配置结果如图15.5所示,我们之前在10.3.4节中进行了讨论。
图15.5通过事务,审计和安全性方面丰富真实的命令服务 |
---|
不幸的是,如果您认为清单15.7看起来相当复杂,这仅仅是开始。 该清单存在许多缺点,其中一些缺点很难解决。 其中包括:
- 当Decorator的任一泛型类型参数与Abstraction的泛型类型参数都不完全匹配时,创建封闭通用的Decorator类型可能会变得困难。
- 无法添加开放式的实现来应用装饰器,而又不必强制为每个封闭式的抽象进行显式注册,这是不可能的。
- 例如,基于泛型类型参数有条件地应用Decorator变得很复杂。
- 如果使用其他生活方式,那么在实现中实现多个接口的情况下,防止Torn Lifestyles变得很复杂。
- 很难区分生活方式。 链中的所有装饰器都具有相同的生活方式。
您可以尝试逐步克服这些限制并建议对清单15.7进行改进,但是您实际上应该在MS.DI之上开发一个新的DI容器(DI Container),因此我们不鼓励这样做。 这不会产生任何效果。 在这种情况下,更好的选择是更好的选择,例如Autofac和Simple Injector。
尽管依赖项序列的使用者可以最直观地使用同一抽象的多个实例,但装饰器是另一个很好的例子。 但是,在第三种情况下,也许会有一些令人惊讶的情况,其中有多个实例在起作用,这就是Composite设计模式。
接线复合材料
在本书的学习过程中,我们多次讨论了复合设计模式。 例如,在6.1.2节中,您创建了一个CompositeNotificationService(清单6.4),它既实现了INotificationService,又包装了一系列INotificationService实现。
接线非通用复合材料
让我们看一下如何注册Composites,例如CompositeNotification-MS.DI中第6章的服务。 以下清单再次显示了该类。
清单15.8第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要求将其添加为默认注册,同时向其注入一系列已解析的实例:
services.AddTransient<OrderApprovedReceiptSender>();
services.AddTransient<AccountingNotifier>();
services.AddTransient<OrderFulfillment>();
services.AddTransient<INotificationService>(c =>
new CompositeNotificationService(
new INotificationService[]
{
c.GetRequiredService<OrderApprovedReceiptSender>(),
c.GetRequiredService<AccountingNotifier>(),
c.GetRequiredService<OrderFulfillment>()
}
));
在此示例中,使用MS.DI的Auto-Wiring API按其具体类型注册了三个INotificationService实现。 另一方面,CompositeNotificationService是使用委托注册的。 在委托内部,对Composite进行了手动更新,并注入了一组INotificationService实例。 通过指定具体类型,可以解决以前进行的注册。
由于通知服务的数量可能会随着时间的推移而增加,因此您可以通过应用自动注册来减轻“组合根(Composition Root)”的负担。 如前所述,由于MS.DI在这方面缺少任何功能,因此您需要自己扫描程序集。
清单15.9注册CompositeNotificationService
Assembly assembly = typeof(OrderFulfillment).Assembly;
Type[] types = (
from type in assembly.GetTypes()
where !type.IsAbstract
where typeof(INotificationService).IsAssignableFrom(type)
select type) .ToArray(); <------ 将查询结果具体化为数组
foreach (Type type in types)
{
services.AddTransient(type);
}
services.AddTransient<INotificationService>(c =>
new CompositeNotificationService(
types.Select(t => (INotificationService)c.GetRequiredService(t)
) .ToArray()));
与清单15.7的Decorator示例相比,清单15.9看起来相当简单。 扫描程序集以获取INotificationService实现,并将每个找到的类型附加到服务集合。 类型数组由CompositeNotificationService注册使用。 Composite注入了一系列INotificationService实例,这些实例通过迭代类型数组来解决。
注 在清单15.9中,将产生的类型具体化为一个数组。 这样可以防止Composite注册表中的Select语句在每个解析上一次又一次地遍历OrderFulfillment的所有类型的程序集,如果程序集中的类型数量很大,则很容易耗尽应用程序的性能。
在处理MS.DI时,您可能已经习惯了所需的复杂性和详细程度,但是很遗憾,我们还没有完成。 我们的LINQ查询将注册任何实现INotificationService的非通用实现。 当您尝试运行前面的代码时,根据您的Composite所在的程序集,MS.DI可能会引发以下异常:
Exception of type 'System.StackOverflowException' was thrown.
哎哟! 堆栈溢出异常确实很痛苦,因为它们会中止正在运行的进程并且很难调试。 此外,此通用异常未提供有关导致堆栈溢出的原因的详细信息。 而是,您希望MS.DI像Autofac和Simple Injector一样抛出描述循环的描述性异常。
注 在撰写本章时,我们发现MS.DI引发的异常消息通常是通用的或令人困惑的,这使得故障排除问题比大多数其他流行的DI容器(DI Container)更难。 大多数成熟的DI容器(DI Container)都有非常清晰的异常消息
此堆栈溢出异常是由CompositeNotificationService中的循环依赖关系引起的。 LINQ查询将选择Composite,并将其作为序列的一部分进行解析。 这导致Composite依赖于自身。 这是MS.DI或与此相关的任何DI容器(DI Container)都无法构造的对象图。 CompositeNotificationService成为序列的一部分,因为我们的LINQ查询找到了所有非通用的INotificationService实现,其中包括Composite。
有多种解决方法。 最简单的解决方案是将“复合材料”移动到其他组件。 例如,包含“组合根(Composition Root)”的程序集。 这样可以防止LINQ查询选择类型。 另一个选择是从列表中过滤CompositeNotificationService:
Type[] types = (
from type in assembly.GetTypes()
where !type.IsAbstract
where typeof(INotificationService) .IsAssignableFrom(type)
where type != typeof(CompositeNotificationService) <----- 过滤组件
select type) .ToArray();
但是,复合类并不是唯一可能需要删除的类。 对于任何装饰器,您都必须执行相同的操作。 这并不是特别困难,但是由于通常会有更多的Decorator实现,因此最好查询类型信息以了解该类型是否表示Decorator。 您也可以按照以下方法过滤装饰器:
Type[] types = (
from type in assembly.GetTypes()
where !type.IsAbstract
where typeof(INotificationService).IsAssignableFrom(type)
where type != typeof(CompositeNotificationService)
where type => !IsDecoratorFor<INotificationService>(type)
select type) .ToArray();
下面的代码显示了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。
接线通用复合材料
在15.4.3节中,您了解了如何注册通用装饰器。 在本节中,我们将介绍如何为常规抽象注册Composites。
在6.1.3节中,您指定了CompositeEventHandler
我们发现,将事件处理程序实现隐藏在Composite后面的最简单方法是根本不注册那些实现,而将处理程序的构造移到Composite上。 这不是很漂亮,但是可以完成工作。 为了将处理程序隐藏在Composite之后,必须将清单6.12的CompositeEventHandler
清单15.10兼容MS.DI的CompositeEventHandler
实现
public class CompositeSettings
{
public Type[] AllHandlerTypes { get; set; }
}
public class CompositeEventHandler<TEvent>
: IEventHandler<TEvent>
{
private readonly IServiceProvider provider;
private readonly CompositeSettings settings;
public CompositeEventHandler( IServiceProvider provider, CompositeSettings settings)
{
this.provider = provider;
this.settings = settings;
}
public void Handle(TEvent e)
{
foreach (var handler in this.GetHandlers())
{
handler.Handle(e);
}
}
IEnumerable<IEventHandler<TEvent>> GetHandlers()
{
return from type in this.settings.AllHandlerTypes
where typeof(IEventHandler<TEvent>) .IsAssignableFrom(type) <---- 仅选择实现给定接口的类型。 如果您要调用CompositeEventHandler <OrderApproved>处理程序,则只会选择实现IEventHandler <OrderApproved>的那些类型。
select (IEventHandler<TEvent>) <----- 自动连接所选类型
ActivatorUtilities.CreateInstance( this.provider, type);}
}
与清单6.12的原始实现相比,此Composite实现更加复杂。 通过利用其IServiceProvider和ActivatorUtilities,它也对MS.DI本身具有严格的依赖性。 考虑到这种依赖性,此Composite肯定属于Composition Root,因为应用程序的其余部分应避免使用DI容器(DI Container)。
Composite依赖于包含所有处理程序类型的Parameter Object,而不是依赖于IEventHandler
清单15.11注册CompositeEventHandler
var handlerTypes =
from type in assembly.GetTypes()
where !type.IsAbstract
where !type.IsGenericType
let serviceTypes = type.GetInterfaces()
.Where(i => i.IsGenericType &&
i.GetGenericTypeDefinition()
== typeof(IEventHandler<>))
where serviceTypes.Any()
select type; <------- 扫描程序集以查找实现IEventHandler <TEvent>的所有具体的非泛型类
services.AddSingleton(new CompositeSettings
{
AllHandlerTypes = handlerTypes.ToArray()
}); <---- 注册参数对象,该对象允许将类型列表传递到Composite中
services.AddTransient(
typeof(IEventHandler<>),
typeof(CompositeEventHandler<>)); <---- 注册组件
与胖的Composite实现一起,最后一个清单有效地结合了MS.DI来实现组合模式。
提示 如果要将装饰器应用于单个事件处理程序,诀窍是将清单15.7的代码混合到清单15.10的GetHandlers
方法中。 换句话说,合成器负责创建对象图(包括装饰器)。
即使我们设法解决了MS.DI的某些限制,但在其他情况下,您可能会不太幸运。 例如,如果元素序列由非泛型和泛型实现组成,泛型实现包含泛型类型约束,或者当装饰器需要条件时,则可能运气不佳。
我们确实承认这是一个令人不愉快的解决方案。 我们更喜欢编写更少的代码来向您展示如何将MS.DI应用于本书中介绍的模式,但是不幸的是,并非全部都是桃子和奶油。 因此,在我们的日常开发工作中,我们更喜欢Pure DI或成熟的DI容器(DI Container)之一,例如Autofac和Simple Injector。
无论您选择哪种DI容器(DI Container),甚至您更喜欢Pure DI,我们都希望本书传达了一个重要观点-DI不依赖于特定的技术,例如特定的DI容器(DI Container)。 可以并且应该使用本书介绍的DI友好模式和实践来设计应用程序。 成功完成此操作后,选择DI容器(DI Container)的重要性就降低了。 DI容器(DI Container)是组成您的应用程序的工具,但理想情况下,您应该能够用另一个容器替换另一个容器,而无需重写应用程序除组成根之外的任何部分。
总结
- Microsoft.Extensions.DependencyInjection(MS.DI)DI容器(DI Container)具有一组有限的功能。 缺少用于自动注册,装饰器和组合的全面API。 这使得它不太适合按照本书介绍的原理和模式设计的应用程序开发。
- MS.DI强制在配置和使用容器之间严格隔离关注点。 您使用ServiceCollection实例配置组件,但是ServiceCollection无法解析组件。 完成ServiceCollection的配置后,就可以使用它来构建ServiceProvider,以用于解析组件。
- 使用MS.DI,直接从根容器进行解析是一种不好的做法。 这很容易导致内存泄漏或并发错误。 相反,您应该始终从IServiceScope进行解析。
- MS.DI支持三种标准的生活方式:Transient,Singleton和Scoped。