一起谈.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。

目录
相关文章
|
6月前
|
开发框架 JSON .NET
ASP.NET Core 自定义配置警告信息
自定义配置警告信息需要在 startup 类中的 ConfigureService 方法中进行配置示例: // 注册 控制器服务 services.AddControllers(configure: setup => { setup.ReturnHttpNotAcceptable = true; ...
42 0
|
7月前
|
XML 存储 JSON
使用自定义XML配置文件在.NET桌面程序中保存设置
本文将详细介绍如何在.NET桌面程序中使用自定义的XML配置文件来保存和读取设置。除了XML之外,我们还将探讨其他常见的配置文件格式,如JSON、INI和YAML,以及它们的优缺点和相关的NuGet类库。最后,我们将重点介绍我们为何选择XML作为配置文件格式,并展示一个实用的示例。
96 0
|
6月前
|
Kubernetes 安全 数据安全/隐私保护
【K8S系列】深入解析k8s网络插件—Weave Net
【K8S系列】深入解析k8s网络插件—Weave Net
256 0
|
3月前
|
IDE 前端开发 JavaScript
【C#】C# 开发环境配置(Rider 一个.NET 跨平台集成开发环境)
【1月更文挑战第26天】【C#】C# 开发环境配置(Rider 一个.NET 跨平台集成开发环境)
|
4月前
|
开发框架 .NET PHP
Web Deploy配置并使用Visual Studio进行.NET Web项目发布部署
Web Deploy配置并使用Visual Studio进行.NET Web项目发布部署
|
4月前
|
XML API 数据库
七天.NET 8操作SQLite入门到实战 - 第六天后端班级管理相关接口完善和Swagger自定义配置
七天.NET 8操作SQLite入门到实战 - 第六天后端班级管理相关接口完善和Swagger自定义配置
|
4月前
|
JSON JavaScript 前端开发
全面的.NET微信网页开发之JS-SDK使用步骤、配置信息和接口请求签名生成详解
全面的.NET微信网页开发之JS-SDK使用步骤、配置信息和接口请求签名生成详解
|
4月前
|
SQL Shell 数据库
七天.NET 8操作SQLite入门到实战 - 第二天 在 Windows 上配置 SQLite环境
七天.NET 8操作SQLite入门到实战 - 第二天 在 Windows 上配置 SQLite环境
|
5月前
|
Windows
基于.Net Core实现自定义皮肤WidForm窗口
基于.Net Core实现自定义皮肤WidForm窗口
48 0
|
5月前
|
监控 数据可视化 前端开发
一个.NetCore前后端分离、模块化、插件式的通用框架
一个.NetCore前后端分离、模块化、插件式的通用框架
100 0