C# - App.config自訂section程式碼架構Part II (巢狀, 陣列)

上一篇中,限量提到的.Net Configuration System的基本概念與基本使用,接著這篇Part II 要來看看更複雜的Configuration File結構,巢狀陣列


巢狀(Nested)

巢狀就是類似的資料結構,層層相疊,就像俄羅斯娃娃一樣,可一層一層剝開。每一層有各自的屬性,而下一層的設定可為這一層的某個屬性,以下為三層式的巢狀結構:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="Company">
      <section name="Setting" type="TestSolution.TestSection, TestSolution" />
    </sectionGroup>
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <Company>
    <Setting name="DebugSetting" type="Console">
      <Cache name="FileCache" path="LimitedLib.Framework.Cache">
        <Global duration="30000"/>
        <Session duration="60000" autoExpire="true"/>
      </Cache>
    </Setting>
  </Company>
</configuration>

上面範例的例子為一個公司的系統設定,第一層的Setting有Name, Type與巢狀的Cache屬性,意思為:設定檔名稱為DebugSetting,系統為Console類型。裏頭使用Cache機制;第二層Cache有Name, Path與巢狀的Global和Session屬性。意思是使用的Cache類別是FileCache,在LimitedLib.Framework.Cache DLL裡,然後FileCache有Global與Session的設定值;再來針對FileCache的Global與Session有不同的設定,Global定義了Cache 30秒,Session定義Cache 60秒後會自動失效。

以此方式我們就可以一層層的加入巢狀設定,讓設定架構更佳完整,再來我們來看看如何使用.Net Configuration System的程式碼:

SettingSection.cs
public class SettingSection : ConfigurationSection
{
    [ConfigurationProperty("name", IsRequired = true)]
    public string Name
    {
        get
        {
            return this["name"] as string;
        }
        set
        {
            this["name"] = value;
        }
    }

    [ConfigurationProperty("type", IsRequired = false)]
    public string Type
    {
        get
        {
            return this["type"] as string;
        }
        set
        {
            this["type"] = value;
        }
    }

    [ConfigurationProperty("Cache")]
    public CacheEle Cache
    {
        get
        {
            return this["Cache"] as CacheEle;
        }
    }
}

CacheEle.cs
public class CacheEle : ConfigurationElement
{
    [ConfigurationProperty("name", IsRequired = true)]
    public string Name
    {
        get
        {
            return this["name"] as string;
        }
        set
        {
            this["name"] = value;
        }
    }

    [ConfigurationProperty("path", IsRequired = true)]
    public string Path
    {
        get
        {
            return this["path"] as string;
        }
        set
        {
            this["path"] = value;
        }
    }

    [ConfigurationProperty("Global")]
    public CacheGlobalEle Global
    {
        get
        {
            return this["Global"] as CacheGlobalEle;
        }
    }

    [ConfigurationProperty("Session")]
    public CacheSessionEle Session
    {
        get
        {
            return this["Session"] as CacheSessionEle;
        }
    }
}

CacheGlobalEle.cs
public class CacheGlobalEle : ConfigurationElement
{
    [ConfigurationProperty("duration")]
    public string Duration
    {
        get
        {
            return this["duration"] as string;
        }
        set
        {
            this["duration"] = value;
        }
    }
}

CacheSessionEle.cs
public class CacheSessionEle : ConfigurationElement
{
    [ConfigurationProperty("duration")]
    public string Duration
    {
        get
        {
            return this["duration"] as string;
        }
        set
        {
            this["duration"] = value;
        }
    }

    [ConfigurationProperty("autoExpire")]
    public string AutoExpire
    {
        get
        {
            return this["autoExpire"] as string;
        }
        set
        {
            this["autoExpire"] = value;
        }
    }
}
因為每個不同的巢狀設定就要建立一個類別去Mapping,所以當設定結構愈複雜的時候,程式碼的量就會愈多,這個部分限量會在Part III中去說明如何減少程式碼的量。先不多說了,我們來寫個遞迴程式來印出所有的巢狀設定:

