ASP.NET Core 依赖注入 — 如何避免手工构建 Service Provider 实例

依赖注入是个很好的理念。不过服务的注册都是在启动时(Startup-time)完成的。在Host完成启动之前的Startup::ConfigureServices()里,我们是没法拿到所注册服务的实例的。如果全盘采用依赖注入,这当然不成问题:服务和服务之间的依赖,可以在动态时刻自动解析。但是,有时候现实并不会那么完美,当配置一些选项的时候,我们可能需要一个具体的实例。有时候这会造成一定的困惑,尤其是当我们需要配合传统的手工new一个服务实例的时候(e.g.: new SomeServiceA(serviceB, serviceC,...))。在以前,网上随处可见有人推荐ServiceCollection.BuilServiceProvider()然后通过ServiceProvier.GetRequiredService<ServiceB>()获取所依赖的服务。

然而,自 ASP.NET Core 3.x 起,如果我们在应用层代码手工调用ServiceCollection.BuildServiceProvider(),很可能会得到一条警告消息:

Warning ASP0000 Calling ‘BuildServiceProvider’ from application code results in an additional copy of singleton services being created.

这是因为构建新的ServiceProvider会导致构建新的服务实例副本,而且这些新的服务实例副本完全独立于原来的服务容器。这往往会造成一些难以发现的Bug,例如这个Stack Overflow上的这个问题

那么如何避免手工创建ServiceCollection.BuildServiceProvider()呢?

普通服务的处理方法

  1. 无脑添加服务类型:

    1
    2
    services.AddSingleton<IServiceA, ServiceB>();
    services.AddSingleton<IServiceB, ServiceB>();
  2. 工厂函数法

有时候上述方法并不可行,我们需要更精细的控制实例的构造过程,这时候可以使用工厂函数来返回一个实例:

1
2
3
4
5
services.AddSingleton<IServiceA, ServcieA>();
services.AddSingleton<IServiceB>(sp => {
var svcA = sp.GetRequiredService<IServiceA>();
return new ServiceB(svcA);
});

Options的处理方法

特别地,针对Options,官方提供了两种注入方法。分别是委托法和配置选项类法。

方法一、 委托法

1
2
3
4
services.AddOptions<MyOptions>("optionalName")
.Configure<Service1, Service2, Service3, Service4, Service5>(
(o, s, s2, s3, s4, s5) =>
o.Property = DoSomethingWith(s, s2, s3, s4, s5));

这里的Configure<...>(opts,...)方法有多种变体,其第一个参数是相关选项,而后的参数分别代表需要通过依赖注入来解析的实例。

一个典型的例子是SO上这个thread:OP需要自定义一个ModelBinderProvider,并希望向其中注入一个ILogger:

1
2
3
4
5
6
7
public class MyOwnModelBinderProvider: IModelBinderProvider
{
public MyOwnModelBinderProvider(ILogger<MyOwnModelBinderProvider> logger)
{
// .. bla bla
}
}

然而,正常情况下,大家添加ModelBinderProvider并非是通过依赖注入进行的,而是手工构建:

1
2
3
4
services.AddControllersAndViews(configure => configure.ModelBinderProviders.Insert(
0,
new MyOwnModelBinderProvider(/** WHAT TO PUT HERE? **/)
));

问题在于,欲手工构建MyOwnModelBinderProvider,必须先拿到ILogger<MyOwnModelBinderProvider>实例。然而,ILogger<MyOwnModelBinderProvider>是在程序启动之后通过DI容器获取的。如何不通过手工构建IServiceProvider实例来解决这个问题呢?

其实,追踪一下ASP.NET Core的源码实现可以发现,AddControllersAndViews(configure)除了添加控制器、视图等相关服务之外,还会配置一个MvcOptions选项。在底层,ASP.NET Core是通过以下这个方法配置MvcOptions的:

1
2
3
4
5
6
7
public static IMvcBuilder AddMvcOptions( this IMvcBuilder builder, Action<MvcOptions> setupAction){
if (builder == null){ /* throw ...*/ }
if (setupAction == null){ /* throw ...*/ }

builder.Services.Configure(setupAction);
return builder;
}

既然MvcOptions是通过Options模式来配置的,我们就可以使用依赖注入来配置它的细节!于是就有了这样一个顺理成章的解决办法

1
2
3
4
5
builder.Services.AddOptions<MvcOptions>()
.Configure<ILoggerFactory>((options, loggerFactory) =>
{
options.ModelBinderProviders.Add(new MyOwnModelBinderProvider(loggerFactory));
});

方法二、IConfigureOptions<TOptions>法:

除了委托法之外,官方还提供了另外一种方式

Create your own type that implements IConfigureOptions<TOptions> or IConfigureNamedOptions<TOptions> and register the type as a service.

显然,这种方式相比于委托法更加重量级。

我们还是以SO上的一个thread为例:OP想要根据当前用户的权限的不同,序列化不同的字段给客户端。总体思路是,我们可以自定义一个DefaultContractResolver类,判断当前属性是否有相关Attribute(例如[RequireRoleView("HR")]),然后通过UserManager服务获取当前用户的所有角色,最后通过比对当前用户是否拥有相应的角色来决定是否实例化该字段。

不过,其中一个问题在于,为了自定义如何JSON序列化,传统上我们是通过为JsonOptions添加ContractResolver进行的,而且,尴尬的是,我们往往都是手工构建这个实例的:

1
2
3
services.AddMvc().AddJsonOptions(o =>{
options.SerializerSettings.ContractResolver = new RoleBasedContractResolver( ??? );
});

显然在这里,这种手工new的方式是不合适的。解决办法和上面的思路一致,我们可以通过依赖注入来配置JsonOptions选项——这里我自定义了一个IConfigureOptions<MvcJsonOptions>类:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyMvcJsonOptionsWrapper : IConfigureOptions<MvcJsonOptions>
{
IServiceProvider ServiceProvider;
public MyMvcJsonOptionsWrapper(IServiceProvider serviceProvider)
{
this.ServiceProvider = serviceProvider;
}
public void Configure(MvcJsonOptions options)
{
options.SerializerSettings.ContractResolver =new RoleBasedContractResolver(ServiceProvider);
}
}

然后将之作为服务注册到DI容器即可:

1
services.AddTransient<IConfigureOptions<MvcJsonOptions>,MyMvcJsonOptionsWrapper>();