.NET Core基礎篇之:依賴注入DependencyInjection

暢飲無緒發表於2021-11-30

依賴注入已經不是什麼新鮮話題了,在.NET Framework時期就已經出現了各種依賴注入框架,比如:autofacunity等。只是在.net core微軟將它搬上了檯面,不用再依賴第三方元件(那是不可能的)。依賴注入的概念與為什麼選擇使用依賴注入這裡就不說了,網上搜一下就會有各種答案,今天這裡的內容是看看在.net core中,簡單實用的依賴注入,背後到底做了哪些操作。

建立專案

今天演示不採用asp.net core專案,而是採用.net core控制檯。相對前者,後者的操作邏輯更加完整簡潔。

準備介面與物件,老User

public class UserService : IUserService
{
    public string getName()
    {
        return "my name is dotnetboy";
    }
}

public interface IUserService
{
    string getName();
}
/// <summary>
/// 入口方法
/// </summary>
/// <param name="args"></param>
public static void Main(string[] args)
{
	// 1、例項化服務容器
    IServiceCollection services = new ServiceCollection();
    // 2、新增服務
    services.AddTransient<IUserService, UserService>();
    // 3、構建服務提供物件
    IServiceProvider serviceProvider = services.BuildServiceProvider();
    // 4、解析並獲取服務物件
    var service = serviceProvider.GetService<IUserService>();
	// 5、呼叫
    Console.WriteLine(service.getName());
}

1、例項化服務容器

IServiceCollection services = new ServiceCollection();

這裡出現了一個新物件:IServiceCollectionService,也就是startup中的

public void ConfigureServices(IServiceCollection services){}

F12 可以看到,IServiceCollectionService 繼承了IList<ServiceDescriptor>介面,又引申出 ServiceDescriptor 物件。

//
// 摘要:
//     Specifies the contract for a collection of service descriptors.
public interface IServiceCollection : IList<ServiceDescriptor>, ICollection<ServiceDescriptor>, IEnumerable<ServiceDescriptor>, IEnumerable
{
}

ServiceDescriptor 物件見名知意就知道是用來描述服務資訊的物件了。ServiceDescriptor 物件的內容有點多,在這裡我們暫時只需要瞭解三個:

	/// <summary>
    /// Describes a service with its service type, implementation, and lifetime.
    /// </summary>
    [DebuggerDisplay("Lifetime = {Lifetime}, ServiceType = {ServiceType}, ImplementationType = {ImplementationType}")]
    public class ServiceDescriptor
    {
        /// 生命週期
        public ServiceLifetime Lifetime { get; }

        /// 服務物件
        public Type ServiceType { get; }

        /// 服務實現物件
        public Type ImplementationType { get; }
}
  • Lifetime:生命週期

  • SericeType:服務物件

  • ImplementationType:服務實現物件

第一步內容比較簡單,就是宣告一個服務容器集合。我們可以把 IServiceCollection 比做成銀行,把服務比喻成 RMB ,現在銀行有了,我們下一步肯定就是存 RMB 進去了。

2、新增服務

上面我們提到了 ServiceDescriptor 物件的三個屬性:LifetimeServiceTypeImplementationType

再回過頭看 services.AddTransient<IUserService, UserService>(); 這段程式碼

  • AddTransient 指定了 Lifetime,也就是 Transient

  • IUserService 表示 ServiceType

  • UserService 表示 ImplementationType

下面是 AddTransient 相關的原始碼,很是直觀明瞭。就是將 RMB 儲存銀行,到底是 活期定期還是理財就由開發者自己去定義了。

public static IServiceCollection AddTransient(
    this IServiceCollection services,
    Type serviceType,
    Type implementationType)
{
    ......
    return Add(services, serviceType, implementationType, ServiceLifetime.Transient);
}
private static IServiceCollection Add(
    IServiceCollection collection,
    Type serviceType,
    Type implementationType,
    ServiceLifetime lifetime)
{
    var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
    collection.Add(descriptor);
    return collection;
}

3、構建服務提供物件

上面兩步,有了銀行,並且將RMB存進去了。接下來我要買包子沒錢,這時候就需要將RMB再取出來。但是存的時候我是在不同的網點存的,取的時候我想在手機銀行APP上取,這第三步就是為了構建手機銀行APP這個角色。

IServiceProvider serviceProvider = services.BuildServiceProvider();

在這一步會引入一個新物件 ServiceProvider ,也就是給我們提供服務的物件,和 ServiceProviderEngine ,服務提供引擎,姑且這麼叫吧。

internal class DynamicServiceProviderEngine : CompiledServiceProviderEngine : ServiceProviderEngine

仔細看下面這段原始碼(去除不相關部分),就是簡單例項化一個 ServiceProvider 物件,ServiceProvider 物件包含一個 IServiceProviderEngine 屬性,在 ServiceProvider 物件的建構函式內例項化並賦值。

