跟同事聊到如何在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>
<addkey="mapping:jeffrey"value="manager"/>
<addkey="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>
<addfrom="jeffrey"to="manager"></add>
<addfrom="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
publicclass Mapping : ConfigurationElement
{
[ConfigurationProperty("from", IsRequired = true)]
publicstring From { get { returnbase["from"].ToString(); } }
[ConfigurationProperty("to", IsRequired = true)]
publicstring To { get { returnbase["to"].ToString(); } }
}
[ConfigurationCollection(typeof(Mapping))]
publicclass MappingCollection : ConfigurationElementCollection
{
protectedoverride ConfigurationElement CreateNewElement()
{
returnnew Mapping();
}
protectedoverrideobject GetElementKey(ConfigurationElement element)
{
return (element as Mapping);
}
}
publicclass AccountMappingSection : ConfigurationSection
{
[ConfigurationProperty("mappings")]
public MappingCollection Mappings
{
get { return ((MappingCollection)(base["mappings"])); }
set { base["mappings"] = value; }
}
}
}
寫好後,記得要在web.config中加入自訂configurationSection項目,如此就能在config使用accountMapping/mappings加入設定:
<configuration>
<configSections>
<sectionname="accountMapping"type="ConfigTest.AccountMappingSection, ConfigTest"/>
</configSections>
<accountMapping>
<mappings>
<addfrom="jeffrey"to="manager"></add>
<addfrom="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能徹底做到條理分明,一絲不苟!但代價是必須定義一堆囉嗦的自訂型別。當設定項目很多且龐雜時,採取嚴謹做法有其必要性,但像文章開頭的範例只想條列幾筆同性質設定,值不值得擺出此等陣仗,大家就自行拿捏囉~