ASP.NET Core 6框架揭秘實例演示[09]:將配置綁定為對象

我們傾向於將IConfiguration對象轉換成一個具體的對象,以面向對象的方式來使用配置,我們將這個轉換過程稱為配置綁定。除了將配置樹葉子節點配置節的綁定為某種標量對象外,我們還可以直接將一個配置節綁定為一個具有對應結構的符合對象。除此之外,配置綁定還支援針對數據、集合和字典類型的綁定。(本篇提供的實例已經匯總到《ASP.NET Core 6框架揭秘-實例演示版》)

[507]綁定配置項的值(源程式碼)
[508]類型轉換器在配置綁定中的應用(源程式碼)
[509]複合對象的配置綁定(源程式碼)
[510]集合的配置綁定(源程式碼)
[511]集合和數組的配置綁定的差異(源程式碼)
[512]字典的配置綁定(源程式碼)

[507]綁定配置項的值

最簡單配置綁定的莫過於針對配置樹葉子節點配置節的綁定。這樣的配置節承載著原子配置項的值,而且這個值是一個字元串,所以針對它的配置綁定最終體現為如何將這個字元串轉換成指定的目標類型,這樣的操作體現在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);
}

對於上面給出的這四個重載的GetValue方法,其中兩個方法提供了一個表示默認值的參數defaultValue,如果對應配置節的值為Null或者空字元串,那麼指定的默認值將作為方法的返回值。其他兩個重載實際上是將Null或者Default(T)作為默認值。這些GetValue方法會將配置節名稱(對應參數sectionKey)作為參數調用指定IConfiguration對象的GetSection方法得到表示對應配置節的IConfigurationSection對象,然後將它的Value屬性提取出來按照如下規則轉換成目標類型。

  • 如果目標類型為object,那麼直接返回原始值(字元串或者Null)。
  • 如果目標類型不是Nullable<T>,那麼針對目標類型的TypeConverter將被用來完成類型轉換。
  • 如果目標類型為Nullable<T>,在原始值不是Null或者空字元串的情況下會直接返回Null,否則會按照上面的規則將值轉換成類型基礎T。

為了驗證上述這些類型轉化規則,我們編寫了如下測試程式。如程式碼片段所示,我們利用註冊的MemoryConfigurationSource添加了三個配置項,對應的值分別為Null、空字元串和「123」。在將IConfiguration對象構建出來後,我們調用它的GetValue<T>將三個值轉換成Object、Int32和Nullable<Int32>類型。

using Microsoft.Extensions.Configuration;
using System.Diagnostics;

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);

[508]類型轉換器在配置綁定中的應用

按照前面介紹的類型轉換規則,如果目標類型支援源自字元串的類型轉換,就能夠將配置項的原始值綁定為該類型的對象。在下面的程式碼片段中,我們定義了一個表示二維坐標的Point記錄(Record),並且為它註冊了一個針對PointTypeConverter的類型轉換器。PointTypeConverter通過實現的ConvertFrom方法將坐標的字元串表達式(如「123」和「456」)轉換成一個Point對象。

[TypeConverter(typeof(PointTypeConverter))]
public readonly record struct Point(double X, double Y);

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)
    {
        var split = (value.ToString() ?? "0.0,0.0").Split(',');
        double x = double.Parse(split[0].Trim().TrimStart('('));
        double y = double.Parse(split[1].Trim().TrimEnd(')'));
        return new Point(x,y);
    }
}

由於定義的Point類型支援源自字元串的類型轉換,所以如果配置項的原始值(字元串)具有與之兼容的格式,我們就可以按照如下方式將其綁定為一個Point對象。

using App;
using Microsoft.Extensions.Configuration;
using System.Diagnostics;

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);

[509]複合對象的配置綁定

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

public readonly record struct Profile(Gender Gender, int Age, ContactInfo ContactInfo);
public readonly record struct ContactInfo(string EmailAddress, string PhoneNo);
public enum Gender
{
    Male,
    Female
}

上面的程式碼片段定義了一個表示個人基本資訊的Profile記錄,它的Gender、Age和ContactInfo屬性分別表示性別、年齡和聯繫方式。表示聯繫方式的ContactInfo記錄定義了EmailAddress和PhoneNo屬性,分別表示電子郵箱地址和電話號碼。一個完整的Profile對象可以通過圖1所示的樹來體現。

6-13

圖1 複雜對象的配置樹

如果需要通過配置的形式表示一個完整的Profile對象,只需要提供四個葉子節點(性別、年齡、電子郵箱地址和電話號碼)對應的配置數據,配置字典只需要按照表1來存儲這四個鍵值對就可以了。

表1 針對複雜對象的配置數據結構

Key

Value

Gender

Male

Age

18

ContactInfo:Email

[email protected]

ContactInfo:PhoneNo

123456789

我們通過下面的程式來驗證針對複合數據類型的綁定。我們先創建一個ConfigurationBuilder對象,並利用註冊的MemoryConfigurationSource對象添加了表5-2所示的配置數據。在構建出IConfiguration對象之後,我們調用它的Get<T>擴展方法將其綁定為Profile對象。

