ASP.NET Core 3框架揭秘] 配置[8]:多樣化的配置源[下篇]


XML也是一種常用的配置定義形式,它對數據的表達能力甚至強於JSON,幾乎所有類型的數據結構都可以通過XML表示出來。當我們通過一個XML元素表示一個復雜對象的時候,對象的數據成員定義成當前XML元素的子元素。雖然XML對數據結構的表達能力總體要強於JSON,但是作為配置模型的數據來源卻有自己的局限性,比如它們對集合的表現形式有點不盡如人意。

一、XML在針對集合表達上的缺陷

舉個簡單的例子,對於一個元素類型為Profile的集合,我們可以采用具有如下結構的XML來表現。

<Profiles>
    <Profile Gender="Male" Age="18">
        <ContactInfo EmailAddress ="foo@outlook.com" PhoneNo="123"/>
    </Profile>
    <Profile Gender="Male" Age="25">
        <ContactInfo EmailAddress ="bar@outlook.com" PhoneNo="456"/>
    </Profile>
    <Profile Gender="Male" Age="36">
        <ContactInfo EmailAddress ="baz@outlook.com" PhoneNo="789"/>
    </Profile>
</Profiles>

但是這段XML卻不能正確地轉換成配置字典,原因很簡單,因為字典的Key必須是唯一的,這必然要求最終構成配置樹的每個節點必須具有不同的路徑。上面這段XML很明顯不滿足這個基本的要求,因為表示一個Profile對象的三個XML元素(<Profile>...</Profile>)是“同質”的,對於由它們表示的三個Profile對象來說,分別表示性別、年齡、電子郵箱地址和電話號碼的四個葉子節點的路徑是完全一樣的,所以根本無法作為配置字典的Key。通過前面針對配置綁定的介紹我們知道,如果需要通過配置字典來表示一個Profile對象的集合,我們需要按照如下的方式為每個集合元素加上相應的索引(“foo”、“bar”和“baz”)。

foo:Gender
foo:Age
foo:ContactInfo:EmailAddress
foo:ContactInfo:PhoneNo

bar:Gender
bar:Age
bar:ContactInfo:EmailAddress
bar:ContactInfo:PhoneNo

baz:Gender
baz:Age
baz:ContactInfo:EmailAddress
baz:ContactInfo:PhoneNo

二、通過自定義IConfigurationSource解決問題

之所以XML不能像JSON格式那樣可以以一種很自然的形式表示集合或者數組,是因為后者對這兩種數據類型提供了明確的定義方式(采用中括號定義),但是XML只有子元素的概念,我們不能確定它的子元素是否是一個集合。如果做這樣一個假設:如果同一個XML元素下的所有子元素都具有相同的名稱,那么我們可以將其視為集合。根據這么一個假設,我們對XmlConfigurationSource略加改造就可以解決XML難以表示集合數據結構的問題。

我們通過派生XmlConfigurationSource創建一個新的IConfigurationSource實現類型,姑且將其命名為ExtendedXmlConfigurationSource。XmlConfigurationSource提供的ConfigurationProvdier類型為ExtendedXmlConfigurationProvider,它派生於XmlConfigurationProvider。在重寫的Load方法中,ExtendedXmlConfigurationProvider通過對原始的XML結構進行相應的改動,從而讓原本不合法的XML(XML元素具有相同的名稱)可以轉換成一個針對集合的配置字典 。下圖展示了XML結構轉換采用的規則和步驟。

6-18

如上圖所示,針對集合對原始XML所作的結構轉換由兩個步驟組成。第一步為表示集合元素的XML元素添加一個名為“append_index”的屬性(Attribute),我們采用零基索引作為該屬性的值。第二步會根據第一步轉換的結果創建一個新的XML,同名的集合元素(比如<profile>)將會根據添加的索引值重新命名(比如<profile_index_0>)。毫無疑問,轉換后的這個XML可以很好地表示一個集合對象。如下所示的是ExtendedXmlConfigurationProvider對象的定義,上述的這個轉換邏輯體現在重寫的Load方法中。

public class ExtendedXmlConfigurationProvider : XmlConfigurationProvider
{
    public ExtendedXmlConfigurationProvider(XmlConfigurationSource source) :  base(source)
    {}

    public override void Load(Stream stream)
    {
        //加載源文件並創建一個XmlDocument        
        var sourceDoc = new XmlDocument();
        sourceDoc.Load(stream);

        //添加索引
        AddIndexes(sourceDoc.DocumentElement);

        //根據添加的索引創建一個新的XmlDocument
        var newDoc = new XmlDocument();
        var documentElement =  newDoc.CreateElement(sourceDoc.DocumentElement.Name);
        newDoc.AppendChild(documentElement);

        foreach (XmlElement element in sourceDoc.DocumentElement.ChildNodes)
        {
            Rebuild(element, documentElement, name => newDoc.CreateElement(name));
        }

        //根據新的XmlDocument初始化配置字典
        using (Stream newStream = new MemoryStream())
        {
            using (XmlWriter writer = XmlWriter.Create(newStream))
            {
                newDoc.WriteTo(writer);
            }
            newStream.Position = 0;
            base.Load(newStream);
        }
    }

    private void AddIndexes(XmlElement element)
    {
        if (element.ChildNodes.OfType<XmlElement>().Count() > 1)
        {
            if (element.ChildNodes.OfType<XmlElement>().GroupBy(it => it.Name).Count() == 1)
            {
                var index = 0;
                foreach (XmlElement subElement in element.ChildNodes)
                {
                    subElement.SetAttribute("append_index", (index++).ToString());
                    AddIndexes(subElement);
                }
            }
        }
    }

    private void Rebuild(XmlElement source, XmlElement destParent, Func<string, XmlElement> creator)
    {
        var index = source.GetAttribute("append_index");
        var elementName = string.IsNullOrEmpty(index)  ? source.Name : $"{source.Name}_index_{index}";
        var element = creator(elementName);
        destParent.AppendChild(element);
        foreach (XmlAttribute attribute in source.Attributes)
        {
            if (attribute.Name != "append_index")
            {
                element.SetAttribute(attribute.Name, attribute.Value);
            }
        }

        foreach (XmlElement subElement in source.ChildNodes)
        {
            Rebuild(subElement, element, creator);
        }
    }
}

為了能夠將上面這個XmlConfigurationProvider對象應用到我們的程序中,我們需要為它定義相應的IConfigurationSource對象,為此我們定義了如下這個ExtendedXmlConfigurationSource類型。它直接繼承自XmlConfigurationSource對象,並在重寫的Build方法中提供上面這個ExtendedXmlConfigurationProvider對象。為了方便將這個ExtendedXmlConfigurationSource對象注冊到IConfigurationBuilder對象上,我們也可以進一步定義如下這些擴展方法。

public class ExtendedXmlConfigurationSource : XmlConfigurationSource
{
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        EnsureDefaults(builder);
        return new ExtendedXmlConfigurationProvider(this);
    }
}

public static class ExtendedXmlConfigurationExtensions
{
    public static IConfigurationBuilder AddExtendedXmlFile( this IConfigurationBuilder builder, string path)=> builder.AddExtendedXmlFile(path, false, false);
    public static IConfigurationBuilder AddExtendedXmlFile( this IConfigurationBuilder builder, string path, bool optional) => builder.AddExtendedXmlFile(path, optional, false);
    public static IConfigurationBuilder AddExtendedXmlFile(tthis IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange)
    {
        builder.Add(new ExtendedXmlConfigurationSource  { Path = path, Optional = optional, ReloadOnChange = reloadOnChange });
        return builder;
    }
}

[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]:自定義配置源


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM