• 一起谈.NET技术,通过自定义配置实现插件式设计 狼人:


      软件设计有一句话叫做约定优于配置,很多人将其作为拒绝配置的理由。但是,约定和配置的使用,都有个度的问题。我不赞为了所谓的扩展性,为你的应用设计一套只有你自己才能看懂的配置体系。但是,在很多场景中,配置是提供应用灵活度的首要甚至是唯一途径。对于框架的设计者来说,对于配置的驾驭是一项基本的技能。

      可能你很少使用自定义配置,可能你理解的自定义配置仅仅限于AppSetting,不过我想你应该对于System.Configuration这个命名空间下的几个基本的类型有基本的了解。比如ConfigurationSection、ConfigurationElement、ConfigurationElementCollection等。本篇文章不会介绍关于System.Configuration的基础知识,而是通过一个简单的例子为你讲述一些所谓高级的知识点,比如不可识别配置元素的动态解析。(源代码从这里下载)

    目录
    一、通过自定义配置实现的最终效果
    二、相关配置类型的定义
    三、两个重要的类型:NameTypeConfigurationElement和NameTypeConfigurationElementCollectionT
    四、ResourceProviderFactory的定义
    五、补充

      一、通过自定义配置实现的最终效果

      为了让大家对自定义配置的作用有一个深刻的映像,我们先来给出一个简单的例子。我们采用在《.NET的资源并不限于.resx文件,你可以采用任意存储形式》中介绍的关于自定义ResourceManager以实现对多种资源存储形式的支持。现在只关注与资源的读取,我们将基于不同存储形式的资源读取操作实现在相应的ResourceProovider中,它们实现如下一个简单的IResourceProvider接口。

    1: public interface IResourceProvider
    2: {
    3: object GetObject(string key);
    4: }

      然后我们创建两个具体的ResourceProvider:DbResourceProvider和XmlResourceProvider,它们分别基于数据库表和XML文件的资源存储形式。DbResourceProvider需要连接数据库,需要引用配置的连接字符串,所以有一个ConnectionStringName属性;而XmlResourceProvider需要访问具体的XML文件,FileName属性表示文件路径。

    1: [ConfigurationElementType(typeof(DbResourceProviderConfigurationElement))]
    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。

    1: ?xml version="1.0" encoding="utf-8" ?
    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。

    1: static void Main(string[] args)
    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: }

      输出结果:

    1: Artech.Resources.DbResourceProvider
    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子节点。

    1: public class ResourceSettings: ConfigurationSection
    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。

    1: public abstract class ResourceProviderConfigurationElement: NameTypeConfigurationElement
    2: {
    3: public abstract IResourceProvider CreateProvider();
    4: }

      DbResourceProvider和XmlResourceProvider具有各自的ResourceProviderConfigurationElement,分别为DbResourceProviderConfigurationElement和XmlResourceProviderConfigurationElement。

    1: public class DbResourceProviderConfigurationElement : ResourceProviderConfigurationElement
    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定义出来用于解决非识别配置项的反序列化问题。

    1: public class NameTypeConfigurationElement : ConfigurationElement
    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。应该说它是整个配置体系的核心,其全部定义如下所示。

    1: public class NameTypeElementCollectionT : ConfigurationElementCollection where T : NameTypeConfigurationElement
    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和对应配置元素之间的匹配关系。

    1: [ConfigurationElementType(typeof(DbResourceProviderConfigurationElement))]
    2: public class DbResourceProvider : IResourceProvider
    3: {
    4: //...
    5: }

      而这个ConfigurationElementTypeAttribute定义非常简单,仅仅定义一个用于表示配置元素类型的ConfigurationElementType属性,该属性在构造函数中初始化。

    1: [AttributeUsage( AttributeTargets.Class)]
    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的实现就非常简单了。

    1: public static class 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。

  • 相关阅读:
    微信小程序开发教程
    微信小程序的动画效果
    关于PHP中的全局变量global和$GLOBALS的不同区分
    爱心背景特效
    jQuery常用语法总结笔记
    微信HTML5页面设计建议
    学习Javascript闭包(Closure)
    转: web 页面加载速度优化实战100% 的飞跃提升
    javascript中slice() splice() concat()操作数组的方法
    Web前端开发规范文档你需要知道的事
  • 原文地址:https://www.cnblogs.com/waw/p/2163059.html
Copyright © 2020-2023  润新知