Skip to content

.NET Core 实战 [No.335~346] 依赖注入与中间件

🏷️ 《.NET Core 实战》

服务

ASP.NET Core 项目中的 “服务”,指的是 用于扩展应用程序功能的一系列类型。在应用程序初始化期间,会把需要的服务类型实例添加到 ServiceCollection 集合中,这些添加到集合中的服务实例将通过 依赖注入 提供给其他代码使用(例如可以注入控制器的构造函数中、或者 StartupConfigure 方法中)。

StartupConfigureServices 方法中添加如下代码,可以查看已经注册到 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 项目的输出结果:

txt
服务类型: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 方法添加。此种服务在整个应用程序运行期间只创建一个实例,不管有多少次请求,也不管被请求访问多少次,此服务只实例化一次。

依赖注入

服务注册到容器之后,就可以在支持依赖注入的地方直接获取注入的实例,不再需要手动创建了。

比如之前的文章示例中用到的 Startup 类的 Configure 方法,默认模板生成的 Configure 方法的第二个参数 env 就是通过依赖注入获取的实例。

最常见的就是在控制器(Controller)的构造函数中使用依赖注入。Razor Web 页面绑定的 PageModel 的构造函数也支持依赖注入。在 Razor Web 页面中则可以通过 @inject 指令接受依赖注入的实例。

在启动过程(如 Main 方法或 StarupConfigure 方法)中需要临时访问服务类型时,可以调用 IServiceProvider 接口的 CreateScope 扩展方法创建一个基于临时作用域IServiceScope 对象。

这篇博客中有关作用域验证的地方就是使用的这种方法创建的临时作用域。

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. 基于约定的中间件类

    主要的约定在于类的方法,基于约定的中间件类必须包含 InvokeInvokeAsync 方法,输人参数为一个 HttpContext 对象,并返回 Task 对象。

    csharp
    public Task Invoke(HttpContext context);
    public Task InvokeAsync(HttpContext context);

    基于约定的中间件类可以通过构造函数的依赖注入来获取下一个中间件的 InvokeInvokeAsync 方法引用。一般来说,中间件类的命名可以的带上 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>();
    
        // ...
    }

在每个中间件的实现代码中都会通过输入参数获得下一个中间件的引用,这样开发人员可以灵活控制:是先执行当前中间件的代码,还是先执行下一个中间件的代码,或者不执行下一个中间件而直接向客户端回写响应消息。

直接调用 IApplicationBuilderRun 扩展方法会使整个 HTTP 请求管道发生 短路 -- 直接把响应消息发回给客户端,终止此次 HTTP 通信。

下面示例代码中的第二个 Run 方法不会被调用。

csharp
app.Run(async (context) =>
{
    await context.Response.WriteAsync("Hello World!");
});

app.Run(async (context) =>
{
    await context.Response.WriteAsync("这里不会被打印!");
});

添加到 HTTP 管道的中间件是默认响应根 URL 请求/)的,若要根据子路径来调用不同的中间件,可以通过调用 IApplicationBuilderMap 扩展方法实现。

csharp
app.Map("home", _app =>
{
    _app.UseMiddleware<TestMiddleware>();

    _app.Run(async (context) =>
    {
        await context.Response.WriteAsync("主页");
    });
});

参考:《.NET Core 实战:手把手教你掌握 380 个精彩案例》 -- 周家安 著