佳佳的博客
Menu
首页
《.NET Core 实战》 [No.335~346] 依赖注入与中间件
Posted by
佳佳
on 2020-04-14
IT
C#
.NET Core
《.NET Core 实战》
读书笔记
<!-- # 《.NET Core 实战》 [No.335~346] 依赖注入与中间件 --> <!-- dotnet-core-dependency-injection-and-middleware --> ## 服务 ASP.NET Core 项目中的 **“服务”**,指的是 **用于扩展应用程序功能的一系列类型**。在应用程序初始化期间,会把需要的服务类型实例添加到 `ServiceCollection` 集合中,这些添加到集合中的服务实例将通过 **依赖注入** 提供给其他代码使用(例如可以注入控制器的构造函数中、或者 `Startup` 的 *Configure* 方法中)。 在 `Startup` 的 *ConfigureServices* 方法中添加如下代码,可以查看已经注册到 `ServiceCollection` 集合中的服务。 ```csharp public void ConfigureServices(IServiceCollection services) { foreach (var srv in services) { Console.WriteLine($"服务类型:{srv.ServiceType?.Name ?? "<无>"},实现类型:{srv.ImplementationType?.Name ?? "<无>"}"); } } ``` 下面是一个空的 ASP.NET Core 2.1 Web 项目的输出结果: ```csharp 服务类型:WebHostOptions,实现类型:<无> 服务类型:IHostingEnvironment,实现类型:<无> 服务类型:IHostingEnvironment,实现类型:<无> 服务类型:WebHostBuilderContext,实现类型:<无> 服务类型:IConfiguration,实现类型:<无> 服务类型:IApplicationBuilderFactory,实现类型:ApplicationBuilderFactory 服务类型:IHttpContextFactory,实现类型:HttpContextFactory 服务类型:IMiddlewareFactory,实现类型:MiddlewareFactory 服务类型:IOptions`1,实现类型:OptionsManager`1 服务类型:IOptionsSnapshot`1,实现类型:OptionsManager`1 服务类型:IOptionsMonitor`1,实现类型:OptionsMonitor`1 服务类型:IOptionsFactory`1,实现类型:OptionsFactory`1 服务类型:IOptionsMonitorCache`1,实现类型:OptionsCache`1 服务类型:ILoggerFactory,实现类型:LoggerFactory 服务类型:ILogger`1,实现类型:Logger`1 服务类型:IConfigureOptions`1,实现类型:<无> 服务类型:IStartupFilter,实现类型:AutoRequestServicesStartupFilter 服务类型:ObjectPoolProvider,实现类型:DefaultObjectPoolProvider 服务类型:ITransportFactory,实现类型:SocketTransportFactory 服务类型:IConfigureOptions`1,实现类型:KestrelServerOptionsSetup 服务类型:IServer,实现类型:KestrelServer 服务类型:IConfigureOptions`1,实现类型:<无> 服务类型:ILoggerProviderConfigurationFactory,实现类型:LoggerProviderConfigurationFactory 服务类型:ILoggerProviderConfiguration`1,实现类型:LoggerProviderConfiguration`1 服务类型:IConfigureOptions`1,实现类型:<无> 服务类型:IOptionsChangeTokenSource`1,实现类型:<无> 服务类型:LoggingConfiguration,实现类型:<无> 服务类型:ILoggerProvider,实现类型:ConsoleLoggerProvider 服务类型:IConfigureOptions`1,实现类型:ConsoleLoggerOptionsSetup 服务类型:IOptionsChangeTokenSource`1,实现类型:LoggerProviderOptionsChangeTokenSource`2 服务类型:ILoggerProvider,实现类型:DebugLoggerProvider 服务类型:IPostConfigureOptions`1,实现类型:<无> 服务类型:IOptionsChangeTokenSource`1,实现类型:<无> 服务类型:IStartupFilter,实现类型:HostFilteringStartupFilter 服务类型:IServiceProviderFactory`1,实现类型:<无> 服务类型:IStartup,实现类型:<无> 服务类型:DiagnosticListener,实现类型:<无> 服务类型:DiagnosticSource,实现类型:<无> 服务类型:IApplicationLifetime,实现类型:ApplicationLifetime 服务类型:IApplicationLifetime,实现类型:<无> 服务类型:HostedServiceExecutor,实现类型:HostedServiceExecutor ``` 编写服务类型有三种方案: 1. 先定义一个接口,然后定义一个类去实现这个接口,再将这个服务接口以及接口的实现类一起添加到`IServiceCollection` 集合中; 2. 先定义抽象类,然后定义一个实现该抽象类的新类,再将这个抽象类与它的实现类一起添加到 `IServiceCollection` 集合中; 3. 不定义接口类型,而是直接定义一个类来实现服务功能,然后把这个类添加到 `IServiceCollection` 集合中。 服务类型添加到 `ServiceCollection` 容器后,其**生命周期**将由框架自动管理。 容器中的服务存在三种生命周期: 1. **暂时服务**:通过调用 *AddTransient* 方法添加。暂时服务的生命周期是最短的,它会在每次被请求使用时都实例化一次,属于轻量级服务。就算是在同一个请求中多次访问,暂时服务每次都会进行实例化。 2. **作用域服务**:这个“作用域”的范围是**单个请求**,通过 *AddScoped* 方法添加。也就是说在单个请求中(从客户端向服务器发出请求到服务器回发响应消息的整个过程),不管被请求访问多少次,作用域服务都只进行一次实例化。 3. **单实例服务**:通过 *AddSingleton* 方法添加。此种服务在整个应用程序运行期间只创建一个实例,不管有多少次请求,也不管被请求访问多少次,此服务只实例化一次。 ## 依赖注入 服务注册到容器之后,就可以在支持依赖注入的地方直接获取注入的实例,不再需要手动创建了。 比如[之前的文章示例][1]中用到的 `Startup` 类的 *Configure* 方法,默认模板生成的 *Configure* 方法的第二个参数 *env* 就是通过依赖注入获取的实例。 最常见的就是在控制器(*Controller*)的构造函数中使用依赖注入。Razor Web 页面绑定的 `PageModel` 的构造函数也支持依赖注入。在 Razor Web 页面中则可以通过 `@inject` 指令接受依赖注入的实例。 在启动过程(如 *Main* 方法或 `Starup` 的 *Configure* 方法)中需要临时访问服务类型时,可以调用 `IServiceProvider` 接口的 *CreateScope* 扩展方法创建一个基于**临时作用域**的 `IServiceScope` 对象。 在[这篇博客][2]中有关作用域验证的地方就是使用的这种方法创建的临时作用域。 ```csharp // Create a new IServiceScope that can be used to resolve scoped services. using (var scope = app.ApplicationServices.CreateScope()) { // resolve the services within this scope ConcreteA A = scope.ServiceProvider.GetRequiredService<ConcreteA>(); //ConcreteA instance and injected ConcreteB are used in the same scope //do something A.Run(); } //both will be properly disposed of here when they both got out of scope. ``` ## 中间件 应用程序对 HTTP 请求的处理过程进行划分,每个环节成为**中间件**,将每个中间件串联起来,就形成了 **HTTP 管道**。 **执行中间件的循序与它们添加到 HTTP 管道的顺序相同。** 在HTTP管道中添加中间件有三种方法: 1. **委托** 中间件专用的委托类型为 `RequestDelegate` 。 ```csharp public delegate Task RequestDelegate(HttpContext context); ``` 一般来说,委托方式适用于代码量较少、处理逻辑比较简单的中间件。 ```csharp app.Use(async (context, next) => { // do something await next(); }); ``` 2. **基于约定的中间件类** 主要的约定在于类的方法,基于约定的中间件类必须包含 *Invoke* 或 *InvokeAsync* 方法,输人参数为一个 `HttpContext` 对象,并返回 `Task` 对象。 ```csharp public Task Invoke(HttpContext context); public Task InvokeAsync(HttpContext context); ``` 基于约定的中间件类可以通过**构造函数的依赖注入**来获取下一个中间件的 *Invoke* 或 *InvokeAsync* 方法引用。一般来说,中间件类的命名可以的带上 *Middleware* 后缀,以方便识别。 ```csharp public class MyMiddleware { // 下一个中间件的引用 private RequestDelegate _next; public MyMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { Console.WriteLine($"{GetType().Name} was called."); // 调用下一个中间件 await _next(context); } } ``` 在 `Startup` 类的 *Configure* 方法中,通过定义 *UseMiddleware* 扩展方法将自定义的中间件添加到 HTTP 管道中。 ```csharp app.UseMiddleware<MyMiddleware>(); ``` 基于约定的中间件**支持自定义参数**,但并不是调用参数,而且仅能在注册中间件时使用,即**在中间件的生命周期内,参数只传递一次**。 中间件的参数是 **通过构造函数传递** 的,即在定义中间件类的构造函数时,第一个参数是 HTTP 管道中下一个中间件的引用(`RequestDelegate` 委托),**从第二个参数开始可以定义中间件的参数**。 ```csharp public CalcMiddleware(RequestDelegate next, int a, int b) { _next = next; _a = a; _b = b; } ``` *UseMiddleware* 扩展方法的最后一个参数 *params object[] args* 即是用来接收自定义参数的。 ```csharp app.UseMiddleware<CalcMiddleware>(1, 18); ``` 3. **实现 `IMiddleware` 接口** 该接口同样包含 *InvokeAsync* 方法,需要 `HttpContext` 对象作为输入参数并返回 `Task` 类型的对象。 ```csharp Task InvokeAsync(HttpContext context, RequestDelegate next); ``` ```csharp public class TestMiddleware : IMiddleware { public TestMiddleware() { Console.WriteLine($"类 {GetType().Name} 的构造函数被调用。"); } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { // do something await next(context); } } ``` 用这种方式定义的中间件**需要在代码中显式将其添加到服务容器中**,因此此种中间件的**生命周期可以被改变**(前面两种方式所定义的中间件都是单实例模式,在应用程序生命周期内仅创建一次实例,而实现了 `IMiddleware` 接口的中间件在添加到服务容器时可以手动设置它的生命周期)。 ```csharp public void ConfigureServices(IServiceCollection services) { services.AddScoped<TestMiddleware>(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseMiddleware<TestMiddleware>(); // ... } ``` 在每个中间件的实现代码中都会通过输入参数获得下一个中间件的引用,这样开发人员可以灵活控制:是先执行当前中间件的代码,还是先执行下一个中间件的代码,或者不执行下一个中间件而直接向客户端回写响应消息。 直接调用 `IApplicationBuilder` 的 *Run* 扩展方法会使整个 HTTP 请求管道发生 **短路** -- 直接把响应消息发回给客户端,终止此次 HTTP 通信。 下面示例代码中的第二个 *Run* 方法不会被调用。 ```csharp app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); app.Run(async (context) => { await context.Response.WriteAsync("这里不会被打印!"); }); ``` 添加到 HTTP 管道的中间件是**默认响应根 URL 请求**(`/`)的,若要根据子路径来调用不同的中间件,可以通过调用 `IApplicationBuilder` 的 *Map* 扩展方法实现。 ```csharp app.Map("home", _app => { _app.UseMiddleware<TestMiddleware>(); _app.Run(async (context) => { await context.Response.WriteAsync("主页"); }); }); ``` <!-- 链接 --> [1]: /2020/4/8/dotnet-core-startup (《.NET Core 实战》 [No.330~332] Startup) [2]: /2020/4/7/dotnet-core-web-host-config (《.NET Core 实战》 [No.326~329] Web 主机配置) --- > 购买本书 => [《.NET Core实战:手把手教你掌握380个精彩案例》][10] -- *周家安* 著 --- [10]:https://union-click.jd.com/jdc?e=&p=AyIGZRhaEwAQBFUZXBIyEgRSEl0QCxc3EUQDS10iXhBeGlcJDBkNXg9JHU4YDk5ER1xOGRNLGEEcVV8BXURFUFdfC0RVU1JRUy1OVxUBFQ5THlIQMm1AEkRdb11GZyNTK0BBZwYIbylWcHILWStaJQITBlYbXB0LFQJlK1sSMkBpja3tzaejG4Gx1MCKhTdUK1sRCxQBVxtTEQIQBlwrXBULIloNXwZBXUReEStrJQEiN2UbaxYyUGlUG1kUBhcGUBILQgUXDlMeUkBVRlUBS10XBkIABhkJRzIQBlQfUg%3D%3D (《.NET Core实战:手把手教你掌握380个精彩案例》)
版权声明:原创文章,未经允许不得转载。
https://www.liujiajia.me/2020/4/14/dotnet-core-dependency-injection-and-middleware
“Buy me a nongfu spring”
« 《重构》 1. 重构,第一个示例
《.NET Core 实战》 [No.333~334] 启动环境 »
昵称
*
电子邮箱
*
回复内容
*
(回复审核后才会显示)
提交
目录
AUTHOR
刘佳佳
江苏 - 苏州
软件工程师