public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options)
{
    ......
    return new ServiceProvider(services, options);
}
private readonly IServiceProviderEngine _engine;
// serviceDescriptors:服務集合,options:服務提供者型別
internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
{
    IServiceProviderEngineCallback callback = null;
    switch (options.Mode)
    {
        case ServiceProviderMode.Default:
            _engine = new DynamicServiceProviderEngine(serviceDescriptors, callback);
    }
}
    

ServiceProviderEngine 物件的內容比較多,由於上面程式碼只做了例項化,所以我們也只看與例項化相關的建構函式程式碼。

internal abstract class ServiceProviderEngine : IServiceProviderEngine, IServiceScopeFactory
{
    private readonly Func<Type, Func<ServiceProviderEngineScope, object>> _createServiceAccessor;
    internal ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object>> RealizedServices { get; }
    public ServiceProviderEngineScope Root { get; }
    public IServiceScope RootScope => Root;
    protected CallSiteRuntimeResolver RuntimeResolver { get; }
    internal CallSiteFactory CallSiteFactory { get; }
    protected ServiceProviderEngine(IEnumerable<ServiceDescriptor> serviceDescriptors, IServiceProviderEngineCallback callback)
    {
        _createServiceAccessor = CreateServiceAccessor;
        Root = new ServiceProviderEngineScope(this);
        RuntimeResolver = new CallSiteRuntimeResolver();
        CallSiteFactory = new CallSiteFactory(serviceDescriptors);
        CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
        CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());
        RealizedServices = new ConcurrentDictionary<Type,Func<ServiceProviderEngineScope, object>>();
    }
}

看了上面的建構函式,哇,又多了這麼多新物件,無從下手是不是。這時候我們記住一點,RMB 存到了銀行,所以我們就盯著銀行:ServiceDescriptor 的動靜,發現銀行與 CallSiteFactory 這個物件有關聯,CallSiteFactory 物件例項化需要銀行(serviceDescriptors)。

CallSiteFactory = new CallSiteFactory(serviceDescriptors);
CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());
private readonly List<ServiceDescriptor> _descriptors;
private readonly Dictionary<Type, ServiceDescriptorCacheItem> _descriptorLookup = new Dictionary<Type, ServiceDescriptorCacheItem>();
private readonly StackGuard _stackGuard;

public CallSiteFactory(IEnumerable<ServiceDescriptor> descriptors)
{
    _stackGuard = new StackGuard();
    _descriptors = descriptors.ToList();
    Populate();
}

private void Populate()
{
    foreach (var descriptor in _descriptors)
    {
        var serviceTypeInfo = descriptor.ServiceType.GetTypeInfo();
        ......
        var cacheKey = descriptor.ServiceType;
        _descriptorLookup.TryGetValue(cacheKey, out var cacheItem);
        _descriptorLookup[cacheKey] = cacheItem.Add(descriptor);
    }
}

進入到 CallSiteFactory 物件內邏輯比較清晰,就是將我們銀行內的 RMB(服務) 資訊遍歷並儲存到相關字典集合內(_descriptorLookup),給後續邏輯提供查詢驗證服務。

4、獲取物件

上一步手機銀行App的角色已經構建好了,這一步要開始取RMB了,取RMB需要什麼?密碼唄,這裡的密碼就是 IUserService

var service = serviceProvider.GetService<IUserService>();

我們接下來看看取錢這一步微軟都做了些什麼操作,就是下面這段程式碼了:

internal object GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
{
    ......
    // 已經實現的服務
    var realizedService = RealizedServices.GetOrAdd(serviceType, _createServiceAccessor);
    // _callback?.OnResolve(serviceType, serviceProviderEngineScope);
    ......
    return realizedService.Invoke(serviceProviderEngineScope);
}

一眼看去,有效程式碼其實就一行,涉及到三個物件:RealizedServicesserviceType_createServiceAccessor,都在上一步中出現過。

RealizedServices.GetOrAdd(serviceType, _createServiceAccessor);
private readonly Func<Type, Func<ServiceProviderEngineScope, object>> _createServiceAccessor = CreateServiceAccessor;
internal ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object>> RealizedServices { get; }

我們將上面的程式碼發散一下,CreateServiceAccessor 就成了我們要研究的重點。

var csa = CreateServiceAccessor(serviceType);
RealizedServices.GetOrAdd(serviceType, csa);
private Func<ServiceProviderEngineScope, object> CreateServiceAccessor(Type serviceType)
{
	var callSite = CallSiteFactory.GetCallSite(serviceType, new CallSiteChain());
	if (callSite != null)
	{
		......
		return RealizeService(callSite);
	}
	return _ => null;
}

