[小菜一碟] 如何讀取自訂 ConfigurationElement 節點內的設定值?

在 .NET Framework 中,無論是 App.ConfigWeb.Config,均有保留 <configSections> 讓我們可以自訂設定區塊(ConfigurationSection),由於曾經看過有一些 Library 把設定值放在節點之中,像這樣:

等到要自己弄的時候才發現,似乎沒有那麼簡單,網路上搜尋到的有關於自訂 ConfigurationSection 的文章,大都沒有提到這一塊。

幸好,不是只有我一個人有這樣的問題,Stack Overflow 上的這個問題替我提供了一個方向,假定我的 ConfigurationSection 長這樣,有 Attributes 也有 Elements:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="MySection" type="WindowsFormsApp5.MySection, WindowsFormsApp5" />
  </configSections>
  ...
  <MySection>
    <MyElement Id="1" Name="Johnny">
      <Age>20</Age>
      <Address>Taipei</Address>
    </MyElement>
  </MySection>
</configuration>

如果我們什麼都不做的話,讀取設定的時候會收到一個例外 屬性 'xxx' 不是 ConfigurationElement。

覆寫 DeserializeElement()

要讀取 AgeAddress 的值,我們需要去覆寫 DeserializeElement() 方法,自行指定反序列化的方式,下面的程式碼就以 MyElement 為例,一一地將所需的設定值讀取進來。

public class MyElement : ConfigurationElement
{
    [ConfigurationProperty("Id")]
    public int Id
    {
        get => (int)this[nameof(this.Id)];
        set => this[nameof(this.Id)] = value;
    }

    [ConfigurationProperty("Name")]
    public string Name
    {
        get => (string)this[nameof(this.Name)];
        set => this[nameof(this.Name)] = value;
    }

    [ConfigurationProperty("Age")]
    public int Age
    {
        get => (int)this[nameof(this.Age)];
        set => this[nameof(this.Age)] = value;
    }

    [ConfigurationProperty("Address")]
    public string Address
    {
        get => (string)this[nameof(this.Address)];
        set => this[nameof(this.Address)] = value;
    }

    protected override void DeserializeElement(XmlReader reader, bool serializeCollectionKey)
    {
        while (reader.MoveToNextAttribute())
        {
            SetAttributeValue();
        }

        while (reader.Read())
        {
            if (reader.NodeType == XmlNodeType.Element)
            {
                SetElementValue();
            }
            else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "MyElement")
            {
                break;
            }
        }

        void SetAttributeValue()
        {
            switch (reader.Name)
            {
                case nameof(this.Id):
                    this[reader.Name] = reader.ReadContentAsInt();
                    break;

                default:
                    this[reader.Name] = reader.ReadContentAsString();
                    break;
            }
        }

        void SetElementValue()
        {
            switch (reader.Name)
            {
                case nameof(this.Age):
                    this[reader.Name] = reader.ReadElementContentAsInt();
                    break;

                default:
                    this[reader.Name] = reader.ReadElementContentAsString();
                    break;
            }
        }
    }
}

撰寫通用的 ConfigurationElement

如果我們應用程式的設定單純,只有單一種結構的 ConfigurationElement,直接在 DeserializeElement() 方法裡面 Hardcode 也就完了,但是當有第二種、第三種結構出現的時候,我們會需要將覆寫過的 DeserializeElement() 方法給通用化。

由於設定在讀取一次之後就會快取起來,所以我們暫不考慮效能,用 Reflection(反射)來做最快,建立一個 ElementalConfigurationElement 抽象類別,把 DeserializeElement() 改寫一下。

public abstract class ElementalConfigurationElement : ConfigurationElement
{
    protected override void DeserializeElement(XmlReader reader, bool serializeCollectionKey)
    {
        var type = this.GetType();
        var properties = type.GetProperties().ToDictionary(p => p.Name, p => p);

        while (reader.MoveToNextAttribute())
        {
            if (!properties.TryGetValue(reader.Name, out var property)) continue;

            this[reader.Name] = reader.ReadContentAs(property.PropertyType, null);
        }

        while (reader.Read())
        {
            if (reader.NodeType == XmlNodeType.Element && properties.TryGetValue(reader.Name, out var property))
            {
                this[reader.Name] = reader.ReadElementContentAs(property.PropertyType, null);
            }
            else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == type.Name)
            {
                break;
            }
        }
    }
}

改這樣之後,只要有從 Element 內容取值的需求,改繼承 ElementalConfigurationElement 就可以了,以上方式提供給有需要朋友做參考。

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學