1. 程式人生 > >[ASP.NET Core 3框架揭祕] 配置[4]:將配置繫結為物件

[ASP.NET Core 3框架揭祕] 配置[4]:將配置繫結為物件

雖然應用程式可以直接利用通過IConfigurationBuilder物件建立的IConfiguration物件來提取配置資料,但是我們更傾向於將其轉換成一個POCO物件,以面向物件的方式來使用配置,我們將這個轉換過程稱為配置繫結。配置繫結可以通過如下幾個針對IConfiguration的擴充套件方法來實現,這些擴充套件方法都定義在NuGet包“Microsoft.Extensions.Configuration.Binder”中。

一、ConfigurationBinder

public static class ConfigurationBinder
{    
    public static void Bind(this IConfiguration configuration, object instance);
    public static void Bind(this IConfiguration configuration, object instance,  Action<BinderOptions> configureOptions);
    public static void Bind(this IConfiguration configuration, string key,  object instance);
   
    public static T Get<T>(this IConfiguration configuration);
    public static T Get<T>(this IConfiguration configuration,  Action<BinderOptions> configureOptions);
    public static object Get(this IConfiguration configuration, Type type);
    public static object Get(this IConfiguration configuration, Type type,  Action<BinderOptions> configureOptions);
}

public class BinderOptions
{
    public bool BindNonPublicProperties { get; set; }
}

Bind方法將指定的IConfiguration物件(對應於configuration引數)繫結一個預先建立的物件(對應於instance引數),如果引數繫結的只是當前IConfiguration物件的某個子配置節,我們需要通過引數sectionKey指定對應子配置節的相對路徑。Get和Get<T>方法則直接將指定的IConfiguration物件轉換成指定型別的POCO物件。

旨在生成POCO物件的配置繫結實現在IConfiguration介面的擴充套件方法Bind上。配置繫結的目標型別可以是一個簡單的基元型別,也可以是一個自定義資料型別,還可以是一個數組、集合或者字典型別。通過前面的介紹我們知道IConfigurationProvider物件將原始的配置資料讀取出來後會將其轉換成Key和Value均為字串的資料字典,那麼針對這些完全不同的目標型別,原始的配置資料如何通過資料字典的形式來體現呢?

二、繫結配置項的值

我們知道配置模型採用字串鍵值對的形式來承載基礎配置資料,我們將這組鍵值對稱為配置字典,扁平的字典因為採用路徑化的Key使配置項在邏輯上具有了層次結構。IConfigurationBuilder物件將配置的層次化結構體現在由它建立的IConfigurationRoot物件上,我們將IConfigurationRoot物件視為一棵配置樹。所謂的配置繫結體現為如何將對映為配置樹上某個節點的IConfiguration物件(可以是IConfigurationRoot物件或者IConfigurationSection物件)轉換成一個對應的POCO物件。

對於針對IConfiguration物件的配置繫結來說,最簡單的莫過於針對葉子節點的IConfigurationSection物件的繫結。表示配置樹葉子節點的IConfigurationSection物件承載著原子配置項的值,而且這個值是一個字串,那麼針對它的配置繫結最終體現為如何將這個字串轉換成指定的目標型別,這樣的操作體現在IConfiguration如下兩個擴充套件方法GetValue上。

public static class ConfigurationBinder
{    
    public static T GetValue<T>(IConfiguration configuration, string sectionKey);
    public static T GetValue<T>(IConfiguration configuration, string sectionKey,  T defaultValue);
    public static object GetValue(IConfiguration configuration, Type type,   string sectionKey);
    public static object GetValue(IConfiguration configuration, Type type,   string sectionKey, object defaultValue);
}

對於給出的這四個過載,其中兩個方法定義了一個表示預設值的defaultValue引數,如果對應配置節的值為Null或者空字串,指定的預設值將作為方法的返回值。對於其他的方法過載,它們實際上將Null或者Default(T)作為隱式預設值。上述這些GetValue方法被執行的時候,它們會將配置節名稱(對應sectionKey引數)作為引數呼叫指定IConfiguation物件的GetSection方法得到表示對應配置節的IConfigurationSection物件,它的Value屬性被提取出來並按照如下的邏輯轉換成目標型別:

  • 如果目標型別為object,直接返回原始值(字串或者Null)。
  • 如果目標型別不是Nullable<T>,那麼針對目標型別的TypeConverter將被用來做型別轉換。
  • 如果目標型別為Nullable<T>,那麼在原始值不為Null或者空字串的情況下會將基礎型別T作為新的目標型別進行轉換,否則直接返回Null。

