Denpendcy Injection 8.0新功能——KeyedService

Turnleft發表於2023-09-21

Denpendcy Injection 8.0新功能——KeyedService

本文只介紹 .NET Denpendcy Injection 8.0新功能——KeyedService,假定讀者已熟練使用之前版本的功能。

註冊帶Key的類

8.0之前,註冊一個類往往是AddSingleton<IFoo, Foo>(),8.0新增了一個新功能:“可以註冊一個帶Key的類AddKeyedSingleton<IFoo, Foo>("keyA")。獲取服務方法由GetService<IFoo>()變成了GetKeyedService<IFoo>("keyA"),並且呼叫這兩個方法建立出來的物件是不同的。

如果想透過建構函式注入,只需要在引數前面加上特性[FromKeyedServices("keyA")] 即可,特性裡的引數就是key的名字。如果想在建構函式中獲取key的值則使用特性[ServiceKey]。我們還可以註冊時把key設定為KeyedService.AnyKey(這是框架提供的類),只需使用任意非null值作為key就可以獲取物件。暫時不支援使用萬用字元匹配,也許以後會加......

class Bar : IBar
{
    public Bar([ServiceKey] int key, [FromKeyedServices("keyA")] IFoo foo, IServiceProvider root)
    {
        //注意:key的型別要和呼叫時一致。
        Console.WriteLine($"key:{key},Compare:{foo == root.GetKeyedService<IFoo>("keyA")}");
    }
}
public static class KeyedService
{
    /// Represents a key that matches any key.
    public static object AnyKey { get; } = new AnyKeyObj();
    private sealed class AnyKeyObj
    {
        public override string? ToString() => "*";
    }
}

深入理解

8.0之前,獲取一個物件需要用到的一個“標識”,比如呼叫GetService<IFoo>(),這個“標識”就是IFoo;也就是ServiceDescriptor裡面的ServiceType。而在8.0後“標識”變成了IFoo+"keyA",也就是ServiceDescriptor裡面的ServiceType+新增的ServiceKey

public class ServiceDescriptor
{
    public object? ServiceKey { get; } 
    public Type? ImplementationType => _implementationType;        
    public Type ServiceType { get; }
    public ServiceLifetime Lifetime { get; }

對於以前註冊的類,ServiceKey預設是null,所以“標識”就是ServiceKey+null。呼叫GetService<IFoo>()就等於呼叫GetKeyedService<IFoo>(null)

再舉一個例子:

//型別的註冊資訊放在_descriptorLookup,8.0前,是透過ServiceType作為字典的鍵,
//8.0是把ServiceIdentifier(也就是ServiceKey+ServiceType)作為字典的鍵
//7.0
private readonly Dictionary<Type, ServiceDescriptorCacheItem> _descriptorLookup = new();
//8.0
private readonly Dictionary<ServiceIdentifier, ServiceDescriptorCacheItem> _descriptorLookup = new();
internal readonly struct ServiceIdentifier : IEquatable<ServiceIdentifier>
{
    public object? ServiceKey { get; }
    public Type ServiceType { get; }
}

迴圈引用

前面講到可以透過[ServiceKey]獲取呼叫時的Key;而沒有註冊key的服務是無法在建構函式中注入key的值。透過這個功能可以解決迴圈引用的問題,先看程式碼。

class Foo : IFoo
{
    //這個建構函式給GetService<IFoo>()使用
    public Foo()
    {
        this.Num = 10;
    }
    //這個建構函式給GetKeyedServices<IFoo>("keyA")使用
    public Foo([ServiceKey] string key, IFoo foo)
    {
        Console.WriteLine($"key:{key},this.Num:{this.Num},foo.Num:{foo.Num}");
    }
    public int Num { get; set; }
}

程式碼執行流程:

  • 1.DI首先獲取Foo的所有建構函式並且按建構函式的引數從多到少進行排序
  • 2.遍歷所有建構函式,首先獲取引數最多的建構函式 Foo([ServiceKey] string key, IFoo foo),開始判斷建構函式的引數能否被DI建立
  • 3.DI首先判斷string key這個引數,能夠建立;然後繼續判斷第二個引數IFoo foo能否被建立
  • 4.重複第一步
  • 5.重複第二步
  • 6.DI首先判斷string key這個引數,不能夠建立;所以無法呼叫建構函式 Foo([ServiceKey] string key, IFoo foo)建立Foo例項
  • 7.繼續遍歷建構函式,第二個建構函式是無參的,DI能夠建立foo物件。
  • 8.對於GetKeyedServices<IFoo>("keyA"),是使用的這個建構函式Foo([ServiceKey] string key, IFoo foo)建立的物件。IFoo foo是使用無參建構函式建立的。

注意點:

  • 引數[ServiceKey] string key一定要寫在引數IFoo foo前面,否則就會迴圈引用
  • 註冊服務時,要註冊兩種(帶key的和不帶key的都要註冊)AddScoped<IFoo, Foo>() .AddKeyedScoped<IFoo, Foo>("keyA")

總結

以前的用法往往是介面對應實現類,透過DI獲取物件,只需要知道介面的名字,就可以透過GetService方法或者建構函式注入獲取物件。
現在是介面+key對應實現類,透過DI獲取物件,需要知道介面+key。如果key為null就和以前的用法一模一樣。
結束。第一次寫文章如有錯誤,歡迎各位批評指點,謝謝!

相關文章