讓 DLL 組件讀取自己的組態檔

上次嘗試利用 Fluent Configuration API 來動態設定應用程式組態,便想到以前經常碰到的問題:如何讓 DLL 組件有自己的「應用程式組態檔」?

這篇就稍微整理一下,並提供一個簡易的、讀取 DLL 組件的工具類別:AssemblySettings。

為什麼?

剛開始,我們通常會把組態項目放在應用程式組態檔的 <appSettings> 區段。但隨著需求逐漸增加,應用程式組態檔裡面可能會塞進太多東西,光要找個東西都有點麻煩。如果把類別庫自己需要的組態項目放在自己的組態檔案裡,會比較乾淨、清楚,也方便維護。

可行方案

解決方案有好幾個,例如,你可以用傳統的 INI 檔案。如果組態項目不多,我往往會先想到 INI 格式,因為它很清楚、簡單,對我的眼睛來說,它比 XML 友善得多。網路上也找得到現成的類別,例如我先前用過的 Nini

你也可以使用 file 或 configSource 屬性,來將 <appSettings> 區段的內容抽離到別的檔案存放。注意 file 和 configSource 的差別,前者可利用外部檔案來擴充既有的 appSettings 區段(外部檔案的組態會跟主要組態檔的內容合併),但僅限於 appSettings 區段才能使用;後者(configSource)則必須整個區段移出去外部檔案,使用對象不限定 appSettings 區段。

或者,你也可以利用 ConfigurationSection 來建立自訂的組態區段。也就是說,把原先放在 <appSettings> 區段中的組態,依其用途或分類將它們抽離出來,成為獨立的區段。相較於將所有組態項目全放進 <appSettings> 區段,這種做法似乎比較好一點。然而,基本上它們還是放在同一個應用程式組態檔中,或者得用剛才提到的 file 或 configSource 元素來把它們抽離出去成為單獨的組態檔當你的組態項目。就算使用這種方式將組態檔獨立出去,我們還是得在「根」應用程式組態檔案中使用 <configSection> 元素來宣告其他「外掛」的組態區段。對於某些應用場合,這種組態檔之間的牽扯會讓我覺得不安--因為切得不夠乾淨。

集中存放當然有它的好處,比如說,當你想要讓應用程式組態檔有多種版本的時候,例如:MyApp.Debug.config 和 MyApp.Release.config,此時將組態項目集中於一個檔案中存放的作法,在部署的時候可能就簡單一些。

無論如何,有時候還是需要把某些組態項目獨立出去,尤其是當我希望類別庫能夠有自己的組態檔的時候。

讀取 DLL 組件組態檔

底下是一個現成的類別,可用來讀取特定 DLL 組件的「應用程式組態檔」。之所以用引號包住,是因為實際使用時,的確是在 DLL 專案中加入一個應用程式組態檔(App.config),但經過 Visual Studio 編譯之後,會在輸出路徑下產生 MyLib.DLL.config 檔案。嚴格說來,這個檔案並不是「應用程式」的組態檔,而是組件自己專屬的組態檔。故將類別命名為 AssemblySettings。

using System;
using System.Configuration;
using System.Reflection;

namespace HuanLib.Configuration
{
    public class AssemblySettings
    {
        private KeyValueConfigurationCollection _settings;

        public AssemblySettings(Assembly asmb)
        {
            LoadSettings(asmb);
        }

        private void LoadSettings(Assembly asmb)
        {
            Configuration config = ConfigurationManager.OpenExeConfiguration(asmb.Location);

            AppSettingsSection section = (config.GetSection("appSettings") as AppSettingsSection);
            _settings = section.Settings;
        }

        public string this[string key]
        {
            get
            {
                return _settings[key].Value;
            }
        }
    }
}

此類別可放在你自己的類別庫裡面,作為通用的工具類別。呼叫端可以透過建構式傳入目標組件。

使用 AssemblySettings 類別時,只要在你的類別庫專案中加入一個 App.config,把需要的組態項目寫在 <appSettings> 區段裡,然後在需要讀取組態項目時這麼寫:

namespace MyLib
{
    public class MyConfig
    {
        private AssemblySettings _settings = new AssemblySettings(Assembly.GetExecutingAssembly());

        public string UserName
        {
            get
            {
                return _settings["UserName"];
            }
        }
    }
}

我在 MyLib 裡面用另一個類別 MyConfig 來封裝自己的組態項目,並以屬性的方式對外公開。如此一來,要存取組態設定時,就可以這麼寫:

    MyConfig myConfig = new MyConfig();
    Console.WriteLine(myConfig.UserName);

如果覺得這樣包一層太麻煩,當然也可以直接在類別庫專案中直接使用 AssemblySettings 物件的 indexer 屬性來取得組態項目的值,而不用像 MyConfig 類別那樣額外提供對應的屬性。
也許不太重要,也許你不會碰到這個問題,但還是補充一下:ConfigurationManager 的 AppSettings 屬性的型別是 NameValueCollection,但這裡所使用的 AppSettingsSection 的 Settings 屬性卻是 KeyValueConfigurationCollection。同樣的用途,.NET Framework 使用了兩種不同型別來實作。因此,如果你想要先檢查應用程式組態檔(ConfigurationManager.AppSettings)裡面有沒有特定的組態項目,若沒有,才以上述方式讀取外部組態檔的話,就得分別寫兩種讀取組態項目值的程式碼--除非把這兩個物件都轉型為 ICollection。問題是 ICollection 沒有 indexer 屬性可以用(說了等於沒說 XD)。
限制