為了驗證上述這些型別轉化規則,我們編寫了如下的測試程式。如下面的程式碼片段所示,我們利用註冊的MemoryConfigurationSource添加了三個配置項,對應的值分別為Null、空字串和“123”,然後呼叫GetValue方法分別對它們進行型別轉換,轉換的目標型別分別是Object、Int32和Nullable<Int32>,上述的轉換規則體現在對應的除錯斷言中。

public class Program
{
    public static void Main()
    {
        var source = new Dictionary<string, string>
        {
            ["foo"] = null,
            ["bar"] = "",
            ["baz"] = "123"
        };

        var root = new ConfigurationBuilder()
            .AddInMemoryCollection(source)
            .Build();

        //針對object
         Debug.Assert(root.GetValue<object>("foo") == null);
        Debug.Assert("".Equals(root.GetValue<object>("bar")));
        Debug.Assert("123".Equals(root.GetValue<object>("baz")));

        //針對普通型別
         Debug.Assert(root.GetValue<int>("foo") == 0);
        Debug.Assert(root.GetValue<int>("baz") == 123);

        //針對Nullable<T>
        Debug.Assert(root.GetValue<int?>("foo") == null);
        Debug.Assert(root.GetValue<int?>("bar") == null);
    }
}

三、自定義TypeConverter

按照前面介紹的型別轉換規則,如果目標型別支援源自字串的型別轉換,那麼我們就能夠將配置項的原始值繫結為該型別的物件,而讓某個型別支援某種型別轉換規則的途徑就是為之註冊相應的TypeConverter。如下面的程式碼片段所示,我們定義了一個表示二維座標的Point物件,併為它註冊了一個型別為PointTypeConverter的TypeConverter,PointTypeConverter通過實現的ConvertFrom方法將座標的字串表示式(比如“(123,456)”)轉換成一個Point物件。

[TypeConverter(typeof(PointTypeConverter))]
public class Point
{
    public double X { get; set; }
    public double Y { get; set; }
}

public class PointTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => sourceType == typeof(string);

    public override object ConvertFrom(ITypeDescriptorContext context,  CultureInfo culture, object value)
    {
        string[] split = value.ToString().Split(',');
        double x = double.Parse(split[0].Trim().TrimStart('('));
        double y = double.Parse(split[1].Trim().TrimEnd(')'));
        return new Point { X = x, Y = y };
    }
}

由於定義的Point型別支援源自字串的型別轉換,所以如果配置項的原始值(字串)具有與之相容的格式,我們將能按照如下的方式將它繫結為一個Point物件。(S608)

public class Program
{
    public static void Main()
    {
        var source = new Dictionary<string, string>
        {
            ["point"] = "(123,456)"
        };

        var root = new ConfigurationBuilder()
            .AddInMemoryCollection(source)
            .Build();

        var point = root.GetValue<Point>("point");
        Debug.Assert(point.X == 123);
        Debug.Assert(point.Y == 456);
    }
}

四、繫結複合資料型別

這裡所謂的複合型別表示一個具有屬性資料成員的自定義型別。如果通過一顆樹來表示一個複合物件,那麼葉子節點承載所有的資料,並且葉子節點的資料型別均為基元型別。如果通過資料字典來提供一個複雜物件所有的原始資料,那麼這個字典中只需要包含葉子節點對應的值即可。至於如何通過一個字典物件體現複合物件的結構,我們只需要將葉子節點所在的路徑作為字典元素的Key就可以了。

public class Profile: IEquatable<Profile>
{
    public Gender Gender { get; set; }
    public int Age { get; set; }
    public ContactInfo ContactInfo { get; set; }

    public Profile() {}
    public Profile(Gender gender, int age, string emailAddress, string phoneNo)
    {
        Gender = gender;
        Age = age;
        ContactInfo = new ContactInfo
        {
            EmailAddress = emailAddress,
            PhoneNo = phoneNo
        };
    }       
    public bool Equals(Profile other)
    {
        return other == null
            ? false
            : Gende == other.Gender &&  Age == other.Age && ContactInfo.Equals(other.ContactInfo);
    }
}

public class ContactInfo: IEquatable<ContactInfo>
{
    public string EmailAddress { get; set; }
    public string PhoneNo { get; set; }  
    public bool Equals(ContactInfo other)
    {
        return other == null
           ? false
           : EmailAddress == other.EmailAddress && PhoneNo == other.PhoneNo;
    }
}

public enum Gender
{
    Male,
    Female
}

如上面的程式碼片段所示,我們定義了一個表示個人基本資訊的Profile類,它的三個屬性(Gender、Age和ContactInfo)分別表示性別、年齡和聯絡方式。由於配置繫結會呼叫預設無參建構函式來建立繫結的目標物件,所以我們需要為Profile型別定義一個預設建構函式。表示聯絡資訊的ContactInfo型別具有兩個屬性(EmailAddress和PhoneNo),它們分別表示電子郵箱地址和電話號碼。一個完整的Profile物件可以通過如下圖所示的樹來體現。