using App;
using Microsoft.Extensions.Configuration;
using System.Diagnostics;

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.Gender == Gender.Male);
Debug.Assert(profile.Age == 18);
Debug.Assert(profile.ContactInfo.EmailAddress == "[email protected]");
Debug.Assert(profile.ContactInfo.PhoneNo == "123456789");

[510]集合的配置綁定

如果配置綁定的目標類型是一個集合(包括數組),那麼當前IConfiguration對象的每個子配置節將綁定為集合的元素。如果目標類型為元素類型為Profile的集合,那麼配置樹應該具有圖2所示的結構。既然能夠正確地將集合對象通過一個合法的配置樹體現出來,那麼就可以將它轉換成配置字典

6-14
圖2 集合對象的配置樹

我們利用如下的實例來演示針對集合的配置綁定。如程式碼片段所示,我們創建了一個ConfigurationBuilder對象,並為它註冊了一個MemoryConfigurationSource對象,並利用註冊的MemoryConfigurationSource對象添加了配置數據。在構建出IConfiguration對象之後,我們調用它的Get<T>擴展方法將它分別綁定為一個IList<Profile>和Profile數組對象。

using App;
using Microsoft.Extensions.Configuration;
using System.Diagnostics;

var source = new Dictionary<string, string>
{
    ["0:gender"] 			= "Male",
    ["0:age"] 			= "18",
    ["0:contactInfo:emailAddress"] 	= "[email protected]",
    ["0:contactInfo:phoneNo"] 		= "123",

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

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

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

var list = configuration.Get<IList<Profile>>();
Debug.Assert(list[0].ContactInfo.EmailAddress == "[email protected]");
Debug.Assert(list[1].ContactInfo.EmailAddress == "[email protected]");
Debug.Assert(list[2].ContactInfo.EmailAddress == "[email protected]");

var array = configuration.Get<Profile[]>();
Debug.Assert(array[0].ContactInfo.EmailAddress == "[email protected]");
Debug.Assert(array[1].ContactInfo.EmailAddress == "[email protected]");
Debug.Assert(array[2].ContactInfo.EmailAddress == "[email protected]");

[511]集合和數組的配置綁定的差異

針對集合的配置綁定不會因為某個元素的綁定失敗而終止。如果目標類型是數組,最終綁定生成的數組長度與子配置節的個數總是一致的。如果目標類型是列表,將不會生成對應的元素。我們將上面演示程式做了稍許的修改,將第一個元素的性別從「Male」改為「男」,那麼針對這個Profile元素綁定將會失敗。如果將目標類型設置為IEnumerable<Profile>,那麼最終生成的集合只有兩個元素。倘若目標類型切換成Profile數組,數組的長度依然為3,但是第一個元素是空。

using App;
using Microsoft.Extensions.Configuration;
using System.Diagnostics;

var source = new Dictionary<string, string>
{
    ["0:gender"] 			= "男",
    ["0:age"] 				= "18",
    ["0:contactInfo:emailAddress"] 	= "[email protected]",
    ["0:contactInfo:phoneNo"] 		= "123",

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

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

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

var list = configuration.Get<IList<Profile>>();
Debug.Assert(list.Count == 2);
Debug.Assert(list[0].ContactInfo.EmailAddress == "[email protected]");
Debug.Assert(list[1].ContactInfo.EmailAddress == "[email protected]");

var array = configuration.Get<Profile[]>();
Debug.Assert(array.Length == 3);
Debug.Assert(array[0] == default);
Debug.Assert(array[1].ContactInfo.EmailAddress == "[email protected]");
Debug.Assert(array[2].ContactInfo.EmailAddress == "[email protected]");

[512]字典的配置綁定

能夠通過配置綁定生成的字典是一個實現了IDictionary<string,T>的類型,它Key必須是一個字元串(或者枚舉)。如果採用配置樹的形式表示這樣一個字典對象,就會發現它與針對集合的配置樹在結構上幾乎是一樣的,唯一的區別是集合元素的索引直接變成字典元素的Key。也就是說,圖2所示的配置樹同樣可以表示成一個具有三個元素的Dictionary<string, Profile>對象,它們對應的Key分別「0」、「1」和「2」,所以我們可以按照如下方式將承載相同結構數據的IConfiguration對象綁定為一個IDictionary<string, Profile >對象。如程式碼片段所示,我們將表示集合索引的整數(「0」、「1」和「2」)改成普通的字元串(「foo」、「bar」和「baz」)。

using App;
using Microsoft.Extensions.Configuration;
using System.Diagnostics;

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 profiles = new ConfigurationBuilder()
    .AddInMemoryCollection(source)
    .Build()
    .Get<IDictionary<string,Profile>>();;

Debug.Assert(profiles["foo"].ContactInfo.EmailAddress == "[email protected]");
Debug.Assert(profiles["bar"].ContactInfo.EmailAddress == "[email protected]");
Debug.Assert(profiles["baz"].ContactInfo.EmailAddress == "[email protected]");