1 介绍
在 Microsoft.Extensions.DependencyInjection 库中,所有注册的服务可以分为三种类型:
- Singleton
- Scoped
- Transient
其中 Singleton 和 Transient 比较容易理解,但是 Scoped 这个概念有点抽象,这篇文章将要回答这些问题,以便更加方便理解这个概念
- 为什么有
Scoped概念? Scoped服务在注册和使用的时候有什么特殊之处?Scoped服务在使用的时候有什么注意的地方?
2 为什么有 Scoped 概念
Singleton 服务在生命周期中只有一个实例,Transient 服务在每次使用时候创建一个实例,而 Scoped 服务的生命周期在两者之间,比如在网络中,一个请求在处理的生命周期中将共用一个服务实例。
使用 Scoped 服务也可以降低资源的使用,比如在一个请求中使用同一个数据库连接实例,不使用全局唯一的连接实例能够保证数据库操作的隔离。在 ASP.NET Core 中,每个网络请求都是由一个个中间件 Middleware 拼接而成,有时候需要在不同的中间件中分享数据,这样 Scoped 服务的唯一性能够保证数据的正确传递。
但是在 ASP.NET Core 中,我们并且直接涉及到 Scoped 服务的使用,那么框架帮我们做了哪些工作呢?
DefaultHttpContextFactory
每个 HTTP 请求都体现为一个 HTTPContext, 每个依赖注入的服务都会从中创建出来,DefaultHttpContextFactory 是其中一个默认实现
internal void Initialize(DefaultHttpContext httpContext, IFeatureCollection featureCollection)
{
httpContext.Initialize(featureCollection);
if (_httpContextAccessor != null)
{
_httpContextAccessor.HttpContext = httpContext;
}
httpContext.FormOptions = _formOptions;
httpContext.ServiceScopeFactory = _serviceScopeFactory;
}
- RequestFeaturesFeatures
IServiceProviderFeature 是 ASP.NET Core 中依赖注册服务的抽象,默认情况是使用 RequestServicesFeature
private static readonly Func<DefaultHttpContext, IServiceProvidersFeature> _newServiceProvidersFeature = context => new RequestServicesFeature(context, context.ServiceScopeFactory);
那么 RequestServicesFeature 中,在实现 IServiceProviderFeature 的 RequestServices 属性的时候,就创建了 ScopedServiceProvider
public IServiceProvider RequestServices
{
get
{
if (!_requestServicesSet && _scopeFactory != null)
{
_context.Response.RegisterForDisposeAsync(this);
_scope = _scopeFactory.CreateScope();
_requestServices = _scope.ServiceProvider;
_requestServicesSet = true;
}
return _requestServices!;
}
set
{
_requestServices = value;
_requestServicesSet = true;
}
}
我们可以看出返回了一个 Scoped 类型的 IServiceProvider 对象,这个对象会被每个请求(HTTPContext) 创建,但是在同一个 HTTPContext 中不会重复创建。
3 Scoped 服务的实现
M.E.DepdendnecyInjection 库实现太过复杂,这里提供一个简易版本,通过它可以理解 Scoped 服务的特殊之处
- ServiceCollection 字段
public class ServiceCollection : IServiceProvider, IDisposable
{
internal readonly ConcurrentDictionary<Type, ServiceRegistry> _registries;
internal readonly ConcurrentDictionary<Key, object?> _services;
public ServiceCollection()
{
_registries = new ConcurrentDictionary<Type, ServiceRegistry>();
_root = this;
_services = new ConcurrentDictionary<Key, object?>();
}
internal ServiceCollection(ServiceCollection parent)
{
_root = parent._root;
_registries = _root._registries;
_services = new ConcurrentDictionary<Key, object?>();
}
}
ServiceCollection 中的 _registeries 和 _services 两个字段分别保留依赖服务的定义和已经创建的服务实例,如果 ServiceCollection 由别的 ServiceCollection 创建,那么 _root 字段指向 ServiceCollection 的根实例,而且注入服务的定义都共享根实例的集合。在我们创建一个 Scoped ServiceCollection 的时候,我们就会使用这种构造方法。
- GetServicesCore
private object? GetServiceCore(ServiceRegistry registry,
Type[] genereicArguments) {
var key = new Key(registry, genereicArguments);
switch (registry.Lifetime) {
case Lifetime.Singleton:
return GetOrCreate(_root._services, _root._disposables);
case Lifetime.Scoped:
return GetOrCreate(_services, _disposables);
default: {
var service = registry.Factory(this, genereicArguments);
if (service is IDisposable disposable && disposable != this) {
_disposables.Add(disposable);
}
return service;
}
}
object? GetOrCreate(ConcurrentDictionary<Key, object?> services,
ConcurrentBag<IDisposable> disposables) {
if (services.TryGetValue(key, out var service)) {
return services;
}
service = registry.Factory(this, genereicArguments);
services[key] = service;
if (service is IDisposable disposable) {
disposables.Add(disposable);
}
return service;
}
}
创建服务的细节都在 GetServicesCore 这个方法中,首先在 GetOrCreate 方法中,我们会判断一下是否这个服务实例已经创建,如果没有创建,则调用注册时候定义创建。然后在创建的时候,如果是 Singleton ,我们就传入 _root 的 _services, 我们刚刚提到的所有 ServiceCollection 都指向了唯一的根实例,也是Singleton 服务的要求;如果是 Scoped 服务,则用当前 ServiceCollection 实例的 _service 去尝试创建,这就说明了如果我们不去创建 Scoped 的 ServiceCollection, 那么 Scoped 服务和 Singleton 服务没有任何本质的区别;最后如果这个服务是 Transient 的,那么每次创建都会创建新的实例,不管之前是否创建过。
所以注册为 Scoped 的服务,在每个 ServiceCollection 中都会被缓存下来,以便这个 ServiceCollection 后续续续使用;并且子 ServiceCollection 或者父 ServiceCollection , 甚至兄弟 ServiceCollection 并不能看到这个缓存的依赖服务的实例;如果这个 ServiceCollection 是根实例,那么 Scoped 和 Singleton 没有区别。
3 Scoped 使用的注意点
在 DependnecyInjection 中如果一个依赖服务出现运行时不匹配,比如一个 Singleton 类型服务依赖一个 Scoped 类型的服务
public class FooService
{
public void DoWork()
{
Console.WriteLine("FooService is starting.");
}
}
public class BarService (FooService foo) : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
foo.DoWork();
return Task.CompletedTask;
}
}
builder.Services.AddHostedService<BarService>();
builder.Services.AddScoped<FooService>();
AddHostedService 会将 BarService 注册为 Singleton 类型,但是它依赖一个 FooService 是 Scope 类型的,所以在运行的时候,就会报错。那么我们该如何解决这个问题呢?我们可以让 BarService 依赖一个 IServiceScopeFactory 接口,因为它是按照 Singleton 类型注入到容器中
public class BarService (IServiceScopeFactory _factory) : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = _factory.CreateScope();
var foo = scope.ServiceProvider.GetRequiredService<FooService>();
foo.DoWork();
return Task.CompletedTask;
}
}
除此之外,如果 Scoped 服务没有处理好的话,还有可能会导致内存泄漏。在我们简易版 ServiceCollection 实现中,我们忽略了一个字段 _disposables, 这是一个集合,它记录了这个 ServiceCollection 创建的所有实现 IDispose 的对象,这个集合会在 ServiceCollection 调用 Dispose 方式被处理
public void Dispose() {
_disposed = true;
foreach (var disposable in _disposables) {
disposable.Dispose();
}
_disposables.Clear();
_services.Clear();
}
在 ASP.NET Core 中,每个请求对象的 ServiceCollection 对象会在请求处理完毕后调用 Dispose 方法
public class RequestServicesFeature : IServiceProvidersFeature,
IDisposable,
IAsyncDisposable {
private readonly IServiceScopeFactory? _scopeFactory;
private IServiceProvider? _requestServices;
private IServiceScope? _scope;
public IServiceProvider RequestServices {
get {
if (!_requestServicesSet && _scopeFactory != null) {
_context.Response.RegisterForDisposeAsync(this);
//...
}
return _requestServices !;
}
}
public ValueTask DisposeAsync() {
switch (_scope) {
case IAsyncDisposable asyncDisposable:
//..
break;
case IDisposable disposable:
disposable.Dispose();
break;
}
_scope = null;
_requestServices = null;
//..
}
/// <inheritdoc />
public void Dispose() { DisposeAsync().AsTask().GetAwaiter().GetResult(); }
}
这里将 RequestServicesFeature 这个对象注册到 Response.RegisterForDisposeAsync 回调中,当请求完成的时候, _scope 对象就会调用 Dispose 方法,其中创建的Scoped 和 Transient 服务就会被释放。我们这些做的前提都是能够创建 Scoped 的 ServiceCollection,因为框架已经帮我们做好相应的工作。但是如果是要给 ASP.NET Framework 引入这个依赖注入,就需要特别注意显示的创建 Scoped ServiceCollection,具体实现细节可以查看这篇问答。