如果需要通過配置的形式來表示一個完整的Profile物件,我們只需要將四個葉子節點(性別、年齡、電子郵箱地址和電話號碼)對應的資料由配置來提供即可。對於承載配置資料的資料字典,我們需要按照如下表所示的方式將這四個葉子節點的路徑作為字典元素的Key。

Key Value
Gender Male
Age 18
ContactInfo:Email [email protected]
ContactInfo:PhoneNo 123456789

我們通過如下的程式來驗證針對複合資料型別的配置繫結。我們建立了一個ConfigurationBuilder物件併為它添加了一個MemoryConfigurationSource物件,它按照如上表所示的結構提供了原始的配置資料。在呼叫Build方法構建出IConfiguration物件之後,我們直接呼叫擴充套件方法Get<T>將它轉換成一個Profile物件。

public class Program
{
    public static void Main()
    {
        var source = new Dictionary<string, string>
        {
            ["gender"]  = "Male",
            ["age"]  = "18",
            ["contactInfo:emailAddress"]  = "[email protected]",
            ["contactInfo:phoneNo"]  = "123456789"
        };

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(source)
            .Build();

        var profile = configuration.Get<Profile>();
        Debug.Assert(profile.Equals( new Profile(Gender.Male, 18, "[email protected]", "123456789")));
    }
}

五、繫結集合物件

如果配置繫結的目標型別是一個集合(包括陣列),那麼當前IConfiguration物件的每一個子配置節將繫結為集合的元素。假設我們需要將一個IConfiguration物件繫結為一個元素型別為Profile的集合,它表示的配置樹應該具有如下圖所示的結構。

既然我們能夠正確將集合物件通過一個合法的配置樹體現出來,那麼我們就可以將它轉換成配置字典。對於通過下表所示的這個包含三個元素的Profile集合,我們可以採用如下表所示的結構來定義對應的配置字典。

Key Value
foo:Gender Male
foo:Age 18
foo:ContactInfo:EmailAddress [email protected]
foo:ContactInfo:PhoneNo 123
bar:Gender Male
bar:Age 25
bar:ContactInfo:EmailAddress [email protected]
bar:ContactInfo:PhoneNo 456
baz:Gender Female
baz:Age 40
baz:ContactInfo:EmailAddress [email protected]
baz:ContactInfo:PhoneNo 789

我們依然通過一個簡單的例項來演示針對集合的配置繫結。如下面的程式碼片段所示,我們建立了一個ConfigurationBuilder物件併為它註冊了一個MemoryConfigurationSource物件,它按照如s上表所示的結構提供了原始的配置資料。在得到這個ConfigurationBuilder物件建立的IConfiguration物件之後,我們兩次呼叫其Get<T>方法將它分別繫結為一個IEnumerable<Profile>物件和一個Profile[] 陣列。由於IConfigurationProvider通過GetChildKeys方法提供的Key是經過排序的,所以在繫結生成的集合或者陣列中的元素的順序與配置源是不相同的,如下的除錯斷言也體現了這一點。

public class Program
{
    public static void Main()
    {
        var source = new Dictionary<string, string>
        {
            ["foo:gender"]  = "Male",
            ["foo:age"]      = "18",
            ["foo:contactInfo:emailAddress"]  = "[email protected]",
            ["foo:contactInfo:phoneNo"] = "123",

            ["bar:gender"]  = "Male",
            ["bar:age"] = "25",
            ["bar:contactInfo:emailAddress"]  = "[email protected]",
            ["bar:contactInfo:phoneNo"]  = "456",

            ["baz:gender"]  = "Female",
            ["baz:age"] = "36",
            ["baz:contactInfo:emailAddress"]  = "[email protected]",
            ["baz:contactInfo:phoneNo"]  = "789"
        };

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(source)
            .Build();

        var profiles = new Profile[]
        {
            new Profile(Gender.Male,18,"[email protected]","123"),
            new Profile(Gender.Male,25,"[email protected]","456"),
            new Profile(Gender.Female,36,"[email protected]","789"),
        };

        var collection = configuration.Get<IEnumerable<Profile>>();
        Debug.Assert(collection.Any(it => it.Equals(profiles[0])));
        Debug.Assert(collection.Any(it => it.Equals(profiles[1])));
        Debug.Assert(collection.Any(it => it.Equals(profiles[2])));

        var array = configuration.Get<Profile[]>();
        Debug.Assert(array[0].Equals(profiles[1]));
        Debug.Assert(array[1].Equals(profiles[2]));
        Debug.Assert(array[2].Equals(profiles[0]));
    }
}