CreateServiceAccessor 方法內的有效程式碼是兩行,我們一步步來深入:

CallSiteFactory.GetCallSite(serviceType, new CallSiteChain());

進入到 GetCallSite 方法內部,一層層剝離

// 第一層
internal ServiceCallSite GetCallSite(Type serviceType, CallSiteChain callSiteChain)
{
    return _callSiteCache.GetOrAdd(serviceType, type => CreateCallSite(type, callSiteChain));
}
// 第二層
private ServiceCallSite CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
{
    ......
    var callSite = TryCreateExact(serviceType, callSiteChain) ??
                               TryCreateOpenGeneric(serviceType, callSiteChain) ??
                               TryCreateEnumerable(serviceType, callSiteChain);
    _callSiteCache[serviceType] = callSite;
    return callSite;
}
// 第三層
private ServiceCallSite TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, int slot)
{
    if (serviceType == descriptor.ServiceType)
    {
        ServiceCallSite callSite;
        var lifetime = new ResultCache(descriptor.Lifetime, serviceType, slot);
        ......
	    new ServiceCallSite(......);
        return callSite;
    }

    return null;
}

上面的程式碼都是圍繞 ServiceCallSite 物件的建立再流轉,依然是在為最後的取RMB(服務)做準備工作,我們注意一下這段程式碼:

var lifetime = new ResultCache(descriptor.Lifetime, serviceType, slot);

出現了一個熟悉的物件:Lifetime,也就是我們註冊服務的生命週期型別。

public ResultCache(ServiceLifetime lifetime, Type type, int slot)
{
    switch (lifetime)
    {
        case ServiceLifetime.Singleton:
            Location = CallSiteResultCacheLocation.Root;
            break;
        case ServiceLifetime.Scoped:
            Location = CallSiteResultCacheLocation.Scope;
            break;
        case ServiceLifetime.Transient:
            Location = CallSiteResultCacheLocation.Dispose;
            break;
        default:
            Location = CallSiteResultCacheLocation.None;
            break;
    }
    Key = new ServiceCacheKey(type, slot);
}
public CallSiteResultCacheLocation Location { get; set; }
public ServiceCacheKey Key { get; set; }

到現在,準備工作的相關程式碼都已經走完了,下面就是最後一步:獲取/構建物件

return RealizeService(callSite);
protected override Func<ServiceProviderEngineScope, object> RealizeService(ServiceCallSite callSite)
{
    var realizedService = ResolverBuilder.Build(callSite);
    RealizedServices[callSite.ServiceType] = realizedService;
    return realizedService;
}
// singleton
public Func<ServiceProviderEngineScope, object> Build(ServiceCallSite callSite)
{
    if (callSite.Cache.Location == CallSiteResultCacheLocation.Root)
    {
        var value = _runtimeResolver.Resolve(callSite, _rootScope);
        return scope => value;
    }
    return BuildType(callSite).Lambda;
}
// Scoped
private GeneratedMethod BuildType(ServiceCallSite callSite)
{
    if (callSite.Cache.Location == CallSiteResultCacheLocation.Scope)
    {
        return _scopeResolverCache.GetOrAdd(callSite.Cache.Key, _buildTypeDelegate, callSite);
    }
    return BuildTypeNoCache(callSite);
}
// Transient
private GeneratedMethod BuildTypeNoCache(ServiceCallSite callSite)
{
    var dynamicMethod = new DynamicMethod("ResolveService",
                                          attributes: MethodAttributes.Public | MethodAttributes.Static,
                                          callingConvention: CallingConventions.Standard,
                                          returnType: typeof(object),
                                          parameterTypes: new[] { typeof(ILEmitResolverBuilderRuntimeContext), typeof(ServiceProviderEngineScope) },
                                          owner: GetType(),
                                          skipVisibility: true);

    var ilGenerator = dynamicMethod.GetILGenerator(512);
    ......
}

上面一段程式碼就是最終構建服務的程式碼了,前面的所有內容都是在為這一步做準備,具體程式碼構建的邏輯這篇文章就不講了,有點技窮。看Transient:BuildTypeNoCache方法內容,可以發現微軟是通過 IL 去動態生成的服務。這只是裡面的一種方式,還有另外一種方式大家可以自行去研究。


最後,寫著寫著就發現,有點把握不住。儘管在調式的時候對裡面的一些程式碼的作用,以及怎麼運轉都有一些理解。但是寫出來就不是那麼回事,漏洞百出,索性貼出關鍵原始碼,記錄一下這兩天的研究成果。有條件的朋友可以自己去調式一遍原始碼,比看什麼部落格有效果多了。

我這裡使用的是:JetBrains Rider ,除錯原始碼比較方便,不用手動下載原始碼。

如果習慣了 vs 的同學可以去 github 上將原始碼下載下來通過 vs 去除錯。

相關文章