Program.cs
class Program
{
    static void Main(string[] args)
    {
        var section = ConfigurationManager.GetSection("Company/Setting") as SettingSection;
        OutputInfo(string.Empty, section);

        /*
        * 執行結果:
        * SettingSection -
        *     Name - DebugSetting
        *     Type - Console
        *     CacheEle -
        *         Name - FileCache
        *         Path - LimitedLib.Framework.Cache
        *         CacheGlobalEle -
        *             Duration - 30000
        *         CacheSessionEle -
        *             Duration - 60000
        *             AutoExpire - true
        */

        Console.Read();
    }

    /// <summary>
    /// 遞迴印出每一層設定值
    /// </summary>
    static void OutputInfo(string prefix, ConfigurationElement ele)
    {
        OutputLine($"{prefix}{ele.GetType().Name} -");
        prefix += "\t";
        foreach (var prop in ele.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
        {
            OutputLine($"{prefix}{prop.Name} - {prop.GetValue(ele)}");
            
            // 檢查屬性宣告的DataType是否繼承ConfigurationElement與是否來自目前執行的Assembly中
            if (prop.PropertyType.IsSubclassOf(typeof(ConfigurationElement)) 
                && 
                (prop.PropertyType.Assembly == Assembly.GetExecutingAssembly()))
            {
                OutputInfo(prefix, prop.GetValue(ele) as ConfigurationElement);
            }
        }
    }

    static void OutputLine(string str)
    {
        Console.WriteLine(str);
        Debug.WriteLine(str);
    }
}


陣列(Array)

在任何資料結構中,陣列是一個很重要且常出現的資料結構,在這裡也是一樣的。同一個設定結構,可能會同時出現或使用,以上面那個系統設定檔來說,FileCache, DBCache, MemoryCache...等各種Cache機制是可以同時存在的,所以下面我們就來看看使用陣列結構的設定檔長成怎樣,這裡限量用前一個範例做變化:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="Company">
      <section name="Setting" type="TestSolution.SettingSection, TestSolution" />
    </sectionGroup>
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <Company>
    <Setting name="DebugSetting" type="Console">
      <Caches>
        <Cache name="FileCache" path="LimitedLib.Framework.Cache">
          <Global duration="30000"/>
          <Session duration="60000" autoExpire="true"/>
        </Cache>
        <Cache name="DBCache" path="LimitedLib.Plugin.Cache">
          <Global duration="10000"/>
        </Cache>
        <Cache name="MemoryCache" path="LimitedLib.Framework.Cache">
          <Session duration="10000" autoExpire="false"/>
        </Cache>
      </Caches>
    </Setting>
  </Company>
</configuration>
這個範例和前一個一樣,只是限量在這裡的Cache加入了DBCache和MemoryCache的設定,這些Cache的設定值外層由Caches包裝。

既然設定檔改了,那程式碼也要調整一下,基本上要調整的只有 SettingSection.cs,另外還要加入CacheEleCollection.cs:


SettingSection.cs
public class SettingSection : ConfigurationSection
{
    [ConfigurationProperty("name", IsRequired = true)]
    public string Name
    {
        get
        {
            return this["name"] as string;
        }
        set
        {
            this["name"] = value;
        }
    }

    [ConfigurationProperty("type", IsRequired = false)]
    public string Type
    {
        get
        {
            return this["type"] as string;
        }
        set
        {
            this["type"] = value;
        }
    }

    [ConfigurationProperty("Caches")]
    [ConfigurationCollection(typeof(CacheEle), AddItemName = "Cache")]
    public CacheEleCollection Caches
    {
        get
        {
            return this["Caches"] as CacheEleCollection;
        }
    }
}

CacheEleCollection.cs
public class CacheEleCollection : ConfigurationElementCollection
{
    protected override ConfigurationElement CreateNewElement()
    {
        return new CacheEle();
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
        return (element as CacheEle).Name;
    }

