跟同事聊到如何在web.config加入多筆式設定。所謂多筆式設定,是指同性質設定可能有1到n筆並存,我常遇到的例子是偵錯用途或排除例外的對應設定,例如:將Windows登入帳號A對應成帳號B,部門C對應成部門D… 等等。這類設定,若筆數很多我通常會另外弄個Text或JSON保存,若筆數不多只有三五筆,我喜歡直接寫進config檔,比較乾淨俐落。

舉個實例,假設有個整合式驗證ASP.NET網站依登入帳號識別使用者身分,帳號manager具有管理權限。開發人員jeffrey臨時想模擬manager登入進行測試,當然不能跑去跟主管講「可不可以給我你的AD帳號密碼?」。我慣用的簡單解法是在系統加入一小段額外邏輯,由web.config讀取設定,允許將某些帳號對應成其他帳號。開發測試人員jeffrey可使用自己的帳號登入,由系統將其轉換成manager身分。測試完畢再移除設定,恢復以jeffrey身分操作系統。

要加入設定,大家最先想到的一定是<appSettings>,但<add key="…" value="…" />設定以key值識別,適合一種設定一筆,當資料有多筆就要想辦法合併或編碼。如果是字串陣列還簡單,可靠CSV搞定,例如:<add key="AdminUsers" value="jeffrey,darkthread" />。但若遇到需要兩個值的對應設定就得自訂編碼法則,例如:<add key="AccountMapping" value="jeffrey:manager;darkthread:admin" />,讀取時得解碼,但感謝有LINQ,我們用一行就可搞定:

(ConfigurationManager.AppSettings["AccountMapping"] ?? "").Split(';')
.Select(o => o.Split(':')).ToDictionary(o => o[0], o => o[1]);

但以上做法有個小缺點,資料筆數一多value值會變得很長很亂,修改起來容易眼花,而且要留意分隔符號出現在設定值裡的可能性。

後來我想到一種自認不錯的解法,指定專屬前置詞(例如:"mapping:"),寫成:

  <appSettings>
    <add key="mapping:jeffrey" value="manager" />
    <add key="mapping:darkthread" value="admin" />
  </appSettings>

一筆設定寫成一行,閱讀或修改都很清楚明瞭,而要取值也很簡單,再次交由神奇的LINQ搞定:

ConfigurationManager.AppSettings.AllKeys.Where(o => o.StartsWith("mapping:"))
.ToDictionary(o => o.Split(':').Last(), o => ConfigurationManager.AppSettings[o]);

今天介紹的這個做法很方便好用吧?我們下次再見… (揮手下降,但馬上被人拖上來)

謎之聲:等等!在config中使用多筆式設定,明明.NET就有提供強型別化的正規寫法,你老是教別人這類取巧的旁門左道像話嗎?你的社會責任呢?

呃… 好的。現在來介紹如何自訂ConfigurationSection,在web.config或App.config使用自訂XML元素名稱優雅地加入多筆式設定,像這樣:

  <accountMapping>
    <mappings>
      <add from="jeffrey" to ="manager"></add>
      <add from="darkthread" to="admin"></add>
    </mappings>
  </accountMapping>

首先,我們要在程式裡定義accountMapping對應的ConfigurationSection型別,mappings對應的ConfigurationElementCollection型別,以及具備from與to兩個屬性的ConfigurationElement型別。程式範例如下:

using System.Configuration;
 
namespace ConfigTest
{
    //REF: http://www.abhisheksur.com/2011/09/writing-custom-configurationsection-to.html
 
    public class Mapping : ConfigurationElement
    {
        [ConfigurationProperty("from", IsRequired = true)]
        public string From { get { return base["from"].ToString(); } }
        [ConfigurationProperty("to", IsRequired = true)]
        public string To { get { return base["to"].ToString(); } }
    }
 
    [ConfigurationCollection(typeof(Mapping))]
    public class MappingCollection : ConfigurationElementCollection
    {
        protected override ConfigurationElement CreateNewElement()
        {
            return new Mapping();
        }
        protected override object GetElementKey(ConfigurationElement element)
        {
            return (element as Mapping);
        }
    }
 
 
    public class AccountMappingSection : ConfigurationSection
    {
        [ConfigurationProperty("mappings")]
        public MappingCollection Mappings
        {
            get { return ((MappingCollection)(base["mappings"])); }
            set { base["mappings"] = value; }
        }
    }
 
}

寫好後,記得要在web.config中加入自訂configurationSection項目,如此就能在config使用accountMapping/mappings加入設定:

<configuration>
  <configSections>
    <section name="accountMapping" type="ConfigTest.AccountMappingSection, ConfigTest" />
  </configSections>
  <accountMapping>
    <mappings>
      <add from="jeffrey" to ="manager"></add>
      <add from="darkthread" to="admin"></add>
    </mappings>
  </accountMapping>
</configuration>

讀取時,使用ConfigurationManager.GetSection()取回設定內容進行型別轉換,可以強型別方式取得設定:

            AccountMappingSection sec = 
                ConfigurationManager.GetSection("accountMapping") 
                as AccountMappingSection;
            if (sec != null)
            {
                foreach (Mapping mapping in sec.Mappings)
                {
                    Console.WriteLine("{0} -> {1}", mapping.From, mapping.To);
                }
            }

當設定內容不合規範,會有明確的ConfigurationErrorsExceptoin錯誤訊息,非常清楚。

自訂ConfigurationSection能徹底做到條理分明,一絲不苟!但代價是必須定義一堆囉嗦的自訂型別。當設定項目很多且龐雜時,採取嚴謹做法有其必要性,但像文章開頭的範例只想條列幾筆同性質設定,值不值得擺出此等陣仗,大家就自行拿捏囉~


Comments

Be the first to post a comment

Post a comment