9 Registering services with dependency injection
9 使用依赖注入注册服务
This chapter covers
本章涵盖
-
Configuring your services to work with dependency injection
配置服务以使用依赖关系注入 -
Choosing the correct lifetime for your services
为您的服务选择正确的生命周期
In chapter 8 you learned about dependency injection (DI) in general, why it’s useful as a pattern for developing loosely coupled code, and its central place in ASP.NET Core. In this chapter you’ll build on that knowledge to apply DI to your own classes.
在第 8 章中,您了解了依赖项注入 (DI) 的一般知识,为什么它作为开发松散耦合代码的模式很有用,以及它在 ASP.NET Core 中的核心位置。在本章中,您将基于这些知识将 DI 应用于您自己的类。
You’ll start by learning how to configure your app so that the ASP.NET Core framework can create your classes for you, removing the pain of having to create new objects manually in your code. We look at the various patterns you can use to register your services and some of the limitations of the built-in DI container.
首先,您将学习如何配置您的应用程序,以便 ASP.NET Core 框架可以为您创建类,从而消除必须在代码中手动创建新对象的痛苦。我们来看看你可以用来注册服务的各种模式,以及内置 DI 容器的一些限制。
Next, you’ll learn how to handle multiple implementations of a service. You’ll learn how to inject multiple versions of a service, how to override a default service registration, and how to register a service conditionally if you don’t know whether it’s already registered.
接下来,您将学习如何处理服务的多个实现。您将学习如何注入服务的多个版本,如何覆盖默认服务注册,以及如何在不知道服务是否已注册时有条件地注册服务。
In section 9.4 we look at how you can control how long your objects are used—that is, their lifetime. We explore the differences among the three lifetime options and some of the pitfalls to be aware of when you come to write your own applications. Finally, in section 9.5 you’ll learn why lifetimes are important when resolving services outside the context of an HTTP request.
在 9.4 中,我们将了解如何控制对象的使用时间 — 即它们的生命周期。我们探讨了三种生命周期选项之间的差异,以及编写自己的应用程序时需要注意的一些陷阱。最后,在 9.5 节中,您将了解为什么在 HTTP 请求上下文之外解析服务时生命周期很重要。
We’ll start by revisiting the EmailSender service from chapter 8 to see how you could register the dependency graph in your DI container.
首先,我们将重新审视第 8 章中的 EmailSender 服务,了解如何在 DI 容器中注册依赖关系图。
9.1 Registering custom services with the DI container
9.1 向 DI 容器注册自定义服务
In this section you’ll learn how to register your own services with the DI container. We’ll explore the difference between a service and an implementation, and learn how to register the EmailSender hierarchy introduced in chapter 8.
在本节中,您将学习如何向 DI 容器注册自己的服务。我们将探讨服务和实现之间的区别,并学习如何注册第 8 章中介绍的 EmailSender 层次结构。
In chapter 8 I described a system for sending emails when a new user registers in your application. Initially, the minimal API endpoint handler RegisterUser created an instance of EmailSender manually, using code similar to the following listing (which you saw in chapter 8).
在第 8 章中,我描述了一个当新用户在您的应用程序中注册时发送电子邮件的系统。最初,最小 API 端点处理程序 RegisterUser 使用类似于以下清单的代码手动创建了一个 EmailSender 实例(您在第 8 章中看到)。
Listing 9.1 Creating an EmailSender instance without dependency injection
示例 9.1 创建无依赖注入的实例EmailSender
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/register/{username}", RegisterUser); ❶
app.Run();
string RegisterUser(string username)
{
IEmailSender emailSender = new EmailSender( ❷
new MessageFactory(), ❸
new NetworkClient( ❹
new EmailServerSettings ❺
( ❺
Host: "smtp.server.com", ❺
Port: 25 ❺
)) ❺
);
emailSender.SendEmail(username); ❻
return $"Email sent to {username}!";
}
❶ The endpoint is called when a new user is created.
创建新用户时调用 endpoint。
❷ To create EmailSender, you must create all its dependencies.
要创建 EmailSender,您必须创建其所有依赖项。
❸ You need a new MessageFactory.
您需要一个新的 MessageFactory。
❹ The NetworkClient also has dependencies.
NetworkClient 也有依赖项。
❺ You’re already two layers deep, but there could feasibly be more.
您已经有两层了,但可能还有更多。
❻ Finally, you can send the email.
最后,您可以发送电子邮件。
We subsequently refactored this code to inject an instance of IEmailSender into the handler instead, as shown in listing 9.2. The IEmailSender interface decouples the endpoint handler from the EmailSender implementation, making it easier to change the implementation of EmailSender (or replace it) without having to rewrite RegisterUser.
我们随后重构了这段代码,以注入一个IEmailSender 添加到处理程序中,如清单所示
9.2. IEmailSender 接口将端点处理程序与 EmailSender 实现解耦,从而更容易更改 mailSender 的实现(或替换它),而无需重写 RegisterUser。
Listing 9.2 Using IEmailSender with dependency injection in an endpoint handler
清单 9.2 使用带有依赖注入的 IEmailSender在端点处理程序中
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/register/{username}", RegisterUser); ❶
app.Run();
string RegisterUser(string username, IEmailSender emailSender) ❷
{
emailSender.SendEmail(username); ❸
return $"Email sent to {username}!";
}
❶ The endpoint is called when a new user is created.
创建新用户时调用 endpoint。
❷ The IEmailSender is injected into the handler using DI.
IEmailSender 使用 DI 注入处理程序。
❸ The handler uses the IEmailSender instance.
处理程序使用 IEmailSender 实例。
The final step in making the refactoring work is configuring your services with the DI container. This configuration lets the DI container know what to use when it needs to fulfill the IEmailSender dependency. If you don’t register your services, you’ll get an exception at runtime, like the one in figure 9.1. This exception describes a model-binding problem; the minimal API infrastructure tries to bind the emailSender parameter to the request body because IEmailSender isn’t a known service in the DI container.
进行重构的最后一步是使用 DI 容器配置您的服务。此配置使 DI 容器知道在需要满足 IEmailSender 依赖项时要使用什么。如果您不注册您的服务,您将在运行时收到异常,如图 9.1 中所示。此异常描述了模型绑定问题;最小 API 基础结构尝试将 emailSender 参数绑定到请求正文,因为 IEmailSender 不是 DI 容器中的已知服务。
Figure 9.1 If you don’t register all your required dependencies with the DI container, you’ll get an exception at runtime, telling you which service wasn’t registered.
图 9.1 如果你没有向 DI 容器注册所有需要的依赖项,你会在运行时收到一个异常,告诉你哪个服务没有注册。
To configure the application completely, you need to register an IEmailSender implementation and all its dependencies with the DI container, as shown in figure 9.2.
要完全配置应用程序,您需要向 DI 容器注册一个 IEmailSender 实现及其所有依赖项,如图 9.2 所示。
Figure 9.2 Configuring the DI container in your application involves telling it what type to use when a given service is requested, such as “Use EmailSender when IEmailSender is required.”
图 9.2 在应用程序中配置 DI 容器包括告诉它在请求给定服务时使用什么类型,例如“当需要 IEmailSender 时使用 EmailSender”。
Configuring DI consists of making a series of statements about the services in your app, such as the following:
配置 DI 包括对应用程序中的服务进行一系列声明,例如:
-
When a service requires IEmailSender, use an instance of EmailSender.
当服务需要 IEmailSender 时,请使用 EmailSender 的实例。 -
When a service requires NetworkClient, use an instance of NetworkClient.
当服务需要 NetworkClient 时,请使用 NetworkClient 的实例。 -
When a service requires MessageFactory, use an instance of MessageFactory.
当服务需要 MessageFactory 时,请使用 MessageFactory 的实例。
Note You’ll also need to register the EmailServerSettings object with the DI container. We’ll do that slightly differently in section 9.2.
注意 您还需要向 DI 容器注册 EmailServerSettings 对象。我们将在 9.2 节中略微不同地执行此作。
These statements are made by calling various Add methods on the IServiceCollection exposed as the WebApplicationBuilder.Services property. Each Add method provides three pieces of information to the DI container:
这些语句是通过对作为 WebApplicationBuilder.Services 属性公开的 IServiceCollection 调用各种 Add 方法来进行的。每个 Add 方法都向 DI 容器提供三条信息:
-
Service type—TService. This class or interface will be requested as a dependency. It’s often an interface, such as IEmailSender, but sometimes a concrete type, such as NetworkClient or MessageFactory.
服务类型 — TService。此类或接口将作为依赖项请求。它通常是一个接口,如 IEmailSender,但有时是一个具体类型,如 NetworkClient 或 MessageFactory。 -
Implementation type—TService or TImplementation. The container should create this class to fulfill the dependency. It must be a concrete type, such as EmailSender. It may be the same as the service type, as for NetworkClient and MessageFactory.
实现类型 - TService 或 TImplementation。容器应创建此类以满足依赖项。它必须是具体类型,例如 EmailSender。它可能与 NetworkClient 和 MessageFactory 的服务类型相同。 -
Lifetime—transient, singleton, or scoped. The lifetime defines how long an instance of the service should be used by the DI container. I discuss lifetimes in detail in section 9.4.
Definition
生存期 - 瞬态、单例或范围。生存期定义 DI 容器应使用服务实例的时间。我在 9.4 节中详细讨论了寿命。
DEFINITION A concrete type is a type that can be created, such as a standard class or struct. It contrasts with a type such as an interface or an abstract class, which can’t be created directly.
定义 具体类型是可以创建的类型,例如标准类或结构。它与 interface 或抽象类等类型形成对比,后者无法直接创建。
Listing 9.3 shows how you can configure EmailSender and its dependencies in your application by using three methods: AddScoped
清单 9.3 展示了如何使用三种方法在应用程序中配置 EmailSender 及其依赖项:AddScoped
Listing 9.3 Registering services with the DI container
清单 9.3 使用 DI 容器注册服务
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IEmailSender, EmailSender>(); ❶
builder.Services.AddScoped<NetworkClient>(); ❷
builder.Services.AddSingleton<MessageFactory>(); ❸
WebApplication app = builder.Build();
app.MapGet("/register/{username}", RegisterUser);
app.Run();
string RegisterUser(string username, IEmailSender emailSender)
{
emailSender.SendEmail(username);
return $"Email sent to {username}!";
}
❶ Whenever you require an IEmailSender, use EmailSender.
每当需要 IEmailSender 时,请使用 EmailSender。
❷ Whenever you require a NetworkClient, use NetworkClient.
每当您需要 NetworkClient 时,请使用 NetworkClient。
❸ Whenever you require a MessageFactory, use MessageFactory.
每当你需要 MessageFactory 时,请使用 MessageFactory。
That’s all there is to DI! It may seem a little bit like magic, but you’re simply giving the container instructions for making all the parts. You give it a recipe for cooking the chili, shred the lettuce, and grate the cheese, so when you ask for a burrito, it can put all the parts together and hand you your meal!
这就是 DI 的全部内容!这可能看起来有点像魔术,但您只是在给容器提供制作所有部件的说明。你给它一个烹饪辣椒、切碎生菜和磨碎奶酪的食谱,这样当你要墨西哥卷饼时,它可以把所有部分放在一起,然后把你的饭菜递给你!
Note Under the hood, the built-in ASP.NET Core DI container uses optimized reflection to create dependencies, but different DI containers may use other approaches. The Add APIs are the only way to register dependencies with the built-in container; there’s no support for using external configuration files to configure the container, for example.
注意 在后台,内置的 ASP.NET Core DI 容器使用优化的反射来创建依赖项,但不同的 DI 容器可能会使用其他方法。Add API 是向内置容器注册依赖项的唯一方法;例如,不支持使用外部配置文件来配置容器。
The service type and implementation type are the same for NetworkClient and MessageFactory, so there’s no need to specify the same type twice in the AddScoped method—hence, the slightly simpler signature.
NetworkClient 和 MessageFactory 的服务类型和实现类型相同,因此无需在 AddScoped 方法中两次指定相同的类型,因此签名稍微简单一些。
Note The EmailSender instance is registered only as an IEmailSender, so you can’t resolve it by requesting the specific EmailSender implementation; you must use the IEmailSender interface.
注意 EmailSender 实例仅注册为 IEmailSender,因此您无法通过请求特定的 EmailSender 实现来解析它;您必须使用 IEmailSender 接口。
These generic methods aren’t the only ways to register services with the container. You can also provide objects directly or by using lambdas, as you’ll see in section 9.2.
这些泛型方法并不是向容器注册服务的唯一方法。您也可以直接或使用 lambda 提供对象,如第 9.2 节所示。
9.2 Registering services using objects and lambdas
9.2 使用对象和 lambda 注册服务
As I mentioned in section 9.1, I didn’t quite register all the services required by EmailSender. In the previous examples, NetworkClient depends on EmailServerSettings, which you’ll also need to register with the DI container for your project to run without exceptions.
正如我在 9.1 节中提到的,我没有完全注册 EmailSender 所需的所有服务。在前面的示例中,NetworkClient 依赖于 EmailServerSettings,您还需要向 DI 容器注册它,以便您的项目能够无异常地运行。
I avoided registering this object in the preceding example because you have to take a slightly different approach. The preceding Add* methods use generics to specify the Type of the class to register, but they don’t give any indication of how to construct an instance of that type. Instead, the container makes several assumptions that you have to adhere to:
在前面的示例中,我避免注册此对象,因为您必须采用略有不同的方法。前面的 Add* 方法使用泛型指定要注册的类的 Type,但它们没有指示如何构造该类型的实例。相反,容器会做出几个您必须遵守的假设:
-
The class must be a concrete type.
该类必须是具体类型。 -
The class must have only a single relevant constructor that the container can use.
该类必须只有一个容器可以使用的相关构造函数。 -
For a constructor to be valid, all constructor arguments must be registered with the container or must be arguments with a default value.
要使构造函数相关,所有构造函数参数都必须注册到容器中,或者必须是具有默认值的参数。
Note These limitations apply to the simple built-in DI container. If you choose to use a third-party container in your app, it may have a different set of limitations.
注意 这些限制适用于简单的内置 DI 容器。如果您选择在应用程序中使用第三方容器,则它可能具有一组不同的限制。
The EmailServerSettings record doesn’t meet these requirements, as it requires you to provide a Host and Port in the constructor, which are a string and int, respectively, without default values:
EmailServerSettings 记录不满足这些要求,因为它要求您在构造函数中提供 Host 和 Port,它们分别是 string 和 int,没有默认值:
public record EmailServerSettings(string Host, int Port);
You can’t register these primitive types in the container. It would be weird to say “For every string constructor argument, in any type, use the "smtp.server.com" value.”
不能在容器中注册这些基元类型。如果说“对于任何类型的每个字符串构造函数参数,请使用 ”smtp.server.com“ 值,那就很奇怪了。
Instead, you can create an instance of the EmailServerSettings object yourself and provide that to the container, as shown in the following listing. The container uses the preconstructed object whenever an instance of the EmailServerSettings object is required.
相反,您可以自己创建 EmailServerSettings 对象的实例,并将其提供给容器,如下面的清单所示。每当需要 EmailServerSettings 对象的实例时,容器都会使用预构造的对象。
Listing 9.4 Providing an object instance when registering services
示例 9.4 在注册服务时提供对象实例
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IEmailSender, EmailSender>();
builder.Services.AddScoped<NetworkClient>();
builder.Services.AddSingleton<MessageFactory>();
builder.Services.AddSingleton(
new EmailServerSettings ❶
( ❶
Host: "smtp.server.com", ❶
Port: 25 ❶
)); ❶
WebApplication app = builder.Build();
app.MapGet("/register/{username}", RegisterUser);
app.Run();
❶ This instance of EmailServerSettings will be used whenever an instance is
required.
每当需要实例时,都会使用这个 EmailServerSettings 实例。
This code works fine if you want to have only a single instance of EmailServerSettings in your application; the same object will be shared everywhere. But what if you want to create a new object each time one is requested?
如果您只想在应用程序中只有一个 EmailServerSettings 实例,则此代码可以正常工作;同一对象将在任何地方共享。但是,如果您想在每次请求时都创建一个新对象,该怎么办?
Note When the same object is used whenever it’s requested, it’s known as a singleton. If you create an object and pass it to the container, it’s always registered as a singleton. You can also register any class using the AddSingleton
注意 当请求时使用相同的对象时,它称为单一实例。如果您创建一个对象并将其传递给容器,则它始终注册为单一实例。您还可以使用 AddSingleton
Instead of providing a single instance that the container will always use, you can provide a function that the container invokes when it needs an instance of the type, as shown in figure 9.3.
你可以提供一个函数,当容器需要该类型的实例时,你可以提供一个函数,而不是提供容器将始终使用的单个实例,如图 9.3 所示。
Figure 9.3 You can register a function with the DI container that will be invoked whenever a new instance of a service is required.
图 9.3 您可以在 DI 容器中注册一个函数,每当需要服务的新实例时,该函数将被调用。
Note Figure 9.3 is an example of the factory pattern, in which you define how a type is created. Note that the factory functions must be synchronous; you can’t create types asynchronously by (for example) using async.
注意 图 9.3 是工厂模式的一个示例,您可以在其中定义如何创建类型。请注意,工厂函数必须是同步的;您不能 (例如) 使用 async 异步创建类型。
The easiest way to register a service using the factory pattern is with a lambda function (an anonymous delegate), in which the container creates a new EmailServerSettings object whenever it’s needed, as shown in the following listing.
使用工厂模式注册服务的最简单方法是使用 lambda 函数(匿名委托),其中容器在需要时创建新的 EmailServerSettings 对象,如下面的清单所示。
Listing 9.5 Using a lambda factory function to register a dependency
清单 9.5 使用 lambda 工厂函数注册依赖项
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IEmailSender, EmailSender>();
builder.Services.AddScoped<NetworkClient>();
builder.Services.AddSingleton<MessageFactory>();
builder.Services.AddScoped( ❶
provider => ❷
new EmailServerSettings ❸
( ❸
Host: "smtp.server.com", ❸
Port: 25 ❸
)); ❸
WebApplication app = builder.Build();
app.MapGet("/register/{username}", RegisterUser);
app.Run();
❶ Because you’re providing a function to create the object, you aren’t restricted to a singleton.
因为你提供了一个函数来创建对象,所以你不限于单一实例。
❷ The lambda is provided an instance of IServiceProvider.
lambda 提供了 IServiceProvider 的实例。
❸ The constructor is called every time an EmailServerSettings object is required instead of only once.
每次需要 EmailServerSettings 对象时都会调用构造函数,而不仅仅是一次。
In this example, I changed the lifetime of the created EmailServerSettings object to scoped instead of singleton and provided a factory lambda function that returns a new EmailServerSettings object. Every time the container requires a new EmailServerSettings, it executes the function and uses the new object it returns.
在此示例中,我将创建的 EmailServerSettings 对象的生命周期更改为 scoped 而不是 singleton,并提供了一个返回新 EmailServerSettings 对象的工厂 lambda 函数。每次容器需要新的 EmailServerSettings 时,它都会执行该函数并使用它返回的新对象。
When you use a lambda to register your services, you’re given an IServiceProvider instance at runtime, called provider in listing 9.5. This instance is the public API of the DI container itself, which exposes the GetService
当您使用 lambda 注册服务时,在运行时会为您提供一个 IServiceProvider 实例,在列表 9.5 中称为 provider。此实例是 DI 容器本身的公共 API,它公开了 GetService
Tip Avoid calling GetService
提示 如果可能,请避免在工厂函数中调用 GetService
Open generics and dependency injection
开放泛型和依赖项注入
As already mentioned, you couldn’t use the generic registration methods with EmailServerSettings because it uses primitive dependencies (in this case, string and int) in its constructor. Neither can you use the generic registration methods to register open generics.
如前所述,您不能将泛型注册方法与 EmailServerSettings 一起使用,因为它在其构造函数中使用基元依赖项(在本例中为 string 和 int)。也不能使用泛型注册方法注册开放泛型。
Open generics are types that contain a generic type parameter, such as Repository. You normally use this sort of type to define a base behavior that you can use with multiple generic types. In the Repository example, you might inject IRepository into your services, which should inject an instance of DbRepository , for example.
开放泛型是包含泛型类型参数的类型,例如 Repository。通常使用这种类型来定义可与多个泛型类型一起使用的基本行为。在 Repository 示例中,您可以将 IRepository 注入到您的服务中,这应该会注入 DbRepository 的实例例如, 。
To register these types, you must use a different overload of the Add methods, as in this example:
要注册这些类型,必须使用 Add 的不同重载方法,如以下示例所示:
builder.Services.AddScoped(typeof(IRespository<>), typeof(DbRepository<>));
This code ensures that whenever a service constructor requires IRespository, the container injects an instance of DbRepository .
此代码可确保每当服务构造函数需要IRespository中,容器会注入 DbRepository 的实例。
At this point, all your dependencies are registered. But your Program.cs is starting to look a little messy, isn’t it? The choice is entirely down to personal preference, but I like to group my services into logical collections and create extension methods for them, as in listing 9.6. This approach creates an equivalent to the framework’s AddRazorPages() extension method—a nice, simple registration API. As you add more features to your app, I think you’ll appreciate it too.
此时,您的所有依赖项都已注册。但是你的Program.cs开始看起来有点凌乱了,不是吗?选择完全取决于个人喜好,但我喜欢将我的服务分组到逻辑集合中,并为它们创建扩展方法,如清单 9.6 所示。此方法创建与框架的 AddRazorPages() 扩展方法等效的 — 一个漂亮、简单的注册 API。随着您向应用程序添加更多功能,我想您也会喜欢它。
Listing 9.6 Creating an extension method to tidy up adding multiple services
清单 9.6 创建一个扩展方法来整理添加多个服务
public static class EmailSenderServiceCollectionExtensions
{
public static IServiceCollection AddEmailSender(
this IServiceCollection services) ❶
{
services.AddScoped<IEmailSender, EmailSender>(); ❷
services.AddSingleton<NetworkClient>(); ❷
services.AddScoped<MessageFactory>(); ❷
services.AddSingleton( ❷
new EmailServerSettings ❷
( ❷
host: "smtp.server.com", ❷
port: 25 ❷
)); ❷
return services; ❸
}
}
❶ Creates an extension method on IServiceCollection by using the “this” keyword
使用“this”关键字在 IServiceCollection 上创建扩展方法
❷ Cuts and pastes your registration code from Program.cs
从 Program.cs 剪切并粘贴您的注册码
❸ By convention, returns the IServiceCollection to allow method chaining
按照约定,返回 IServiceCollection 以允许方法链接
With the preceding extension method created, the following listing shows that your startup code is much easier to grok!
创建上述扩展方法后,以下清单显示您的启动代码更容易理解!
Listing 9.7 Using an extension method to register your services
清单 9.7 使用扩展方法注册您的服务
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddEmailSender(); ❶
WebApplication app = builder.Build();
app.MapGet("/register/{username}", RegisterUser);
app.Run();
❶ The extension method registers all the services associated with the
EmailSender.
扩展方法注册与 EmailSender 关联的所有服务。
So far, you’ve seen how to register the simple DI cases in which you have a single implementation of a service. In some scenarios, you may have multiple implementations of an interface. In section 9.3 you’ll see how to register these with the container to match your requirements.
到目前为止,您已经了解了如何注册简单的 DI 案例,其中您有一个服务的单个实现。在某些情况下,您可能有多个接口实现。在 Section 9.3 中,您将看到如何将这些注册到容器中以满足您的需求。
9.3 Registering a service in the container multiple times
9.3 在容器中多次注册服务
One advantage of coding to interfaces is that you can create multiple implementations of a service. Suppose that you want to create a more generalized version of IEmailSender so that you can send messages via Short Message Service (SMS) or Facebook, as well as by email. You create the interface for it as follows,
对接口进行编码的一个优点是,您可以创建服务的多个实现。假设您要创建更通用的 IEmailSender 版本,以便可以通过短消息服务 (SMS) 或 Facebook 以及电子邮件发送消息。您可以按如下方式为其创建接口:
public interface IMessageSender
{
public void SendMessage(string message);
}
as well as several implementations: EmailSender, SmsSender, and FacebookSender. But how do you register these implementations in the container? And how can you inject these implementations into your RegisterUser handler? The answers vary slightly, depending on whether you want to use all the implementations in your consumer or only one.
以及多种实现:EmailSender、SmsSender 和 FacebookSender。但是如何在容器中注册这些实现呢?如何将这些实现注入到 RegisterUser 处理程序中呢?答案略有不同,具体取决于您是要使用 Consumer 中的所有 implementations,还是只使用 one。
9.3.1 Injecting multiple implementations of an interface
9.3.1 注入接口的多个实现
Suppose that you want to send a message using each of the IMessageSender implementations whenever a new user registers so that they get an email, an SMS text, and a Facebook message, as shown in figure 9.4.
假设您希望在新用户注册时使用每个 IMessageSender 实现发送消息,以便他们收到电子邮件、SMS 文本和 Facebook 消息,如图 9.4 所示。
Figure 9.4 When a user registers with your application, they call the RegisterUser handler. This handler sends them an email, an SMS text, and a Facebook message using the IMessageSender classes.
图 9.4 当用户注册到您的应用程序时,他们会调用 RegisterUser 处理程序。此处理程序使用 IMessageSender 类向他们发送电子邮件、SMS 文本和 Facebook 消息。
The easiest way to achieve this goal is to register all the service implementations in your DI container and have it inject one of each type into the RegisterUser endpoint handler. Then RegisterUser can use a simple foreach loop to call SendMessage() on each implementation, as shown in figure 9.5.
实现此目标的最简单方法是在 DI 容器中注册所有服务实现,并让它将每种类型中的一个注入到 RegisterUser 端点处理程序中。然后 RegisterUser 可以使用一个简单的 foreach 循环在每个实现上调用 SendMessage(),如图 9.5 所示。
Figure 9.5 You can register multiple implementations of a service with the DI container, such as IEmailSender in this example. You can retrieve an instance of each of these implementations by requiring IEnumerable
图 9.5 你可以向 DI 容器注册服务的多个实现,例如本例中的 IEmailSender。您可以通过在 RegisterUser 处理程序中要求 IEnumerable
You register multiple implementations of the same service with a DI container in exactly the same way as for single implementations, using the Add* extension methods as in this example:
使用 Add 扩展方法,使用 Add 扩展方法,以与单个实现完全相同的方式向 DI 容器注册同一服务的多个实现,如下例所示:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMessageSender, EmailSender>();
builder.Services.AddScoped<IMessageSender, SmsSender>();
builder.Services.AddScoped<IMessageSender, FacebookSender>();
Then you can inject IEnumerable
然后你可以将 IEnumerable
Listing 9.8 Injecting multiple implementations of a service into an endpoint
清单 9.8 将服务的多个实现注入到端点中
string RegisterUser(
string username,
IEnumerable<IMessageSender> senders) ❶
{
foreach(var sender in senders) ❷
{ ❷
Sender.SendMessage($”Hello {username}!”); ❷
} ❷
return $"Welcome message sent to {username}";
}
❶ Requests an IEnumerable injects an array of IMessageSender
请求 IEnumerable 注入 IMessageSender 数组
❷ Each IMessageSender in the IEnumerable is a different implementation.
IEnumerable 中的每个 IMessageSender 都是不同的实现。
Warning You must use IEnumerable
警告 您必须使用 IEnumerable
It’s simple enough to inject all the registered implementations of a service, but what if you need only one? How does the container know which one to use?
注入服务的所有已注册实现非常简单,但如果你只需要一个呢?容器如何知道要使用哪一个?
9.3.2 Injecting a single implementation when multiple services are registered
9.3.2 在注册多个服务时注入单个实现
Suppose that you’ve already registered all the IMessageSender implementations. What happens if you have a service that requires only one of them? Consider this example:
假设您已经注册了所有 IMessageSender 实现。如果您的服务只需要其中一个,会发生什么情况?请考虑以下示例:
public class SingleMessageSender
{
private readonly IMessageSender _messageSender;
public SingleMessageSender(IMessageSender messageSender)
{
_messageSender = messageSender;
}
}
Of the three implementations available, the container needs to pick a single IMessageSender to inject into this service. It does this by using the last registered implementation: FacebookSender from the previous example.
在三种可用的实现中,容器需要选取一个 IMessageSender 以注入到此服务中。它通过使用上一个示例中的 FacebookSender 来实现此目的。
Note The DI container will use the last registered implementation of a service when resolving a single instance of the service.
注意 在解析服务的单个实例时,DI 容器将使用上次注册的服务实现。
This feature can be particularly useful for replacing built-in DI registrations with your own services. If you have a custom implementation of a service that you know is registered within a library’s Add* extension method, you can override that registration by registering your own implementation afterward. The DI container will use your implementation whenever a single instance of the service is requested.
此功能对于将内置 DI 注册替换为您自己的服务特别有用。如果您知道在库的 Add* 扩展方法中注册了服务的自定义实施,则可以通过在之后注册自己的实施来覆盖该注册。每当请求服务的单个实例时,DI 容器都会使用您的实现。
The main disadvantage of this approach is that you still end up with multiple implementations registered; you can inject an IEnumerable
这种方法的主要缺点是你最终仍然注册了多个 implementation;您可以像以前一样注入 IEnumerable
9.3.3 Conditionally registering services using TryAdd
9.3.3 使用 TryAdd 有条件地注册服务
Sometimes you want to add an implementation of a service only if one hasn’t already been added. This approach is particularly useful for library authors; they can create a default implementation of an interface and register it only if the user hasn’t already registered their own implementation.
有时,仅当尚未添加服务时,您才希望添加服务的实现。此方法对库作者特别有用;他们可以创建 interface 的默认 implementation ,并且只有在用户尚未注册自己的 implementation 时才能注册它。
You can find several extension methods for conditional registration in the Microsoft.Extensions.DependencyInjection.Extensions namespace, such as TryAddScoped. This method checks whether a service has been registered with the container before calling AddScoped on the implementation. Listing 9.9 shows how you can add SmsSender conditionally if there are no existing IMessageSender implementations. As you initially register EmailSender, the container ignores the SmsSender registration, so it isn’t available in your app.
您可以在 Microsoft.Extensions.DependencyInjection.Exte nsions 命名空间中找到多种用于条件注册的扩展方法,例如 TryAddScoped。此方法在对实现调用 AddScoped 之前,检查是否已向容器注册服务。清单 9.9 展示了如何有条件地添加 SmsSender如果没有现有的 IMessageSender 实现。当您最初注册 EmailSender 时,容器会忽略 SmsSender 注册,因此它在您的应用程序中不可用。
Listing 9.9 Conditionally adding a service using TryAddScoped
清单 9.9 使用 TryAdd 有条件地注册服务
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMessageSender, EmailSender>(); ❶
builder.Services.TryAddScoped<IMessageSender, SmsSender>(); ❷
❶ EmailSender is registered with the container.
EmailSender 已注册到容器中。
❷ There’s already an IMessageSender implementation, so SmsSender isn’t
registered.
已经有一个 IMessageSender 实现,因此 SmsSender 未注册。
Code like this doesn’t often make a lot of sense at the application level, but it can be useful if you’re building libraries for use in multiple apps. The ASP.NET Core framework, for example, uses TryAdd* in many places, which lets you easily register alternative implementations of internal components in your own application if you want.
像这样的代码在应用程序级别通常没有多大意义,但如果您正在构建用于多个应用程序的库,它可能很有用。例如,ASP.NET Core 框架在许多地方都使用 TryAdd*,它允许您根据需要轻松地在自己的应用程序中注册内部组件的替代实施。
You can also replace a previously registered implementation by using the Replace() extension method. Unfortunately, the API for this method isn’t as friendly as the TryAdd methods. To replace a previously registered IMessageSender with SmsSender, you’d use
您还可以使用 Replace() 扩展方法替换以前注册的实现。遗憾的是,此方法的 API 不如 TryAdd 方法友好。要将以前注册的 IMessageSender 替换为 SmsSender,请使用
builder.Services.Replace(new ServiceDescriptor(
typeof(IMessageSender), typeof(SmsSender), ServiceLifetime.Scoped
));
Tip When using Replace, you must provide the same lifetime that was used to register the service that’s being replaced.
提示 使用 Replace 时,您必须提供用于注册要替换的服务的相同生命周期。
We’ve pretty much covered registering dependencies but touched only vaguely on one important aspect: lifetimes. Understanding lifetimes is crucial in working with DI containers, so it’s important to pay close attention to them when registering your services with the container.
我们几乎已经介绍了注册依赖项,但只模糊地触及了一个重要的方面:生命周期。了解生命周期对于使用 DI 容器至关重要,因此在向容器注册服务时,请务必密切关注它们。
9.4 Understanding lifetimes: When are services created?
9.4 了解生命周期:何时创建服务?
Whenever the DI container is asked for a particular registered service, such as an instance of IMessageSender, it can do either of two things to fulfill the request:
每当向 DI 容器请求特定的已注册服务(如 IMessageSender 的实例)时,它都可以执行以下两项作之一来满足请求:
-
Create and return a new instance of the service
创建并返回服务的新实例。 -
Return an existing instance of the service
返回服务的现有实例。
The lifetime of a service controls the behavior of the DI container with respect to these two options. You define the lifetime of a service during DI service registration. The lifetime dictates when a DI container reuses an existing instance of the service to fulfill service dependencies and when it creates a new one.
服务的生存期控制 DI 容器相对于这两个选项的行为。您可以在 DI 服务注册期间定义服务的生命周期。生命周期规定了 DI 容器何时重用服务的现有实例来实现服务依赖项,以及何时创建新实例。
Definition The lifetime of a service is how long an instance of a service should live in a container before the container creates a new instance.
定义 服务的生命周期是指在容器创建新实例之前,服务实例应在容器中存在的时间。
It’s important to get your head around the implications for the different lifetimes used in ASP.NET Core, so this section looks at each lifetime option and when you should use it. In particular, you’ll see how the lifetime affects how often the DI container creates new objects. In section 9.4.4 I’ll show you an antipattern of lifetimes to watch out for, in which a short-lifetime dependency is captured by a long-lifetime dependency. This antipattern can cause some hard-to-debug issues, so it’s important to bear in mind when configuring your app.
了解 ASP.NET Core 中使用的不同生命周期的影响非常重要,因此本节将介绍每个生命周期选项以及何时应该使用它。特别是,您将看到生命周期如何影响 DI 容器创建新对象的频率。在 9.4.4 节中,我将向你展示一个需要注意的生命周期的反模式,其中短生命周期的依赖性被长生命周期的依赖性捕获。这种反模式可能会导致一些难以调试的问题,因此在配置应用程序时请务必记住。
In ASP.NET Core, you can specify one of three lifetimes when registering a service with the built-in container:
在 ASP.NET Core 中,您可以在使用内置容器注册服务时指定三个生命周期之一:
-
Transient—Every time a service is requested, a new instance is created. Potentially, you can have different instances of the same class within the same dependency graph.
Transient (瞬态) – 每次请求服务时,都会创建一个新实例。您可能会在同一依赖项关系图中拥有同一类的不同实例。 -
Scoped—Within a scope, all requests for a service give you the same object. For different scopes, you get different objects. In ASP.NET Core, each web request gets its own scope.
范围 - 在一个范围内,服务的所有请求都会为您提供相同的对象。对于不同的范围,你会得到不同的对象。在 ASP.NET Core 中,每个 Web 请求都有自己的范围。 -
Singleton—You always get the same instance of the service, regardless of scope.
单一实例 - 无论范围如何,您始终会获得相同的服务实例。
Note These concepts align well with most other DI containers, but the terminology may differ. If you’re familiar with a third-party DI container, be sure you understand how the lifetime concepts align with the built-in ASP.NET Core DI container.
注意 这些概念与大多数其他 DI 容器非常一致,但术语可能有所不同。如果您熟悉使用第三方 DI 容器时,请确保您了解生命周期概念如何与内置的 ASP.NET Core DI 容器保持一致。
To illustrate the behavior of each lifetime, I use a simple example in this section. Suppose that you have DataContext, which has a connection to a database, as shown in listing 9.10. It has a single property, RowCount, which represents the number of rows in the Users table of a database. For the purposes of this example, we emulate calling the database by setting the number of rows randomly when the DataContext object is created, so you always get the same value every time you call RowCount on a given DataContext instance. Different instances of DataContext return different RowCount values.
为了说明每个生命周期的行为,我在本节中使用了一个简单的示例。假设你有 DataContext,它与一个数据库有连接,如清单 9.10 所示。它有一个属性 RowCount,该属性表示数据库的 Users 表中的行数。对于此示例,我们通过在创建 DataContext 对象时随机设置行数来模拟调用数据库,因此每次在给定 DataContext 实例上调用 RowCount 时,您始终会获得相同的值。DataContext 的不同实例返回不同的 RowCount 值。
Listing 9.10 DataContext generating a random RowCount on creation
清单 9.10 在创建DataContext时生成一个 random RowCount
class DataContext
{
public int RowCount { get; } ❶
= Random.Shared.Next(1, 1_000_000_000); ❷
}
❶ The property is read-only, so it always returns the same value.
该属性是只读的,因此它始终返回相同的值。
❷ Generates a random number between 1 and 1,000,000,000
生成一个介于 1 和 1,000,000,000 之间的随机数
You also have a Repository class that has a dependency on the DataContext, as shown in the next listing. It also exposes a RowCount property, but this property delegates the call to its instance of DataContext. Whatever value DataContext was created with, the Repository displays the same value.
您还有一个依赖于 DataContext 的 Repository 类,如下一个清单所示。它还公开了一个 RowCount 属性,但此属性将调用委托给其 DataContext 实例。无论价值如何DataContext 时,Repository 显示相同的值。
Listing 9.11 Repository service that depends on an instance of DataContext
清单 9.11 依赖于DataContext 的实例
public class Repository
{
private readonly DataContext _dataContext; ❶
public Repository(DataContext dataContext) ❶
{ ❶
_dataContext = dataContext; ❶
} ❶
public int RowCount => _dataContext.RowCount; ❷
}
❶ An instance of DataContext is provided using DI.
DataContext 的实例是使用 DI 提供的。
❷ RowCount returns the same value as the current instance of DataContext.
RowCount 返回与 DataContext 的当前实例相同的值。
Finally, you have your endpoint handler, RowCounts, which takes a dependency on both Repository and on DataContext directly. When the minimal API infrastructure creates the arguments needed to call RowCounts, the DI container injects an instance of DataContext and an instance of Repository. To create Repository, it must create a second instance of DataContext. Over the course of two requests, four instances of DataContext will be required, as shown in figure 9.6.
最后,您有终端节点处理程序 RowCounts,它直接依赖于 Repository 和 DataContext。当最小的 API 基础设施创建调用 RowCounts 所需的参数时,DI 容器会注入一个 DataContext 实例和一个 Repository 实例。要创建 Repository,它必须创建 DataContext 的第二个实例。在两个请求的过程中,将需要 4 个 DataContext 实例,如图 9.6 所示。
Figure 9.6 The DI container uses two instances of DataContext for each request. Depending on the lifetime with which the DataContext type is registered, the container might create one, two, or four instances of DataContext.
图 9.6 DI 容器为每个请求使用两个 DataContext 实例。根据 DataContext 类型注册的生命周期,容器可能会创建一个、两个或四个 DataContext 实例。
The RowCounts handler retrieves the value of RowCount returned from both Repository and DataContext and then returns them as a string, similar to the code in listing 9.12. The sample code associated with this book also records and displays the values from previous requests so you can easily track how the values change with each request.
RowCounts 处理程序检索从 Repository 和 DataContext 返回的 RowCount 的值,然后将它们作为字符串返回,类似于清单中的代码9.12. 与本书关联的示例代码还记录并显示先前请求的值,因此您可以轻松跟踪每个请求的值如何变化。
Listing 9.12 The RowCounts handler depends on DataContext and Repository
清单 9.12 RowCounts 处理程序依赖于DataContext 和存储库
static string RowCounts( ❶
DataContext db, ❶
Repository repository) ❶
{
int dbCount = db.RowCount; ❷
int repositoryCount = repository.RowCount; ❷
return: $"DataContext: {dbCount}, Repository: {repositoryCount}"; ❸
}
❶ DataContext and Repository are created using DI.
DataContext 和 Repository 是使用 DI 创建的。
❷ When invoked, the page handler retrieves and records RowCount from both
dependencies.
调用时,页面处理程序会从两个依赖项中检索并记录 RowCount。
❸ The counts are returned in the response.
响应中返回计数。
The purpose of this example is to explore the relationships among the four DataContext instances, depending on the lifetimes you use to register the services with the container. I’m generating a random number in DataContext as a way of uniquely identifying a DataContext instance, but you can think of this example as being a point-in-time snapshot of, say, the number of users logged on to your site or the amount of stock in a warehouse.
此示例的目的是探索 4 个 DataContext 实例之间的关系,具体取决于您用于向容器注册服务的生命周期。我在 DataContext 中生成一个随机数,作为唯一标识 DataContext 实例的一种方式,但您可以将此示例视为登录到您站点的用户数量或仓库中库存量的时间点快照。
I’ll start with the shortest-lived lifetime (transient), move on to the common scoped lifetime, and then take a look at singletons. Finally, I’ll show an important trap you should be on the lookout for when registering services in your own apps.
我将从最短生存期 (transient) 开始,然后转到常见的作用域生存期,然后看一下单例。最后,我将展示一个重要的陷阱在您自己的应用程序中注册服务时要注意。
9.4.1 Transient: Everyone is unique
9.4.1 瞬态:每个人都是独一无二的
In the ASP.NET Core DI container, transient services are always created new whenever they’re needed to fulfill a dependency. You can register your services using the AddTransient extension methods:
在 ASP.NET Core DI 容器中,每当需要临时服务来实现依赖项时,它们总是会创建新的。您可以使用 AddTransient 扩展方法注册您的服务:
builder.Services.AddTransient<DataContext>();
builder.Services.AddTransient<Repository>();
When you register services this way, every time a dependency is required, the container creates a new one. This behavior of the container for transient services applies both between requests and within requests; the DataContext injected into the Repository will be a different instance from the one injected into the RowCounts handler.
当您以这种方式注册服务时,每次需要依赖项时,容器都会创建一个新依赖项。临时服务容器的这种行为适用于请求之间和请求内;注入 Repository 的 DataContext 将与注入 RowCounts 处理程序的实例不同。
Note Transient dependencies can result in different instances of the same type within a single dependency graph.
注意:瞬态依赖关系可能会导致单个依赖关系图中出现相同类型的不同实例。
Figure 9.7 shows the results you get from calling the API repeatedly when you use the transient lifetime for both services. You can see that every value is different, both within a request and between requests. Note that figure 9.7 was generated using the source code for this chapter, which is based on the listings in this chapter, but also displays the results from previous requests to make the behavior easier to observe.
图 9.7 显示了在对这两个服务使用瞬态生命周期时重复调用 API 所获得的结果。您可以看到,每个值都不同,无论是在请求中还是在请求之间。请注意,图 9.7 是使用本章的源代码生成的,该源代码基于本章中的清单,但也显示了来自先前请求的结果,以使行为更易于观察。
Figure 9.7 When registered using the transient lifetime, all DataContext objects are different, as you see by the fact that all the values are different within and between requests.
图 9.7 当使用瞬态生命周期注册时,所有 DataContext 对象都是不同的,正如您所看到的,请求内部和请求之间的所有值都不同。
Transient lifetimes can result in the creation of a lot of objects, so they make the most sense for lightweight services with little or no state. Using the transient lifetime is equivalent to calling new every time you need a new object, so bear that in mind when using it. You probably won’t use the transient lifetime often; the majority of your services will probably be scoped instead.
瞬态生命周期可能会导致创建大量对象,因此它们对于状态很少或没有状态的轻量级服务最有意义。使用 transient 生命周期相当于每次需要新对象时调用 new,因此在使用它时请记住这一点。您可能不会使用瞬态生存期通常是;您的大多数服务可能会改为限定范围。
9.4.2 Scoped: Let’s stick together
9.4.2 范围:让我们团结一致
The scoped lifetime states that a single instance of an object will be used within a given scope, but a different instance will be used between different scopes. In ASP.NET Core, a scope maps to a request, so within a single request, the container will use the same object to fulfill all dependencies.
作用域生命周期表示将在给定范围内使用对象的单个实例,但将在不同的作用域之间使用不同的实例。在 ASP.NET Core 中,范围映射到请求,因此在单个请求中,容器将使用相同的对象来满足所有依赖项。
In the row-count example, within a single request (a single scope) the same DataContext is used throughout the dependency graph. The DataContext injected into the Repository is the same instance as the one injected into the RowCounts handler.
在行计数示例中,在单个请求(单个范围)中,在整个依赖关系图中使用相同的 DataContext。注入 Repository 的 DataContext 与注入 RowCounts 处理程序的实例相同。
In the next request, you’re in a different scope, so the container creates a new instance of DataContext, as shown in figure 9.8. A different instance means a different RowCount for each request, as you can see. As before, figure 9.8 also shows the counts for previous requests.
在下一个请求中,您处于不同的范围内,因此容器会创建一个新的 DataContext 实例,如图 9.8 所示。如您所见,不同的实例意味着每个请求的 RowCount 不同。和以前一样,图 9.8 也显示了先前请求的计数。
Figure 9.8 Scoped dependencies use the same instance of DataContext within a single request but a new instance for a separate request. Consequently, the RowCounts are identical within a request.
图 9.8 作用域依赖项在单个请求中使用相同的 DataContext 实例,但对单独的请求使用新实例。因此,请求中的 RowCount是相同的。
You can register dependencies as scoped by using the AddScoped extension methods. In this example, I registered DataContext as scoped and left Repository as transient, but you’d get the same results in this case if both were scoped:
您可以使用 AddScoped 扩展方法将依赖项注册为 scoped。在此示例中,我将 DataContext 注册为范围,并将 Repository 保留为 transient,但在这种情况下,如果两者都是范围,您将得到相同的结果:
builder.Services.AddScoped<DataContext>();
Due to the nature of web requests, you’ll often find services registered as scoped dependencies in ASP.NET Core. Database contexts and authentication services are common examples of services that should be scoped to a request—anything that you want to share across your services within a single request but that needs to change between requests.
由于 Web 请求的性质,您经常会发现在 ASP.NET Core 中注册为范围依赖项的服务。数据库上下文和身份验证服务是应将范围限定为请求的服务的常见示例,请求是您希望在单个请求中跨服务共享但需要在请求之间更改的任何内容。
NOTE If your scoped or transient services implement IDisposable, the DI container automatically disposes them
when the scope ends.
注意 如果您的范围或临时服务实现 IDisposable,则 DI 容器会在范围结束时自动释放它们。
Generally speaking, you’ll find a lot of services registered using the scoped lifetime—especially anything that uses a database, anything that’s dependent on details of the HTTP request, or anything that uses a scoped service. But some services don’t need to change between requests, such as a service that calculates the area of a circle or returns the current time in different time zones. For these services, a singleton lifetime might be more appropriate.
一般来说,您会发现许多使用作用域生命周期注册的服务,尤其是任何使用数据库的服务、任何依赖于 HTTP 请求详细信息的服务,或者任何使用作用域服务的服务。但有些服务不需要在请求之间更改,例如计算圆的面积或返回不同时区的当前时间的服务。对于这些服务,单一实例生存期可能更合适。
9.4.3 Singleton: There can be only one
9.4.3 Singleton:只能有一个
The singleton is a pattern that came before DI; the DI container provides a robust and easy-to-use implementation of it. The singleton is conceptually simple: an instance of the service is created when it’s first needed (or during registration, as in section 9.2), and that’s it. You’ll always get the same instance injected into your services.
singleton 是 DI 之前的模式;DI 容器提供了强大且易于使用的实现。singleton 在概念上很简单:在第一次需要时(或在注册期间,如 9.2 节)创建服务的实例,仅此而已。您将始终将相同的实例注入到您的服务中。
The singleton pattern is particularly useful for objects that are expensive to create, contain data that must be shared across requests, or don’t hold state. The latter two points are important: any service registered as a singleton should be thread-safe.
对于创建成本高昂、包含必须在请求之间共享的数据或不保存状态的对象,单独模式特别有用。后两点很重要:任何注册为单一实例的服务都应该是线程安全的。
Warning Singleton services must be thread-safe in a web application, as they’ll typically be used by multiple threads during concurrent requests.
警告 单例服务在 Web 应用程序中必须是线程安全的,因为它们通常在并发请求期间由多个线程使用。
Let’s consider what using singletons means for the row-count example. We can update the registration of DataContext to be a singleton:
让我们考虑一下使用单例对行计数示例意味着什么。我们可以将 DataContext 的注册更新为单例:
builder.Services.AddSingleton<DataContext>();
Then we can call the RowCounts handler and observe the results in figure 9.9. We can see that every instance has returned the same value, indicating that the same instance of DataContext is used in every request, both when injected directly into the endpoint handler and when referenced transitively by Repository.
然后我们可以调用 RowCounts 处理程序并观察图 9.9 中的结果。我们可以看到每个实例都返回了相同的值,这表明每个请求都使用了相同的 DataContext 实例,无论是直接注入到端点处理程序中时,还是被 Repository 传递引用时。
Figure 9.9 Any service registered as a singleton always returns the same instance. Consequently, all the calls to the RowCounts handler return the same value, both within a request and between requests.
图 9.9 任何注册为单例的服务总是返回相同的实例。因此,对 RowCounts 处理程序的所有调用在请求内和请求之间都返回相同的值。
Singletons are convenient for objects that need to be shared or that are immutable and expensive to create. A caching service should be a singleton, as all requests need to share the service. It must be thread-safe, though. Similarly, you might register a settings object loaded from a remote server as a singleton if you load the settings once at startup and reuse them through the lifetime of your app.
单例对于需要共享或不可变且创建成本高昂的对象来说很方便。缓存服务应该是单一实例,因为所有请求都需要共享该服务。不过,它必须是线程安全的。同样,如果您在启动时加载一次设置,并在应用程序的生命周期中重复使用它们,则可以将从远程服务器加载的设置对象注册为单一实例。
On the face of it, choosing a lifetime for a service may not seem to be too tricky. But an important gotcha can come back to bite you in subtle ways, as you’ll see in section 9.4.4.
从表面上看,为服务选择生命周期似乎并不太棘手。但是一个重要的问题可能会以微妙的方式回来咬你,正如您将在 9.4.4 节中看到的那样。
9.4.4 Keeping an eye out for captive dependencies
9.4.4 密切关注捕获依赖项
Suppose that you’re configuring the lifetime for the DataContext and Repository examples. You think about the suggestions I’ve provided and decide on the following lifetimes:
假设您正在为 DataContext 和 Repository 示例配置生命周期。您考虑我提供的建议并决定以下生命周期:
-
DataContext—Scoped, as it should be shared for a single request
DataContext — 范围限定,因为它应该为单个请求共享 -
Repository—Singleton, as it has no state of its own and is thread-safe, so why not?
存储库 — 单例,因为它没有自己的状态并且是线程安全的,那么为什么不呢?
Warning This lifetime configuration is to explore a bug. Don’t use it in your code; if you do, you’ll experience a similar problem!
警告 此生命周期配置用于探索 bug。不要在代码中使用它;如果你这样做,你会遇到类似的问题!
Unfortunately, you’ve created a captive dependency because you’re injecting a scoped object, DataContext, into a singleton, Repository. As it’s a singleton, the same Repository instance is used throughout the lifetime of the app, so the DataContext that was injected into it will also hang around, even though a new one should be used with every request. Figure 9.10 shows this scenario, in which a new instance of DataContext is created for each scope but the instance inside Repository hangs around for the lifetime of the app.
遗憾的是,您创建了捕获依赖项,因为您正在将范围对象 DataContext 注入到单一实例 Repository 中。由于它是一个单例,因此在应用程序的整个生命周期中使用相同的 Repository 实例,因此注入其中的 DataContext 也将挂起,即使每个请求都应该使用一个新的 DataContext 也是如此。图 9.10 显示了这种情况,其中为每个范围创建了一个新的 DataContext 实例,但Repository 中的实例在应用程序的生命周期内挂起。
Listing 9.10 DataContext is registered as a scoped dependency, but Repository is a singleton. Even though you expect a new DataContext for every request, Repository captures the injected DataContext and causes it to be reused for the lifetime of the app.
图 9.10 DataContext 注册为范围依赖项,但 Repository 是单例。即使您希望每个请求都有一个新的 DataContext,Repository 也会捕获 注入的 DataContext,并使其在应用程序的生命周期内重复使用。
Captive dependencies can cause subtle bugs that are hard to root out, so you should always keep an eye out for them. These captive dependencies are relatively easy to introduce, so always think carefully when registering a singleton service.
捕获依赖项可能会导致难以根除的细微错误,因此您应该始终留意它们。这些捕获依赖项相对容易引入,因此在注册 singleton 服务时请始终仔细考虑。
Warning A service should use only dependencies that have a lifetime longer than or equal to the service’s lifetime. A service registered as a singleton can safely use only singleton dependencies. A service registered as scoped can safely use scoped or singleton dependencies. A transient service can use dependencies with any lifetime.
警告: 服务应仅使用生命周期长于或等于服务生命周期的依赖项。注册为单一实例的服务可以安全地仅使用单一实例依赖项。注册为 scoped 的服务可以安全地使用 scoped 或单一实例依赖项。临时服务可以使用任何生命周期的依赖项。
At this point, I should mention one glimmer of hope in this cautionary tale: ASP.NET Core automatically checks for these kinds of captive dependencies and throws an exception on application startup if it detects them, or on first use of a captive dependency, as shown in figure 9.11.
在这一点上,我应该在这个警示故事中提到一丝希望:ASP.NET Core 会自动检查这些类型的捕获依赖项,并在应用程序启动时或首次使用捕获依赖项时引发异常,如图 9.11 所示。
Figure 9.11 When ValidateScopes is enabled, the DI container throws an exception when it creates a service with a captive dependency. By default, this check is enabled only for development environments.
图 9.11 启用 ValidateScopes 后,DI 容器在创建具有捕获依赖项的服务时会引发异常。默认情况下,仅对开发环境启用此检查。
This scope validation check has a performance effect, so by default it’s enabled only when your app is running in a development environment, but it should help you catch most problems of this kind. You can enable or disable this check regardless of environment by configuring the ValidateScopes option on your WebApplicationBuilder in Program.cs by using the Host property, as shown in the following listing.
此范围验证检查会产生性能成本,因此默认情况下,仅当您的应用程序在开发环境中运行时,它才会启用,但它应该可以帮助您捕获大多数此类问题。无论环境如何,您都可以通过使用 Host 属性在 Program.cs WebApplicationBuilder 上配置 ValidateScopes 选项来启用或禁用此检查,如下面的清单所示。
Listing 9.13 Setting the ValidateScopes property to always validate scopes
Listing 9.13 设置ValidateScopes属性以始终验证范围
WebApplicationBuilder builder = WebApplication.CreateBuilder(args); ❶
builder.Host.UseDefaultServiceProvider(o => ❷
{
o.ValidateScopes = true; ❸
o.ValidateOnBuild = true; ❹
});
❶ The default builder sets ValidateScopes to validate only in development
environments.
默认生成器将 ValidateScopes 设置为仅在开发环境中验证。
❷ You can override the validation check with the UseDefaultServiceProvider
extension.
您可以使用 UseDefaultServiceProvider 扩展覆盖验证检查。
❸ Setting this to true will validate scopes in all environments, which has
performance implications.
将此项设置为 true 将验证所有环境中的作用域,这会影响性能。
❹ ValidateOnBuild checks that every registered service has all its dependencies registered.
ValidateOnBuild 检查每个已注册的服务是否都已注册其所有依赖项。
Listing 9.13 shows another setting you can enable, ValidateOnBuild, which goes one step further. When the setting is enabled, the DI container checks on application startup that it has dependencies registered for every service it needs to build. If it doesn’t, it throws an exception and shuts down the app, as shown in figure 9.12, letting you know about the misconfiguration. This setting also has a performance effect, so it’s enabled only in development environments by default, but it’s useful for pointing out any missed service registrations.
清单 9.13 显示了另一个你可以启用的设置,ValidateOnBuild,它更进一步。启用该设置后,DI 容器会在应用程序启动时检查它是否为需要构建的每个服务注册了依赖项。如果没有,它会抛出一个异常并关闭应用程序,如图 9.12 所示,让您知道配置错误。此设置也有性能成本,因此默认情况下仅在开发环境中启用,但它对于指出任何错过的服务注册非常有用。
Figure 9.12 When ValidateOnBuild is enabled, the DI container checks on app startup that it can create all the registered services. If it finds a service it can’t create, it throws an exception. By default, this check is enabled only for development environments.
图 9.12 启用 ValidateOnBuild 后,DI 容器会在应用程序启动时检查它是否可以创建所有已注册的服务。如果它找到一个服务,它就找不到create 时,它会引发异常。默认情况下,仅对开发环境启用此检查。
Warning Unfortunately, the container can’t catch everything. For a list of caveats and exceptions, see this post from my blog: http://mng.bz/QmwG.
警告 遗憾的是,容器无法捕获所有内容。有关注意事项和例外情况的列表,请参阅我博客中的这篇文章:http://mng.bz/QmwG。
We’ve almost covered everything about dependency injection now, and there’s only one more thing to consider: how to resolve scoped services on app startup in Program.cs.
我们现在几乎已经涵盖了有关依赖项注入的所有内容,现在只需要考虑一件事:如何在 Program.cs 中解析应用程序启动时的作用域服务。
9.5 Resolving scoped services outside a request
9.5 在请求之外解析分区服务
In chapter 8, I said that there are two main ways to resolve services from the DI container for minimal API applications:
在第 8 章中,我说过,对于最小的 API 应用程序,有两种主要方法可以从 DI 容器中解析服务:
-
Injecting services into an endpoint handler
将服务注入端点处理程序 -
Accessing the DI container directly in Program.cs
直接在 Program.cs 中访问 DI 容器
You’ve seen the first of those approaches several times now in this chapter. In chapter 8 you saw that you can access services in Program.cs by calling GetRequiredService
在本章中,您已经多次看到这些方法中的第一种。在第 8 章中,您看到您可以通过对 WebApplication.Services 调用 GetRequiredService
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
var settings = app.Services.GetRequiredService<EmailServerSettings>();
It’s important, however, that you resolve only singleton services this way. The IServiceProvider exposed as WebApplication.Services is the root DI container for your app. Services resolved this way live for the lifetime of your app, which is fine for singleton services but typically isn’t the behavior you want for scoped or transient services.
但是,请务必以这种方式仅解析单一实例服务。作为 WebApplication.Services 公开的 IServiceProvider 是应用的根 DI 容器。以这种方式解析的服务在应用程序的生命周期内有效,这对于单一实例服务来说很好,但通常不是您想要的作用域或瞬态服务的行为。
Warning Don’t resolve scoped or transient services directly from WebApplication.Services. This approach can lead to leaking of memory, as the objects are kept alive till the app exits and aren’t garbage-collected.
警告 不要直接从 WebApplication.Services 解析范围或暂时性服务。这种方法可能会导致内存泄漏,因为对象在应用程序退出之前保持活动状态,并且不会进行垃圾回收。
Instead, you should only resolve scoped and transient services from an active scope. A new scope is created automatically for every HTTP request, but when you’re resolving services from the DI container directly in Program.cs (or anywhere else that’s outside the context of an HTTP request), you need to create (and dispose of) a scope manually.
相反,您应该只从活动范围解析范围服务和暂时性服务。系统会自动为每个 HTTP 请求创建一个新范围,但是当您在 Program.cs 中(或 HTTP 请求上下文之外的任何其他位置)中直接从 DI 容器中解析服务时,您需要手动创建(和释放)范围。
You can create a new scope by calling CreateScope() or CreateAsyncScope() on IServiceProvider, which returns a disposable IServiceScope object, as shown in figure 9.13. IServiceScope also exposes an IServiceProvider property, but any services resolved from this provider are disposed of automatically when you dispose the IServiceScope, ensuring that all the resources held by the scoped and transient services are released correctly.
您可以通过对 IServiceProvider 调用 CreateScope() 或 CreateAsyncScope() 来创建新范围,这将返回一个可释放的 IServiceScope 对象,如图 9.13 所示。IServiceScope 还公开 IServiceProvider 属性,但在释放 IServiceScope 时,将自动释放从此提供程序解析的任何服务,从而确保正确释放作用域服务和暂时性服务所持有的所有资源。
Figure 9.13 To resolve scoped or transient services manually, you must create an IServiceScope object by calling CreateScope() on WebApplication.Services. Any scoped or transient services resolved from the DI container exposed as IServiceScope.ServiceProvider are disposed of automatically when you dispose of the IServiceScope object.
图 9.13 要手动解析作用域或临时服务,必须通过在 WebApplication.Services 上调用 CreateScope() 来创建 IServiceScope 对象。在释放 IServiceScope 对象时,将自动释放从公开为 IServiceScope.ServiceProvider 的 DI 容器解析的任何作用域或暂时性服务。
The following listing shows how you can resolve a scoped service in Program.cs using the pattern in figure 9.13. This pattern ensures that the scoped DataContext object is disposed of correctly before the call to app.Run().
下面的清单显示了如何使用图 9.13 中的模式在 Program.cs 中解析范围服务。这
pattern 确保在调用 app 之前正确处理作用域内的 DataContext 对象。
Listing 9.14 Resolving a scoped service using IServiceScope in Program.cs
清单 9.14 使用Program.cs 中的 IServiceScope
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<DataContext>(); ❶
WebApplication app = builder.Build();
await using (var scope = app.Services.CreateAsyncScope()) ❷
{
var dbContext = ❸
scope.ServiceProvider.GetRequiredService<DataContext>(); ❸
Console.WriteLine($"Retrieved scope: {dbContext.RowCount}");
} ❹
app.Run();
❶ DataContext is registered as scoped, so it shouldn’t be resolved directly from app.Services.
DataContext 已注册为 scoped,因此不应直接从 app 解析app.Services。
❷ Creates an IServiceScope
创建 IServiceScope
❸ Resolves the scoped service from the scoped container
从范围容器解析范围服务
❹ When the IServiceScope is disposed, all resolved services are also disposed.
释放 IServiceScope 时,也会释放所有已解析的服务。
This example uses the async form CreateAsyncScope() instead of CreateScope(), which you generally should favor whenever possible. CreateAsyncScope was introduced in .NET 6 to fix an edge case related to IAsyncDisposable (introduced in .NET Core 3.0). You can read more about this scenario on my blog at http://mng.bz/zXGB.
此示例使用异步形式 CreateAsyncScope() 而不是 CreateScope(),您通常应尽可能使用后者。CreateAsyncScope 是在 .NET 6 中引入的,用于修复与 IAsyncDisposable 相关的边缘情况(在 .NET Core 3.0 中引入)。您可以在我的博客 http://mng.bz/zXGB 上阅读有关此方案的更多信息。
With that, you’ve reached the end of this introduction to DI in ASP.NET Core. Now you know how to register your own services with the DI container, and ideally, you have a good understanding of the three service lifetimes used in .NET. DI appears everywhere in .NET, so it’s important to try to get your head around it.
至此,您已完成 ASP.NET Core 中的 DI 简介的结尾。现在您知道如何注册自己的服务,理想情况下,您对 .NET 中使用的三个服务生存期有很好的了解。DI 在 .NET 中无处不在,因此请务必尝试了解它。
In chapter 10 we’ll look at the ASP.NET Core configuration model. You’ll see how to load settings from a file at runtime, store sensitive settings safely, and make your application behave differently depending on which machine it’s running on. We’ll even use a bit of DI; it gets everywhere in ASP.NET Core!
在第 10 章中,我们将介绍 ASP.NET Core 配置模型。您将了解如何在运行时从文件加载设置,安全地存储敏感设置,以及使您的应用程序根据运行它的机器运行不同的行为。我们甚至会使用一点 DI;它在 ASP.NET Core 中无处不在!
9.6 Summary
9.6 总结
-
When registering your services, you describe three things: the service type, the implementation type, and the lifetime. The service type defines which class or interface will be requested as a dependency. The implementation type is the class the container should create to fulfill the dependency. The lifetime is how long an instance of the service should be used for.
在注册服务时,您需要描述三项内容:服务类型、实现类型和生命周期。服务类型定义将请求哪个类或接口作为依赖项。implementation type 是容器为实现依赖项而应创建的类。生存期是服务实例的使用时间。 -
You can register a service by using generic methods if the class is concrete and all its constructor arguments are registered with the container or have default values.
如果类是具体的,并且其所有构造函数参数都已注册到容器或具有默认值,则可以使用泛型方法注册服务。 -
You can provide an instance of a service during registration, which will register that instance as a singleton. This approach can be useful when you already have an instance of the service available.
您可以在注册期间提供服务的实例,该实例会将该实例注册为单一实例。当您已有可用的服务实例时,此方法可能很有用。 -
You can provide a lambda factory function that describes how to create an instance of a service with any lifetime you choose. You can take this approach when your services depend on other services that are accessible only when your application is running.
您可以提供一个 lambda 工厂函数,用于描述如何创建具有您选择的任何生命周期的服务实例。当您的服务依赖于仅在应用程序运行时才能访问的其他服务时,您可以采用此方法。 -
Avoid calling GetService() or GetRequiredService() in your factory functions if possible. Instead, favor constructor injection; it’s more performant and simpler to reason about.
如果可能,请避免在工厂函数中调用 GetService() 或 GetRequiredService()。 相反,支持构造函数注入;它的性能更高,推理也更简单。 -
You can register multiple implementations for a service. Then you can inject IEnumerable
to get access to all the implementations at runtime.
您可以为一个服务注册多个实现。然后,您可以注入 IEnumerable以在运行时访问所有实现。 -
If you inject a single instance of a multiple-registered service, the container injects the last implementation registered.
如果您注入多个注册服务的单个实例,则容器将注入最后一个注册的实现。 -
You can use the TryAdd extension methods to ensure that an implementation is registered only if no other implementation of the service has been registered. This approach can be useful for library authors to add default services while still allowing consumers to override the registered services.
您可以使用 TryAdd 扩展方法确保仅在未注册服务的其他实施时注册实施。这种方法对于库作者来说非常有用,可以添加默认服务,同时仍允许使用者覆盖已注册的服务。 -
You define the lifetime of a service during DI service registration to dictate when a DI container will reuse an existing instance of the service to fulfill service dependencies and when it will create a new one.
在 DI 服务注册期间定义服务的生存期,以指示 DI 容器何时重用服务的现有实例来满足服务依赖项,以及何时创建新实例。 -
A transient lifetime means that every time a service is requested, a new instance is created.
瞬态生存期意味着每次请求服务时,都会创建一个新实例。 -
A scoped lifetime means that within a scope, all requests for a service will give you the same object. For different scopes, you’ll get different objects. In ASP.NET Core, each web request gets its own scope.
作用域生命周期意味着在一个范围内,对服务的所有请求都将为你提供相同的对象。对于不同的范围,你将获得不同的对象。在 ASP.NET Core 中,每个 Web 请求都有自己的范围。 -
You’ll always get the same instance of a singleton service, regardless of scope.
无论范围如何,您都将始终获得单一实例服务的相同实例。 -
A service should use only dependencies with a lifetime longer than or equal to the lifetime of the service. By default, ASP.NET Core performs scope validation to check for errors like this one and throws an exception when it finds them, but this feature is enabled only in development environments, as it has a performance cost.
服务应仅使用生命周期长于或等于服务生命周期的依赖项。默认情况下,ASP.NET Core 执行范围验证以检查此类错误,并在找到错误时引发异常,但此功能仅在开发环境中启用,因为它会降低性能。 -
To access scoped services in Program.cs, you must first create an IServiceScope object by calling CreateScope() or CreateAsyncScope() on WebApplication.Services. You can resolve services from the IServiceScope.ServiceProvider property. When you dispose IServiceScope, any scoped or transient services resolved from the scope are also disposed.
若要访问 Program.cs 中的分区服务,必须首先通过在 WebApplication 上调用 CreateScope() 或 CreateAsyncScope() 来创建 IServiceScope 对象。服务。可以从 IServiceScope.ServiceProvider 属性解析服务。释放 IServiceScope 时,还会释放从该范围解析的任何作用域或暂时性服务。