    public CacheEle this[int index]
    {
        get
        {
            return this.BaseGet(index) as CacheEle;
        }
    }
}

在SettingSection.cs中,限量將Cache Property改成Caches,回傳CacheEleCollection,這裡要注意的是要額外加上ConfigurationCollection Attribute,在ConfigurationCollection Attribute要指定陣列的資料型別與子項目的名稱(設定AddItemName,因為預設子項目名稱為add,另外還有remove, clear,詳細請見Configuration Sections Schema)。

在CacheEleCollection.cs中,要實作覆寫兩個方法,另外,限量加上了Index,這樣方便用Index取得特定位置的子項目,或者可以考慮實作用Key的Index去取得子項目。

再來看看執行的結果:
class Program
{
    static void Main(string[] args)
    {
        var section = ConfigurationManager.GetSection("Company/Setting") as SettingSection;
        OutputInfo(string.Empty, section);

            /*
            * 執行結果:
            * SettingSection -
            *   Name - DebugSetting
            *     Type - Console
            *     Caches - TestSolution.CacheEleCollection
            *         CacheEle -
            *             Name - FileCache
            *             Path - LimitedLib.Framework.Cache
            *             Global - TestSolution.CacheGlobalEle
            *             CacheGlobalEle -
            *                 Duration - 30000
            *             Session - TestSolution.CacheSessionEle
            *             CacheSessionEle -
            *                 Duration - 60000
            *                 AutoExpire - true
            *         CacheEle -
            *             Name - DBCache
            *             Path - LimitedLib.Plugin.Cache
            *             Global - TestSolution.CacheGlobalEle
            *             CacheGlobalEle -
            *                 Duration - 10000
            *             Session - TestSolution.CacheSessionEle
            *             CacheSessionEle -
            *                 Duration - 
            *                 AutoExpire - 
            *         CacheEle -
            *             Name - MemoryCache
            *             Path - LimitedLib.Framework.Cache
            *             Global - TestSolution.CacheGlobalEle
            *             CacheGlobalEle -
            *                 Duration - 
            *             Session - TestSolution.CacheSessionEle
            *             CacheSessionEle -
            *                 Duration - 10000
            *                 AutoExpire - false
            */

        Console.Read();
    }

    /// <summary>
    /// 遞迴印出每一層設定值
    /// </summary>
    static void OutputInfo(string prefix, ConfigurationElement ele)
    {
        OutputLine($"{prefix}{ele.GetType().Name} -");
        prefix += "\t";
        foreach (var prop in ele.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
        {
            OutputLine($"{prefix}{prop.Name} - {prop.GetValue(ele)}");

            // 檢查屬性宣告的DataType是否繼承ConfigurationElement與是否來自目前執行的Assembly中
            if (prop.PropertyType.IsSubclassOf(typeof(ConfigurationElement)) 
                && 
                (prop.PropertyType.Assembly == Assembly.GetExecutingAssembly()))
            {
                // 是否為ConfigurationElementCollection
                if (prop.PropertyType.IsSubclassOf(typeof(ConfigurationElementCollection)))
                {
                    foreach(var item in prop.GetValue(ele) as ConfigurationElementCollection)
                    {
                        OutputInfo(prefix + "\t", item as ConfigurationElement);
                        continue;
                    }
                }
                else
                {
                    OutputInfo(prefix, prop.GetValue(ele) as ConfigurationElement);
                    continue;
                }
            }
        }
    }

    static void OutputLine(string str)
    {
        Console.WriteLine(str);
        Debug.WriteLine(str);
    }
}

了解這兩個結構類型後就可以做隨意變化,像是巢狀陣列...,愈複雜的結構只會產生出更多程式碼,所以下一篇限量就要來做簡化程式碼的動作,請期待下集。



站內相關文章:

C# - App.config自訂section程式碼架構Part I(基本用法)
C# - App.config自訂section程式碼架構Part III (使用泛型)


參考來源:
MSDN - Configuration Sections Schema






留言