来源: .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的设计,但是我们没有进行任何缓存行为。即,每次都去访问调用点。