在針對集合型別的配置繫結過程中,如果某個配置節繫結失敗,該配置節將被忽略並選擇下一個配置節繼續進行繫結。但是如果目標型別為陣列,最終繫結生成的陣列長度與子配置節的個數總是一致的,繫結失敗的元素將被設定為Null。比如我們將上面的程式作了如下的改寫,儲存原始配置的字典物件包含兩個元素,第一個元素的性別從“Male”改為“男”,毫無疑問這個值是不可能轉換成Gender列舉物件的,所以針對這個Profile的配置繫結會失敗。如果將目標型別設定為IEnumerable<Profile>,那麼最終生成的集合只會有兩個元素,倘若目標型別切換成Profile陣列,陣列的長度依然為3,但是第一個元素是Null。

public class Program
{
    public static void Main()
    {
        var source = new Dictionary<string, string>
        {
            ["foo:gender"] = "男",
            ["foo:age"]      = "18",
            ["foo:contactInfo:emailAddress"] = "[email protected]",
            ["foo:contactInfo:phoneNo"] = "123",

            ["bar:gender"]  = "Male",
            ["bar:age"]      = "25",
            ["bar:contactInfo:emailAddress"]  = "[email protected]",
            ["bar:contactInfo:phoneNo"]  = "456",

            ["baz:gender"]  = "Female",
            ["baz:age"]      = "36",
            ["baz:contactInfo:emailAddress"]  = "[email protected]",
            ["baz:contactInfo:phoneNo"]  = "789"
        };

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(source)
            .Build();

        var collection = configuration.Get<IEnumerable<Profile>>();
        Debug.Assert(collection.Count() == 2);

        var array = configuration.Get<Profile[]>();
        Debug.Assert(array.Length == 3);
        Debug.Assert(array[2] == null); 
        //由於配置節按照Key進行排序,繫結失敗的配置節為最後一個
    }
}

六、繫結字典

能夠通過配置繫結生成的字典是一個實現了IDictionary<string,T>的型別,也就是說配置模型沒有對字典的Value型別作任何要求,但是字典物件的Key必須是一個字串(或者列舉)。如果採用配置樹的形式來表示這麼一個字典物件,我們會發現它與針對集合的配置樹在結構上幾乎是一樣的。唯一的區別是集合元素的索引直接變成了字典元素的Key。

也就是說上圖所示的這棵配置樹同樣可以表示成一個具有三個元素的Dictionary<string, Profile>物件 ,它們對應的Key分別是“Foo”、“Bar”和“Baz”,所以我們可以按照如下的方式將承載相同資料的IConfiguration物件繫結為一個IDictionary<string,T>物件。(S612)

public class Program
{
    public static void Main()
    {
        var source = new Dictionary<string, string>
        {
            ["foo:gender"]  = "Male",
            ["foo:age"]      = "18",
            ["foo:contactInfo:emailAddress"]  = "[email protected]",
            ["foo:contactInfo:phoneNo"] = "123",

            ["bar:gender"]  = "Male",
            ["bar:age"]      = "25",
            ["bar:contactInfo:emailAddress"]  = "[email protected]",
            ["bar:contactInfo:phoneNo"]  = "456",

            ["baz:gender"]  = "Female",
            ["baz:age"]        = "36",
            ["baz:contactInfo:emailAddress"]  = "[email protected]",
            ["baz:contactInfo:phoneNo"]  = "789"
        };

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(source)
            .Build();

        var profiles = configuration.Get<IDictionary<string, Profile>>();
        Debug.Assert(profiles["foo"].Equals(  new Profile(Gender.Male, 18, "[email protected]", "123")));
        Debug.Assert(profiles["bar"].Equals(  new Profile(Gender.Male, 25, "[email protected]", "456")));
        Debug.Assert(profiles["baz"].Equals(  new Profile(Gender.Female, 36, "[email protected]", "789")));
    }
}

[ASP.NET Core 3框架揭祕] 配置[1]:讀取配置資料[上篇]
[ASP.NET Core 3框架揭祕] 配置[2]:讀取配置資料[下篇]
[ASP.NET Core 3框架揭祕] 配置[3]:配置模型總體設計
[ASP.NET Core 3框架揭祕] 配置[4]:將配置繫結為物件
[ASP.NET Core 3框架揭祕] 配置[5]:配置資料與資料來源的實時同步
[ASP.NET Core 3框架揭祕] 配置[6]:多樣化的配置源[上篇]
[ASP.NET Core 3框架揭祕] 配置[7]:多樣化的配置源[中篇]
[ASP.NET Core 3框架揭祕] 配置[8]:多樣化的配置源[下篇]
[ASP.NET Core 3框架揭祕] 配置[9]:自定義配