此作法的一個小問題,就是組件的組態檔必須跟著 DLL 組件一起部署。當你在應用程式專案中參考類別庫專案或 DLL 組件時,該組件的組態檔並不會跟著複製一份到應用程式專案的輸出路徑。這個部分,要麼手動處理,或者寫點建置腳本,或者透過修改專案選項的輸出路徑來解決。

此外,組件的組態檔就跟應用程式組態檔一樣,可以有 appSettings 區段、connectionStrings 區段,但是如果任意加入自訂區段,例如:<dbSettings>,AssemblySettings 類別便無法取得這些自訂區段的內容。如果想要任意加入自訂區段,可以參考 Mike Woodring 的 ConfigFileReader,它是以 XmlTextReader 來讀取組態檔,所以任何符合 XML 格式的組態檔都適用。

如果是 ASP.NET 應用程式呢?

上述技巧同樣適用於 ASP.NET 應用程式,可是要稍微改一下,因為 AssemblySettings 類別是利用 Assembly 物件的 Location 屬性來作為 ConfigurationManager 的 OpenExeConfiguration 方法的傳入參數。這種做法,在 ASP.NET 環境下會找不到組態檔,因為我們所要開啟的是跟 DLL 放在同一個目錄下的組態檔,可是 ASP.NET 應用程式在執行時,檔案會被複製一份到 ASP.NET 暫存資料夾,亦即真正執行的組件,其實是放在暫存資料夾裡面的 DLL 組件。問題就在於 ASP.NET 並不會幫我們把Bin 目錄下的組態檔複製過去,故若以 Assembly 的 Location  的方式來指定組態檔的位置,就會變成去 ASP.NET 暫存目錄下找組態檔,這就當然找不到了。

若要讓一般 Windows 程式和 ASP.NET 程式都能使用此類別,我們得用其他方式來取得組件的所在位置。原本的 LoadSettings 可以改成這樣:

    private void LoadSettings(Assembly asmb, string sectionName)
    {
        ExeConfigurationFileMap cfgFileMap = new ExeConfigurationFileMap();
        Uri codeBaseUri = new Uri(asmb.CodeBase);
        cfgFileMap.ExeConfigFilename = codeBaseUri.AbsolutePath + ".config";

        if (!File.Exists(cfgFileMap.ExeConfigFilename))
        {
            throw new Exception("Configuration file not found: " + cfgFileMap.ExeConfigFilename);
        }

        Configuration config = ConfigurationManager.OpenMappedExeConfiguration(cfgFileMap, ConfigurationUserLevel.None);

        if (String.IsNullOrEmpty(sectionName))
        {
            sectionName = "appSettings";
        }

        AppSettingsSection section = (config.GetSection(sectionName) as AppSettingsSection);
        _settings = section.Settings;
        }

這次是用 OpenMappedExeConfiguration 方法來開啟組態檔。使用這種方法,組態檔的名稱就可完全自訂了,不用像 OpenExeConfiguration 那樣,檔名必須按照 assembly.dll.config 的格式。

還有一個問題, ASP.NET 應用程式的 DLL 檔案是放在網站的 bin 目錄下,你可能不想把組態檔放在這裡,而想跟 web.config 放在一起。若是這樣,只要再稍微修改一下 AssemblySettings 的 LoadSettings 方法,利用 HttpContext.Current 是否為 null 來判斷目前的應用程式是否執行於 ASP.NET 環境下,然後利用 HttpContext.Current.Server.Map 方法來取得組態檔的實體路徑,應該就差不多了。

小結

幾乎任何 non-trivial 的應用程式都少不了組態檔。組態檔可能很簡單,只有幾項參數,用幾行簡單的程式碼就能處理;也可能很複雜,複雜到令人覺得全塞在單一檔案裏面會不太好維護,甚至可能還得考慮加入組態繼承、快取等機制。如果是 data-centric 的應用程式,還可能會把用戶端的組態盡量保存於後端資料庫,以減輕維護組態的成本。

換句話說,設計應用程式的組態管理框架時,最好能一併考慮兩種組態來源:一個是檔案,一個是資料庫。嗯...要考慮的東西似乎越來越多了。Orz

下載範例程式:DllConfigDemo.7z (Visual Studio 2010)

延伸閱讀

6 則留言:

  1. 你好! 有一個問題想請教你,我想做一個WCF DLL。但是HOST可能是 IIS 或 應用程式 (希望能兩用)。 但是不知道該怎麼規劃 DLL 專用的設定檔。如果是在 IIS 環境設定檔就須為 web.config ,若是在 應用程式環境下設定檔就須為 app.config 嗎 ?

    回覆刪除
  2. 是的,就像你說的那樣。但是你的 WCF DLL 基本上不用管 host 是哪一種,因為無論是 web.config 還是 app.config,在程式中都是以相同的寫法去存取組態項目。

    回覆刪除
  3. 可能我貪心,由於讀取web.config 跟 app.config 引用的組件不相同。 如果針對我的兩用型(用於IIS/應用程式) DLL 想找一個通用型的作法來讀取設定值。 不知道版主有甚麼建議?

    回覆刪除
  4. 你好! 請問 DLL 可得知目前 HOST 是 IIS 還是應用程式嗎 ?

    回覆刪除
  5. 你可以用 HttpContext.Current 是否為 null 來判斷,例如:

    if (System.Web.HttpContext.Current != null)
    {
    // 是 ASP.NET 應用程式
    }
    else
    {
    // 是 windows app
    }

    回覆刪除
  6. 感謝版主提供寶貴的建議!!

    回覆刪除

技術提供:Blogger.
回頂端⬆️