来源: .NET8 依赖注入 – xiaolipro – 博客园
依赖注入(Dependency Injection,简称DI)是一种设计模式,用于解耦组件(服务)之间的依赖关系。它通过将依赖关系的创建和管理交给外部容器来实现,而不是在组件(服务)内部直接创建依赖对象。
咱就是通过 IServiceCollection 和 IServiceProvider 来实现的,他们直接被收入到了runtime libraries,在整个.NET平台下通用!
3.1 ServiceCollection
IServiceCollection 本质是一个 ServiceDescriptor 而 ServiceDescriptor 则是用于描述服务类型,实现和生命周期
public interface IServiceCollection :
IList<ServiceDescriptor>,
ICollection<ServiceDescriptor>,
IEnumerable<ServiceDescriptor>,
IEnumerable;
官方提供一些列拓展帮助我们向服务容器中添加服务描述,具体在 ServiceCollectionServiceExtensions
builder.Services.AddTransient<StudentService>();
builder.Services.AddKeyedTransient<IStudentRepository, StudentRepository>("a");
builder.Services.AddKeyedTransient<IStudentRepository, StudentRepository2>("b");
builder.Services.AddTransient<TransientService>();
builder.Services.AddScoped<ScopeService>();
builder.Services.AddSingleton<SingletonService>();
3.2 ServiceProvider
IServiceProvider 定义了一个方法 GetService,帮助我们通过给定的服务类型,获取其服务实例
public interface IServiceProvider
{
object? GetService(Type serviceType);
}
下面是 GetService 的默认实现(如果不给定engine scope,则默认是root)
public object? GetService(Type serviceType) => GetService(ServiceIdentifier.FromServiceType(serviceType), Root);
也就是
internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
{
if (_disposed)
{
ThrowHelper.ThrowObjectDisposedException();
}
ServiceAccessor serviceAccessor = _serviceAccessors.GetOrAdd(serviceIdentifier, _createServiceAccessor);
OnResolve(serviceAccessor.CallSite, serviceProviderEngineScope);
DependencyInjectionEventSource.Log.ServiceResolved(this, serviceIdentifier.ServiceType);
object? result = serviceAccessor.RealizedService?.Invoke(serviceProviderEngineScope);
System.Diagnostics.Debug.Assert(result is null || CallSiteFactory.IsService(serviceIdentifier));
return result;
}
其中,服务标识符 ServiceIdentifier 其实就是包了一下服务类型,和服务Key(为了.NET8的键化服务)
internal readonly struct ServiceIdentifier : IEquatable<ServiceIdentifier>
{
public object? ServiceKey { get; }
public Type ServiceType { get; }
}
显而易见的,我们的服务解析是由 serviceAccessor.RealizedService 提供,而创建服务访问器 serviceAccessor 只有一个实现 CreateServiceAccessor
private ServiceAccessor CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
{
ServiceCallSite? callSite = CallSiteFactory.GetCallSite(serviceIdentifier, new CallSiteChain());
if (callSite != null)
{
DependencyInjectionEventSource.Log.CallSiteBuilt(this, serviceIdentifier.ServiceType, callSite);
OnCreate(callSite);
if (callSite.Cache.Location == CallSiteResultCacheLocation.Root)
{
object? value = CallSiteRuntimeResolver.Instance.Resolve(callSite, Root);
return new ServiceAccessor { CallSite = callSite, RealizedService = scope => value };
}
Func<ServiceProviderEngineScope, object?> realizedService = _engine.RealizeService(callSite);
return new ServiceAccessor { CallSite = callSite, RealizedService = realizedService };
}
return new ServiceAccessor { CallSite = callSite, RealizedService = _ => null };
}
3.2.1 ServiceProviderEngine
ServiceProviderEngine 是服务商解析服务的执行引擎,它在服务商被初始化时建立。有两种引擎,分别是动态引擎和运行时引擎,在 NETFRAMEWORK || NETSTANDARD2_0 默认使用动态引擎。
private ServiceProviderEngine GetEngine()
{
ServiceProviderEngine engine;
#if NETFRAMEWORK || NETSTANDARD2_0
engine = CreateDynamicEngine();
#else
if (RuntimeFeature.IsDynamicCodeCompiled && !DisableDynamicEngine)
{
engine = CreateDynamicEngine();
}
else
{
engine = RuntimeServiceProviderEngine.Instance;
}
#endif
return engine;
[UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode",
Justification = "CreateDynamicEngine won't be called when using NativeAOT.")]
ServiceProviderEngine CreateDynamicEngine() => new DynamicServiceProviderEngine(this);
}
由于.NET Aot技术与dynamic技术冲突,因此Aot下只能使用运行时引擎,但动态引擎在大多情况下仍然是默认的。
动态引擎使用了emit技术,这是一个动态编译技术,而aot的所有代码都需要在部署前编译好,因此运行时无法生成新的代码。那运行时引擎主要使用反射,目标是在不牺牲太多性能的情况下,提供一个在aot环境中可行的解决方案。
我们展开动态引擎来看看它是如何解析服务的。
public override Func<ServiceProviderEngineScope, object?> RealizeService(ServiceCallSite callSite)
{
int callCount = 0;
return scope =>
{
var result = CallSiteRuntimeResolver.Instance.Resolve(callSite, scope);
if (Interlocked.Increment(ref callCount) == 2)
{
_ = ThreadPool.UnsafeQueueUserWorkItem(_ =>
{
try
{
_serviceProvider.ReplaceServiceAccessor(callSite, base.RealizeService(callSite));
}
catch (Exception ex)
{
DependencyInjectionEventSource.Log.ServiceRealizationFailed(ex, _serviceProvider.GetHashCode());
Debug.Fail($"We should never get exceptions from the background compilation.{Environment.NewLine}{ex}");
}
},
null);
}
return result;
};
}
这个实现的关键思想是,第一次解析服务时使用一个简单的运行时解析器,这样可以快速返回服务实例。然后,当服务再被解析,它会在后台线程上启动一个编译过程,生成一个更高效的服务解析委托。一旦编译完成,新的委托会替换掉原来的委托,以后的服务解析将使用这个新的、更高效的委托。这种方法可以在不影响应用程序启动时间的情况下,逐渐优化服务解析的性能。
3.2.2 ServiceProviderEngineScope
ServiceProviderEngineScope 闪亮登场,他是我们服务商的代言人,从定义不难看出他对外提供了服务商所具备的一切能力
internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IKeyedServiceProvider, IAsyncDisposable, IServiceScopeFactory
{
private List<object>? _disposables;
internal Dictionary<ServiceCacheKey, object?> ResolvedServices { get; }
public IServiceProvider ServiceProvider => this;
public IServiceScope CreateScope() => RootProvider.CreateScope();
}
我们来观察他获取服务的逻辑,可以发现他就是很朴实无华的用着我们根服务商 ServiceProvider,去解析服务,那 engine scope 呢,就是 this。现在我们已经隐约可以知道engine scope,就是为了满足scope生命周期而生。而 ResolvedServices 中存的呢,就是该scope中的所有scope生命周期服务实例啦,在这个scope中他们是唯一的。
public object? GetService(Type serviceType)
{
if (_disposed)
{
ThrowHelper.ThrowObjectDisposedException();
}
return RootProvider.GetService(ServiceIdentifier.FromServiceType(serviceType), this);
}
再来看另一个核心的方法:CaptureDisposable,实现disposable的服务会被添加到 _disposables。
internal object? CaptureDisposable(object? service)
{
if (ReferenceEquals(this, service) || !(service is IDisposable || service is IAsyncDisposable))
{
return service;
}
bool disposed = false;
lock (Sync)
{
if (_disposed)
{
disposed = true;
}
else
{
_disposables ??= new List<object>();
_disposables.Add(service);
}
}
if (disposed)
{
if (service is IDisposable disposable)
{
disposable.Dispose();
}
else
{
object? localService = service;
Task.Run(() => ((IAsyncDisposable)localService).DisposeAsync().AsTask()).GetAwaiter().GetResult();
}
ThrowHelper.ThrowObjectDisposedException();
}
return service;
}
在engine scope销毁时,其作用域中所有scope生命周期且实现了disposable的服务(其实就是_disposable)呢,也会被一同的销毁。
public ValueTask DisposeAsync()
{
List<object>? toDispose = BeginDispose();
if (toDispose != null)
{
}
}
那么有同学可能就要问了:单例实例既然不存在root scope中,而是单独丢到了调用点上,那他是咋销毁的?压根没看到啊,那不得泄露了?
其实呀,同学们并不用担心这个问题。首先,单例服务的实例确实是缓存在调用点上,但 disable 服务仍然会被 scope 捕获呀(在下文会详细介绍)。在 BeginDispose 中的,我们会去判断,如果是 singleton case,且root scope 没有被销毁过,我们会主动去销毁喔~
if (IsRootScope && !RootProvider.IsDisposed()) RootProvider.Dispose();
3.3 ServiceCallSite
ServiceCallSite 的主要职责是封装服务解析的逻辑,它可以代表一个构造函数调用、属性注入、工厂方法调用等。DI系统使用这个抽象来表示服务的各种解析策略,并且可以通过它来生成服务实例。
internal abstract class ServiceCallSite
{
protected ServiceCallSite(ResultCache cache)
{
Cache = cache;
}
public abstract Type ServiceType { get; }
public abstract Type? ImplementationType { get; }
public abstract CallSiteKind Kind { get; }
public ResultCache Cache { get; }
public object? Value { get; set; }
public object? Key { get; set; }
public bool CaptureDisposable => ImplementationType == null ||
typeof(IDisposable).IsAssignableFrom(ImplementationType) ||
typeof(IAsyncDisposable).IsAssignableFrom(ImplementationType);
}
3.3.1 ResultCache
其中 ResultCache 定义了我们如何缓存解析后的结果
public CallSiteResultCacheLocation Location { get; set; }
public ServiceCacheKey Key { get; set; }
CallSiteResultCacheLocation 是一个枚举,定义了几个值
Root:表示服务实例应该在根级别的 IServiceProvider 中缓存。这通常意味着服务实例是单例的(Singleton),在整个应用程序的生命周期内只会创建一次,并且在所有请求中共享。
Scope:表示服务实例应该在当前作用域(Scope)中缓存。对于作用域服务(Scoped),实例会在每个作用域中创建一次,并在该作用域内的所有请求中共享。
Dispose:尽管这个名称可能会让人误解,但在 ResultCache 的上下文中,Dispose 表示着服务是瞬态的(每次请求都创建新实例)。
None:表示没有缓存服务实例。
ServiceCacheKey 结构体就是包了一下服务标识符和一个slot,用于适配多实现的
internal readonly struct ServiceCacheKey : IEquatable<ServiceCacheKey>
{
public ServiceIdentifier ServiceIdentifier { get; }
public int Slot { get; }
}
3.3.2 CallSiteFactory.GetCallSite
那我们来看看调用点是怎么创建的吧,其实上面已经出现过一次了:
private ServiceCallSite? CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
{
if (!_stackGuard.TryEnterOnCurrentStack())
{
return _stackGuard.RunOnEmptyStack(CreateCallSite, serviceIdentifier, callSiteChain);
}
var callsiteLock = _callSiteLocks.GetOrAdd(serviceIdentifier, static _ => new object());
lock (callsiteLock)
{
callSiteChain.CheckCircularDependency(serviceIdentifier);
ServiceCallSite? callSite = TryCreateExact(serviceIdentifier, callSiteChain) ??
TryCreateOpenGeneric(serviceIdentifier, callSiteChain) ??
TryCreateEnumerable(serviceIdentifier, callSiteChain);
return callSite;
}
}
那服务点的创建过程我就简单概述一下啦
- 查找调用点缓存,存在就直接返回啦
- 服务标识符会被转成服务描述符
ServiceDescriptor (key化服务不指定key默认取last)
- 计算
ServiceCallSite,依次是:
- TryCreateExact
- 计算
ResultCache
- 如果已经有实现实例了,则返回
ConstantCallSite:表示直接返回已经创建的实例的调用点。
- 如果有实现工厂,则返回
FactoryCallSite:表示通过工厂方法创建服务实例的调用点。
- 如果有实现类型,则返回
ConstructorCallSite:表示通过构造函数创建服务实例的调用点。
- TryCreateOpenGeneric
- 根据泛型定义获取服务描述符
ServiceDescriptor
- 计算
ResultCache
- 使用服务标识符中的具体泛型参数来构造实现的闭合类型
- AOT兼容性测试(因为不能保证值类型泛型的代码已经生成)
- 如果成功闭合,则返回
ConstructorCallSite:表示通过构造函数创建服务实例的调用点。
- TryCreateEnumerable
- 确定类型是
IEnumerable<T>
- AOT兼容性测试(因为不能保证值类型数组的代码已经生成)
- 如果
T 不是泛型类型,并且可以找到对应的服务描述符集合,则循环 TryCreateExact
- 否则,方向循环 TryCreateExact,然后方向循环 TryCreateOpenGeneric
3.4 CallSiteVisitor
好了,有了上面的了解我们可以开始探索服务解析的内幕了。服务解析说白了就是引擎围着 CallSiteVisitor 转圈圈,所谓的解析服务,其实就是访问调用点了。
protected virtual TResult VisitCallSite(ServiceCallSite callSite, TArgument argument)
{
if (!_stackGuard.TryEnterOnCurrentStack())
{
return _stackGuard.RunOnEmptyStack(VisitCallSite, callSite, argument);
}
switch (callSite.Cache.Location)
{
case CallSiteResultCacheLocation.Root:
return VisitRootCache(callSite, argument);
case CallSiteResultCacheLocation.Scope:
return VisitScopeCache(callSite, argument);
case CallSiteResultCacheLocation.Dispose:
return VisitDisposeCache(callSite, argument);
case CallSiteResultCacheLocation.None:
return VisitNoCache(callSite, argument);
default:
throw new ArgumentOutOfRangeException();
}
}
为了方便展示,我们这里的解析器都拿运行时来说,因为内部是反射,而emit、expression实在是难以观看。
3.4.1 VisitRootCache
那我们来看看单例的情况下,是如何访问的:
protected override object? VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
{
if (callSite.Value is object value)
{
return value;
}
var lockType = RuntimeResolverLock.Root;
ServiceProviderEngineScope serviceProviderEngine = context.Scope.RootProvider.Root;
lock (callSite)
{
if (callSite.Value is object callSiteValue)
{
return callSiteValue;
}
object? resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext
{
Scope = serviceProviderEngine,
AcquiredLocks = context.AcquiredLocks | lockType
});
serviceProviderEngine.CaptureDisposable(resolved);
callSite.Value = resolved;
return resolved;
}
}
好,可以看到真正解析调用点的主角出来了 VisitCallSiteMain,那这里的 CallSiteKind 上面计算 ServiceCallSite 时呢已经讲的很清楚啦,咱对号入座就行了
protected virtual TResult VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
{
switch (callSite.Kind)
{
case CallSiteKind.Factory:
return VisitFactory((FactoryCallSite)callSite, argument);
case CallSiteKind.IEnumerable:
return VisitIEnumerable((IEnumerableCallSite)callSite, argument);
case CallSiteKind.Constructor:
return VisitConstructor((ConstructorCallSite)callSite, argument);
case CallSiteKind.Constant:
return VisitConstant((ConstantCallSite)callSite, argument);
case CallSiteKind.ServiceProvider:
return VisitServiceProvider((ServiceProviderCallSite)callSite, argument);
default:
throw new NotSupportedException(SR.Format(SR.CallSiteTypeNotSupported, callSite.GetType()));
}
}
我们就看看最经典的通过构造函数创建服务实例的调用点 ConstructorCallSite,很显然就是new嘛,只不过可能构造中依赖其它服务,那就递归创建呗。easy,其它几种太简单了大家自己去看看吧。
protected override object VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
{
object?[] parameterValues;
if (constructorCallSite.ParameterCallSites.Length == 0)
{
parameterValues = Array.Empty<object>();
}
else
{
parameterValues = new object?[constructorCallSite.ParameterCallSites.Length];
for (int index = 0; index < parameterValues.Length; index++)
{
parameterValues[index] = VisitCallSite(constructorCallSite.ParameterCallSites[index], context);
}
}
return constructorCallSite.ConstructorInfo.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: parameterValues, culture: null);
}
3.4.2 VisitScopeCache
在访问单例缓存的时候呢,仅仅通过了一个double check lock就搞定了,因为人家全局的嘛,咱再来看看访问作用域缓存,会不会有什么不一样
protected override object? VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
{
return context.Scope.IsRootScope ?
VisitRootCache(callSite, context) :
VisitCache(callSite, context, context.Scope, RuntimeResolverLock.Scope);
}
哈哈,它果然很不一般啊,上来就来检查我们是否是 root scope。如果是这种case呢,则走 VisitRootCache。但是奇怪啊,为什么访问 scope cache,所在 engine scope 能是 root scope?
还记得 ServiceProvider 获取的服务实例的核心方法吗?engine scope 他是传进来的,如果我们给一个 root scope,自然就出现的这种case,只是这种 case 特别罕见。
internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
VisitCache 的同步模型写的实在是酷,我们看 RuntimeResolverLock 的枚举就两个:Scope = 1 和 Root = 2
- AcquiredLocks=Scope时
- 那 AcquiredLocks&false==0 显然成立,申请锁,也就是尝试独占改作用域的ResolvedServices
- 申请成功进入同步块,重新计算AcquiredLocks|true=1
- 如此,在该engine scope 中这条链路上的调用点都被占有,直到结束
- AcquiredLocks=Root 时
- 那显然 engine scope 也应该是 root scope,那么走
VisitRootCache case
- 在
VisitRootCache 通过DCL锁住 root scope 上链路涉及的服务点,直至结束
至此我们应该不难看出这个设计的精妙之处,即在非 root scope(scope生命周期)中,scope之间是互相隔离的,没有必要像 root scope(singleton生命周期)那样,在所有scope中独占服务点。
private object? VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine
{
bool lockTaken = false;
object sync = serviceProviderEngine.Sync;
Dictionary<ServiceCacheKey, object?> resolvedServices = serviceProviderEngine.ResolvedServices;
if ((context.AcquiredLocks & lockType) == 0)
{
Monitor.Enter(sync, ref lockTaken);
}
try
{
if (resolvedServices.TryGetValue(callSite.Cache.Key, out object? resolved))
{
return resolved;
}
resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext
{
Scope = serviceProviderEngine,
AcquiredLocks = context.AcquiredLocks | lockType
});
serviceProviderEngine.CaptureDisposable(resolved);
resolvedServices.Add(callSite.Cache.Key, resolved);
return resolved;
}
finally
{
if (lockTaken)
{
Monitor.Exit(sync);
}
}
}
3.4.3 VisitDisposeCache
我们看最后一个,也就是 Transient case
protected override object? VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
{
return context.Scope.CaptureDisposable(VisitCallSiteMain(transientCallSite, context));
}
异常的简单,我们沿用了scope的设计,但是我们没有进行任何缓存行为。即,每次都去访问调用点。