通過自定義配置實現插件式設計
軟件設計有一句話叫做約定優于配置,很多人將其作為拒絕配置的理由。但是,約定和配置的使用,都有個度的問題。我不贊為了所謂的擴展性,為你的應用設計一套只有你自己才能看懂的配置體系。但是,在很多場景中,配置是提供應用靈活度的首要甚至是唯一途徑。對于框架的設計者來說,對于配置的駕馭是一項基本的技能。
可能你很少使用自定義配置,可能你理解的自定義配置僅僅限于AppSetting,不過我想你應該對于System.Configuration這個命名空間下的幾個基本的類型有基本的了解。比如ConfigurationSection、ConfigurationElement、ConfigurationElementCollection等。本篇文章不會介紹關于System.Configuration的基礎知識,而是通過一個簡單的例子為你講述一些所謂高級的知識點,比如不可識別配置元素的動態解析。(源代碼從這里下載)
目錄
一、通過自定義配置實現的最終效果
二、相關配置類型的定義
三、兩個重要的類型:NameTypeConfigurationElement和NameTypeConfigurationElementCollectionT
四、ResourceProviderFactory的定義
五、補充
一、通過自定義配置實現的最終效果
為了讓大家對自定義配置的作用有一個深刻的映像,我們先來給出一個簡單的例子。我們采用在《.NET的資源并不限于.resx文件,你可以采用任意存儲形式》中介紹的關于自定義ResourceManager以實現對多種資源存儲形式的支持。現在只關注與資源的讀取,我們將基于不同存儲形式的資源讀取操作實現在相應的ResourceProovider中,它們實現如下一個簡單的IResourceProvider接口。
2: {
3: object GetObject(string key);
4: }
然后我們創建兩個具體的ResourceProvider:DbResourceProvider和XmlResourceProvider,它們分別基于數據庫表和XML文件的資源存儲形式。DbResourceProvider需要連接數據庫,需要引用配置的連接字符串,所以有一個ConnectionStringName屬性;而XmlResourceProvider需要訪問具體的XML文件,FileName屬性表示文件路徑。
2: public class DbResourceProvider : IResourceProvider
3: {
4: public string ConnnectionStringName { get; private set; }
5: public DbResourceProvider(string connectionStringName)
6: {
7: this.ConnnectionStringName = connectionStringName;
8: }
9: public object GetObject(string key)
10: {
11: throw new NotImplementedException();
12: }
13: public override string ToString()
14: {
15: return string.Format("{0}\n\tConncectionString Name:{1}", typeof(DbResourceProvider).FullName, this.ConnnectionStringName);
16: }
17: }
18:
19: [ConfigurationElementType(typeof(XmlResourceProviderConfigurationElement))]
20: public class XmlResourceProvider : IResourceProvider
21: {
22: public string FileName { get; private set; }
23: public XmlResourceProvider(string fileName)
24: {
25: this.FileName = fileName;
26: }
27: public object GetObject(string key)
28: {
29: throw new NotImplementedException();
30: }
31: public override string ToString()
32: {
33: return string.Format("{0}\n\tFile Name:{1}", typeof(XmlResourceProvider).FullName, this.FileName);
34: }
35: }
具體使用哪個ResourceProvider,通過配置來決定。整個配置定義在artech.resources配置節中,該配置節具有一個providers子節點,它定義了一系列ResourceProvider的列表。每個ResourceProvider配置具有兩個相同的屬性:Name和Type,以及一些自己專屬的配置屬性(比如DbResourceProvider的connectionStringName,XmlResourceProvider的fileName)。至于默認采用哪個Provider,則通過配置節的defaultProvider屬性來決定。在本例中,我們默認采用的是DbProvider。
2: configuration
3: configSections
4: section name="artech.resources" type="Artech.Resources.Configuration.ResourceSettings,Artech.CustomConfiguration"/
5: /configSections
6: artech.resources defaultProvider="DbProvider"
7: providers
8: add name="DbProvider" type="Artech.Resources.DbResourceProvider, Artech.CustomConfiguration" connectionStringName="LocalSqlServer"/
9: add name="XmlProvider" type="Artech.Resources.XmlResourceProvider, Artech.CustomConfiguration" fileName="C:\resources.xml"/
10: /providers
11: /artech.resources
12: /configuration
現在我們有一個ResourceProviderFactory的工廠類來幫助我們根據配置創建默認的ResourceProvider,或者創建指定名稱的ResourceProvider。現在我們按照如下的方式使用ResourceProviderFactory。
2: {
3: IResourceProvider resourceProvider = ResourceProviderFactory.GetResourceProvider();
4: Console.WriteLine(resourceProvider);
5: Console.WriteLine();
6:
7: resourceProvider = ResourceProviderFactory.GetResourceProvider("XmlProvider");
8: Console.WriteLine(resourceProvider);
9: Console.WriteLine();
10:
11: resourceProvider = ResourceProviderFactory.GetResourceProvider("DbProvider");
12: Console.WriteLine(resourceProvider);
13: Console.WriteLine();
14: }
輸出結果:
2: ConncectionString Name:LocalSqlServer
3:
4: Artech.Resources.XmlResourceProvider
5: File Name:C:\resources.xml
6:
7: Artech.Resources.DbResourceProvider
8: ConncectionString Name:LocalSqlServer
接下來我們就來介紹整個配置體系,以及ResourceProviderFactory的實現。
二、相關配置類型的定義
我們現在來看看與配置相關的類型的定義。整個配置節定義在如下一個ResourceSettings的類中,它直接繼承自ConfigurationSection。ResourceSettings具有兩個配置屬性:DefaultProvider和Providers,分別代表artech.resources的defaultProvider屬性和providers子節點。
2: {
3: [ConfigurationProperty("defaultProvider", IsRequired = true)]
4: public string DefaultProvider
5: {
6: get{return (string)this["defaultProvider"];}
7: set{this["defaultProvider"] = value;}
8: }
9: [ConfigurationProperty("providers", IsRequired = true)]
10: public NameTypeElementCollectionResourceProviderConfigurationElement Providers
11: {
12: get{return (NameTypeElementCollectionResourceProviderConfigurationElement)this["providers"];}
13: set{this["providers"] = value;}
14: }
15: public static ResourceSettings GetConfiguration()
16: {
17: return (ResourceSettings)ConfigurationManager.GetSection("artech.resources");
18: }
19: }
屬性Providers是一個名稱為NameTypeElementCollectionT的泛型類型。從名稱我們不難看出,這是一個集合類型,代表配置的ResourceProvider集合。而基于ResourceProvider的配置定義在如下一個ResourceProviderConfigurationElement抽象類中。該類繼承自我們自定義的NameTypeConfigurationElement類型,具有一個CreateProvider抽象方法用于創建相應的ResourceProvider。
2: {
3: public abstract IResourceProvider CreateProvider();
4: }
DbResourceProvider和XmlResourceProvider具有各自的ResourceProviderConfigurationElement,分別為DbResourceProviderConfigurationElement和XmlResourceProviderConfigurationElement。
2: {
3: [ConfigurationProperty("connectionStringName", IsRequired = true)]
4: public string ConnectionStringName
5: {
6: get{return (string)this["connectionStringName"];}
7: set{this["connectionStringName"] = value;}
8: }
9: public override IResourceProvider CreateProvider()
10: {
11: return new DbResourceProvider(this.ConnectionStringName);
12: }
13: }
14:
15: public class XmlResourceProviderConfigurationElement : ResourceProviderConfigurationElement
16: {
17: [ConfigurationProperty("fileName", IsRequired = true)]
18: public string FileName
19: {
20: get{return (string)this["fileName"];}
21: set{this["fileName"] = value;}
22: }
23: public override IResourceProvider CreateProvider()
24: {
25: return new XmlResourceProvider(this.FileName);
26: }
27: }
三、兩個重要的類型:NameTypeConfigurationElement和NameTypeConfigurationElementCollectionT
接下來介紹兩個重要的類型,第一個是ResourceProviderConfigurationElement的基類:NameTypeConfigurationElement。顧名思義,NameTypeConfigurationElement就是具有兩個基本配置屬性Name和Type的配置元素(ConfigurationElement),其定義如下。方法DeserializeElement定義出來用于解決非識別配置項的反序列化問題。
2: {
3: [ConfigurationProperty("name", IsRequired = true, IsKey = true)]
4: public string Name
5: {
6: get{return (string)this["name"];}
7: set{this["name"] = value;}
8: }
9: [ConfigurationProperty("type", IsRequired = true)]
10: public string TypeName
11: {
12: get{return (string)this["type"];}
13: set{this["type"] = value;}
14: }
15: public Type Type
16: {
17: get{return Type.GetType(this.TypeName);}
18: }
19: public void DeserializeElement(XmlReader reader)
20: {
21: base.DeserializeElement(reader, false);
22: }
23: }
另一個類型就是NameTypeConfigurationElement的配置元素集合(ConfigurationElementCollection):NameTypeElementCollectionT。應該說它是整個配置體系的核心,其全部定義如下所示。
2: {
3: protected override ConfigurationElement CreateNewElement()
4: {
5: return Activator.CreateInstanceT();
6: }
7: protected override object GetElementKey(ConfigurationElement element)
8: {
9: return (element as NameTypeConfigurationElement).Name;
10: }
11: protected virtual Type RetrieveConfigurationElementType(XmlReader reader)
12: {
13: Type configurationElementType = null;
14: if (reader.AttributeCount 0)
15: {
16: for (bool go = reader.MoveToFirstAttribute(); go; go = reader.MoveToNextAttribute())
17: {
18: if ("type".Equals(reader.Name))
19: {
20: Type providerType = Type.GetType(reader.Value, false);
21: Attribute attribute = Attribute.GetCustomAttribute(providerType, typeof(ConfigurationElementTypeAttribute));
22: if (attribute == null)
23: {
24: throw new ConfigurationErrorsException("No ConfigurationElementTypeAttribute is applied.");
25: }
26: configurationElementType = ((ConfigurationElementTypeAttribute)attribute).ConfigurationElementType;
27: break;
28: }
29: }
30: reader.MoveToElement();
31: }
32: return configurationElementType;
33: }
34: protected override bool OnDeserializeUnrecognizedElement(string elementName, XmlReader reader)
35: {
36: if (base.AddElementName.Equals(elementName))
37: {
38: Type configurationElementType = this.RetrieveConfigurationElementType(reader);
39: var currentElement = (T)Activator.CreateInstance(configurationElementType);
40: currentElement.DeserializeElement(reader);
41: base.BaseAdd(currentElement, true);
42: return true;
43: }
44: return base.OnDeserializeUnrecognizedElement(elementName, reader);
45: }
46: public T GetConfigurationElement(string name)
47: {
48: return (T)this.BaseGet(name);
49: }
50: }
對于配置我們應該有這樣的認識:我們通過相應的類型來定義配置文件中的某個XML元素,在進行讀取的時候實際上就是一個反序列化的工作。而對于成功進行序列化和反序列化,其根本前提是確定目標類型,因為類型描述了元數據。帶著這個結論再來看看我們的以XML表示的配置和ResourceSettings的定義,我們會發現一個問題:ResourceSetting的Providers屬性的類型是NameTypeElementCollectionResourceProviderConfigurationElement,配置元素類型ResourceProviderConfigurationElement是一個抽象類型。而我們需要將具體的ResourceProvider配置反序列化成DbResourceProviderConfigurationElement和XmlResourceProviderConfigurationElement,而整個配置系統似乎找不到這個兩個類型的影子。如果不能預先確定配置元素需要反序列化成的真實類型,整個配置的讀取將會失敗。具體來說,它不能識別DbProvider元素的connectionStringName屬性,和XmlProvider的fileName屬性,因為基類ResourceProviderConfigurationElement沒有相關屬性的定義。
既然在默認情況下具體ResourceProvider的配置元素不能被反序列化,它們屬于不可識別元素(Unrecognized Element),那么我們只要手工對其實施反序列化,具體做法就是重寫ConfigurationElementCollection的OnDeserializeUnrecognizedElement方法。但是即使手工進行反序列化,也需要確定具體的配置元素類型,這又如何解決呢?如果你足夠仔細的話,在定義DbResourceProvider和XmlResourceProvider的時候,在類上面應用了一個特殊的自定義特性:ConfigurationElementTypeAttribute,它建立起了具體ResourceProvider和對應配置元素之間的匹配關系。
2: public class DbResourceProvider : IResourceProvider
3: {
4: //...
5: }
而這個ConfigurationElementTypeAttribute定義非常簡單,僅僅定義一個用于表示配置元素類型的ConfigurationElementType屬性,該屬性在構造函數中初始化。
2: public class ConfigurationElementTypeAttribute: Attribute
3: {
4: public Type ConfigurationElementType { get; private set; }
5: public ConfigurationElementTypeAttribute(Type configurationElementType)
6: {
7: this.ConfigurationElementType = configurationElementType;
8: }
9: }
由于每個具體的ResourceProvider都具有這樣一個ConfigurationElementTypeAttribute來指定對應的ConfigurationElement類型,那么我們就可以反射來為反序列化確定配置元素的目標類型了。這樣的操作實現在RetrieveConfigurationElementType方法中。
四、ResourceProviderFactory的定義
NameTypeElementCollectionT通過重寫OnDeserializeUnrecognizedElement方法,以及借助于ConfigurationElementTypeAttribute特性,解決了對不可識別元素的解析問題。而具體的ResourceProviderConfigurationElement都實現了CreateProvider方法來創建對應的ResourceProvider,那么ResourceProviderFactory的實現就非常簡單了。
2: {
3: public static IResourceProvider GetResourceProvider()
4: {
5: ResourceSettings settings = ResourceSettings.GetConfiguration();
6: return GetResourceProvider(settings.DefaultProvider);
7: }
8: public static IResourceProvider GetResourceProvider(string name)
9: {
10: ResourceSettings settings = ResourceSettings.GetConfiguration();
11: return settings.Providers.GetConfigurationElement(name).CreateProvider();
12: }
13: }
五、補充
經常關注我博客朋友應該知道本人對微軟開源框架EnterLib有一定的了解。熟悉EnterLib的朋友經常詬病于它繁瑣的配置,這確實是一個問題。不過這從另一個方面說明了EnterLib底層配置系統的強大,不然很難支持如此復雜的配置。對于學習自定義配置,了解EnterLib配置體系的實現是一個不錯的途徑。實際上,本篇文章關于不可識別配置元素的解析的解決方案就是來源于EnterLib。