Skip to content
On this page

依赖注入

基于IHostBuilder/IHost的服务承载系统建立在依赖注入框架之上,依赖注入是.Net的基础编程框架,在前面章节我们详细介绍了依赖注入,接下来我们来探讨一下依赖注入在管道中的工作过程。

1. 服务注册

ASP.Net 应用提供了两种服务注册方式,一种是调用IWebHostBuilder接口的ConfigureServices方法,另一种则是利用注册的Startup类型来完成服务的注册。

ASP.Net 应用针对请求的处理能力与方式完全取决于注册的中间件,所以针对应用程序的初始化主要体现在针对中间件的注册上。对于注册的中间件来说,它往往具有针对其它服务的依赖。中间件依赖的这些服务自然需要被预先注册,所以中间件和服务注册成为Startup对象的两个核心功能。

csharp
public class Startup
{
    public void ConfigureService(IServiceCollection services);
    public void Configure(IApplicationBuilder app);
}

与中间件类型类似,我们在大部分情况下会采用约定的形式来定义Startup类型。中间件和服务的注册分别实现在Configure方法和ConfigureServices方法中。由于并不是在任何情况下都有服务注册的需求,所以ConfigureServices方法并不是必需的。Startup对象的 ConfigureServices方法的调用发生在整个服务注册的最后阶段,在此之后,ASP.Net应用就会利用所有的服务注册来创建作为依赖注入容器的IServiceProvider对象。

ASP.Net 框架本身在构建请求处理管道之前也会注册一些服务,这些公共服务除了供框架自身消费,也可以供应用程序使用。如IHostEnvironment/IConfiguration/IApplicationLifeTime/IOptions<TOptions>/ILogger<TCategoryName>等。

2. 服务消费

2.1 Startup

Startup除了支持支持构造函数注入,还可以在其Configure方法中使用方法注入。

csharp
public static void Main(string[] args)
{
    Host.CreateDefaultBuilder()
        .ConfigureServices(services => services.AddSingleton<IFoo, Foo>())
        .ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>())
        .Build()
        .Run();
}

public class Startup
{
    public Startup(IFoo foo) =>
        Debug.Assert(foo != null);

    public void ConfigureServices(IServiceCollection services) =>
        services.AddSingleton<IBar, Bar>();

    public void Config(IApplicationBuilder app, IBar bar) =>
        Debug.Assert(bar != null);
}

2.2 中间件

ASP.Net 在创建中间件对象并利用它们构建整个请求处理管道时,所有的服务都已经注册完毕,所以注册的任何一个服务都可以注入中间件类型的构造函数中。中间件的InvokeAsync也支持方法注入。

对于基于约定的中间件,构造函数注入与方法注入存在一个本质区别。基于约定的中间件会被注册为一个Singleton对象,所以我们不应该在它的构造函数中注入Scoped服务。Scoped服务只能注入中间件类型的InvokeAsync方法中,因为依赖服务是在针对当前请求的服务范围中提供的,所以能够确保Scoped服务在当前请求处理结束之后被释放。

csharp
public static void Main(string[] args)
{
    Host.CreateDefaultBuilder()
        .ConfigureServices(services => services
            .AddSingleton<IFoo, Foo>()
            .AddScoped<IBar, Bar>())
        .ConfigureWebHostDefaults(builder =>
            builder.Configure(app => app.UseMiddleware<HelloMiddleware>(false)))
        .Build()
        .Run();
}

public sealed class HelloMiddleware
{
    private readonly RequestDelegate _next;
    private readonly bool _foreward2Next;

    // 基于约定的中间件 构造函数只能注入 单例服务
    public HelloMiddleware(RequestDelegate next, IFoo foo, bool foreward2Next = true)
    {
        Debug.Assert(foo != null);
        _next = next;
        _foreward2Next = foreward2Next;
    }

    // Scoped服务要在InvokeAsync中做方法注入
    public async Task InvokeAsync(HttpContext context, IBar bar)
    {
        Debug.Assert(bar != null);
        await context.Response.WriteAsync("Hello world");
        if (_foreward2Next) await _next(context);
    }
}

2.3 MVC应用

2.3.1 Controller/PageModel

csharp
private IHostEnvironment _env;
public AccountController(IHostEnvironment env) => _env = env;

如果仅在个别Action方法使用消费服务,也可以通过[FromService]方式注入对象。

csharp
public async Task Post([FromServices] IHostEnvironment env){}

2.3.2 View

View中需要用@inject再声明一下,起一个别名。

html
@inject IHostEnvironment env
<!DOCTYPE html>
<html>
<head></head>
<body>
  @env.EnvironmentName
</body>
</html>

3. 生命周期

服务声明周期中我们对依赖注入服务的生命周期做了深入的探讨。

3.1 IServiceProvider

ASP.Net 在应用程序正常启动后,它会利用注册的服务创建一个作为根容器的IServiceProvider 对象,我们可以将它称为 ApplicationServices 。如果应用在处理某个请求的过程中需要采用依赖注入的方式激活某个服务实例,那么它会利用这个IServiceProvider对象创建一个代表服务范围的ServiceScope对象,后者会指定一个IServiceProvider对象作为子容器,请求处理过程中所需的服务实例均由它来提供,我们可以将它称为 RequestServices

3.2 Scoped

Scoped服务既不应该由作为根容器的ApplicationServices来提供,也不能注入一个 Singleton服务中,否则它将无法在请求结束之后释放。如果忽视了这个问题,就容易造成内存泄漏

我们可以通过启用针对服务范围的验证来避免采用作为根容器的IServiceProvider对象来提供 Scoped服务实例。此选项默认是开启的。

csharp
public static void Main(string[] args)
{
    Host.CreateDefaultBuilder()
        .ConfigureServices(services => services
            .AddScoped<IFoo, Foo>())
        .UseDefaultServiceProvider(configure => configure.ValidateScopes = true)
        .ConfigureWebHostDefaults(builder => builder.Configure(app =>
            app.Run(async context =>
            {
                // 错误示范
                // var foo = app.ApplicationServices.GetService<IFoo>();
                // Debug.Assert(foo != null);

                // 正确做法
                var foo = context.RequestServices.GetService<IFoo>();
                Debug.Assert(foo != null);
                
                await context.Response.WriteAsync("Hello world");
            })
        ))
        .Build()
        .Run();
}

以上手动关闭了验证并使用根容器获取Scoped服务是错误的做法,此处仅做学习探讨示范,切勿在开发中使用。

如果需要在中间件中注入Scoped服务,可以采用强类型(实现IMiddleware接口)的中间件定义方式,并将中间件以Scoped服务进行注册即可。如果采用基于约定的中间件定义方式,我们有两种方案来解决这个问题:第一种解决方案就是在 InvokeAsync方法中利用 HttpContextRequestServices属性得到基于当前请求的 IServiceProvider对象,并利用它来提供依赖的服务。第二种解决方案则是按照如下所示的方式直接在InvokeAsync方法中注入依赖的服务。用法参见2.2 中间件

3.3 第三方DI框架

通过调用IHostBuilder接口的UseServiceProviderFactory<TContainerBuilder>方法注册IServiceProviderFactory<TContainerBuilder>工厂的方式可以实现与第三方依赖注入框架的整合。使用案例参考Autofac

Released under the MIT License.