创建自定义配置






4.47/5 (17投票s)
2004年9月3日
10分钟阅读

82272

1737
通过利用 .NET 的灵活性创建自己的配置节和处理程序,在 web.config 中创建隔离设置、强类型对象和集合。
目录
引言
ASP.NET 引入的 web.config 文件极大地弥补了经典 ASP 中我们不得不处理的配置漏洞。我们实际上从几乎没有(global.asa 是最接近的文件)发展到结构良好、可即时更改的 XML 文件。在大多数情况下,web.config 文件运行良好。但是,使用它存在一些限制和约束,您可能已经意识到了。您可能不知道的是,通过创建更灵活的替代方案,克服这些问题是多么容易。
关于这个主题已经有很多文章了,为什么还要再写一篇呢?嗯,解释如何做某事的教程永远不嫌多。我的目标是以一种易于剪切和粘贴的方式呈现,您可以立即使用,并提供对架构的一些见解。
内置配置
概述
我将非常快速地介绍 .NET 的内置配置支持,因为您很可能已经熟悉它。每个 Web 应用程序的根目录都有一个名为 web.config 的文件。除了包含应用程序行为的配置设置外,web.config 还定义了一个名为 appSetting
的特殊节,它允许您创建自己的键值设置。一个示例 appSetting
节可能如下所示:
1: <appSettings>
2: <add key="recursiveCopy" value="true" />
3: <add key="maxDepth" value="5" />
4: <add key="sourceServer" value="10.1.1.2" />
5: <add key="sourceServerName" value="BlueBelle" />
6: <add key="server1" value="10.0.22.3" />
7: <add key="serverName1" value="CottonCandy" />
8: <add key="server2" value="10.0.22.4" />
9: <add key="serverName2" value="Butterscotch" />
10: </appSettings>
此节定义了多个键,每个键都有一个指定的值。在我们的代码中,这些值可以通过 System.Configuration.ConfigurationSettings.AppSettings
静态属性轻松检索,例如,要检索与 maxDepth
键关联的值,我们将使用:
1: //notice how these aren't strongly typed and have to be cast
2: int maxDepth = Convert.ToInt32(ConfigurationSettings.AppSettings["maxDepth"]);
如果某个值可能会在不同安装之间发生变化(例如,您的开发环境的数据库连接字符串几乎肯定与您客户的不同),那么将其放置在可以轻松更改的位置是一个非常基本的要求。
问题
毫无疑问,上述代码非常易于使用,并且通常非常充分。但是,只需少量类,我们就可以构建一个同样简单且灵活得多的替代方案。您可能会问我“灵活”是什么意思,或者当前模型如何不灵活。我认为当前的配置模型存在三个主要问题:
- AppSettings 是一个共享空间。因此,如果您出售一个论坛,它期望在
AppSettings
ConnectionString
键中找到其数据库连接字符串,而您的客户已经在使用该键指向不同的数据库,那么您的客户必须更改他或她的代码以适应您的代码。如果他们不能这样做,例如因为他们正在使用另一个期望其ConnectionString
在那里的第三方组件,那么您就失去了一笔销售。 - 没有强类型。
appSetting
中的每个值都是一个string
——如果您想要一个整数、一个日期或一个序列化对象,您必须每次访问这些值时都将它们从string
转换。此外,ConfigurationSettings.AppSettings
只是一个NameValueCollection
,所以您可以传入任何值作为值,它会编译——但不要指望它能运行。我对那些本可以强类型但却不是的代码有严重的问题。它给您(开发者)带来了相当大的负担,而不是您正在使用的工具(intellisense 和编译器)。 - 不支持复杂对象。这与第二点相关,但在
AppSetting
节中不可能有复杂对象(例如集合)。我在编写代码以允许我们的网络管理员手动将图像同步到一个(或所有)四个生产图像服务器时遇到了这个限制。我的第一个想法是简单地添加四个键,“Server1
”、“Server2
”、“Server3
”和“Server4
”,每个键的值都是一个网络地址。但是当添加第 5 个和第 6 个服务器时会发生什么?我不仅要添加键,还需要更改和重新编译我的代码。我可以在代码中循环并检查"Server" + i.ToString()
是否存在,但这不是一个优雅的解决方案。
例如,从上面的 web.config 中,要加载 Server
对象的强类型集合,我们需要做类似以下的事情:
1: ServerCollection sc = new ServerCollection();
2: sc.Add(new Server(ConfigurationSettings.AppSettings["sourceServerName"],
ConfigurationSettings.AppSettings["sourceServer"],true));
3: sc.Add(new Server(ConfigurationSettings.AppSettings["serverName1"],
ConfigurationSettings.AppSettings["server1"],false));
4: sc.Add(new Server(ConfigurationSettings.AppSettings["serverName2"],
ConfigurationSettings.AppSettings["server2"],false));
但是如果我们需要添加另一个服务器呢?这些值是硬编码在代码中的!
本教程的目标是解决上述问题。
创建自己的基本配置节
我们将要研究的三个问题的解决方案是在 web.config 文件中创建您自己的小空间,在那里您拥有至高无上的权力。虽然 .NET 中的内置配置功能可能有些有限,但像大多数其他事情一样,它确实允许我们轻松扩展并做我们自己的事情。可以在我们的小节上使用 ASP.NET 自己的配置解析器,但这只会解决第一个问题(即,我们仍然会受制于字符串,并且无法拥有集合)。作为起点,这正是我们要做的。但首先,让我们回顾一下配置节的基础知识。
就像您可以在 web.config 中添加 AppSetting
元素一样,您也可以添加 configSections
元素。configSection
允许您定义自己的配置节的名称以及将用于解析它的处理程序。在本部分中,我们将使用与 AppSetting
相同的处理程序,所以我们只会获得隔离我们的值。首先,我们定义新节:
1: <configuration>
2: <configSections>
3: <sectionGroup name="CompanyCo">
4: <section name="Portal"
type="System.Configuration.NameValueSectionHandler,system,
Version=1.0.3300.0,Culture=neutral,
PublicKeyToken=b77a5c561934e089,Custom=null" />
5: </sectionGroup>
6: </configSections>
7: <system.web>
8: ....
9: </system.web>
10: <CompanyCo>
11: <Portal>
12: <add key="recursiveCopy" value="true" />
13: <add key="maxDepth" value="5" />
14: <add key="sourceServer" value="10.1.1.2" />
15: <add key="sourceServerName" value="BlueBelle" />
16: <add key="server1" value="10.0.22.3" />
17: <add key="serverName1" value="CottonCandy" />
18: <add key="server2" value="10.0.22.4" />
19: <add key="serverName2" value="Butterscotch" />
20: </Portal>
21: </CompanyCo>
22: </configuration>
所有自定义节都使用 <configSections>
元素 [行: 2] 定义。在此节中,可以有 0 个或多个 <sectionGroup>
元素,这些元素也可以嵌套,以及 0 个或多个 <section>
元素。sectionGroups
的目的是允许您进行更进一步的组织。如您所见,对于我们的自定义节,我们告诉它使用 NameValueSectionHandler
[行: 4] 来处理我们的节——这与默认的 appSetting
处理程序相同。
我们的自定义节定义在 [行: 10-21],我们使用相同的 <add key="xxx" value="yyy" />
语法。注意我们的自定义节元素 <CompanyCo>
和 <Portal>
与我们在定义 sectionGroup
[行: 3] 和 section
[行: 4] 时指定的名称之间的关系。同样,我们可以嵌套任意数量(或 0 个)的 sectionGroups
以进行进一步的组织。
使用我们自定义节中的值与简单地调用 AppConfig
略有不同:
1: NameValueCollection settings = (NameValueCollection)
ConfigurationSettings.GetConfig("CompanyCo/Portal");
2: int maxDepth = Convert.ToInt32(settings["maxDepth"]);
首先,我们通过调用 GetConfig
并传入我们节的路径 [行: 1] 来获取一个 NameValueCollection
(类似于 hashtable
,但只支持 string
)。接下来,我们通过键名访问值 [行: 2]。
我们在这里所做的只是将我们的配置值隔离到它们自己的小节中。这是一个很好的开始,也是理解节如何工作的关键。在下一节中,我们将真正开始行动。
创建自己的配置节和处理程序
配置处理程序
我们真正想要的是创建自己的配置处理程序。这将允许我们干净地抽象解析逻辑,并支持更丰富的对象。.NET 框架提供了一个非常方便的接口 System.Configuration.IConfigurationSectionHandler
,它定义了一个单一方法来帮助我们做到这一点。下面是一个示例:
1: public class SampleConfigurationHandler : IConfigurationSectionHandler {
2: public object Create(object parent, object context, XmlNode node) {
3: SampleConfiguration config = new SampleConfiguration();
4: config.LoadValues(node);
5: return config;
6: }
7: }
从我们的角度来看,这段代码并没有做太多事情。一个新的 SampleConfiguration
类被创建 [行: 3](我们稍后会看到)。LoadValues
被调用 [行:4],这是真正的工作完成的地方,然后返回配置 [行: 5]。
尽管上面的代码相当基础,但它实际上是连接自定义处理程序的关键。有了这段代码,我们可以回到 web.config 中,而不是告诉 .NET 让 NameValueSectionHandler
处理我们的节,我们可以告诉它使用我们的 SampleConfigurationHandler
:
1: <configSections>
2: <sectionGroup name="CompanyCo">
3: <section name="Portal"
type="ConfigurationSample.SampleConfigurationHandler,ConfigurationSample" />
4: </sectionGroup>
5: </configSections>
如果您不熟悉 .NET 中类型的工作方式,它们的 string
表示(我们在这里使用的)格式是 {FullNamespace}.{ClassName},{AssemblyName}。不要将 .DLL 添加到 {AssemblyName}。您还可以添加其他信息,例如 Version
、Culture
和公共密钥。您可以在 此处 阅读更多关于 AssemblyName
的信息。接下来,我们可以在 web.config 中创建一个更漂亮的节:
1: <CompanyCo>
2: <Portal recursiveCopy="true" maxDepth="5">
3: <servers>
4: <clear />
5: <add name="BlueBell" address="10.1.1.2" isSource="true" />
6: <add name="CottonCandy" address="10.0.22.3" isSource="false" />
7: <add name="Butterscoth" address="10.0.22.4" isSource="false" />
8: <add name="Snuzzle" address="10.0.22.5" isSource="false" />
9: <add name="Minty" address="10.0.22.6" isSource="false" />
10: </servers>
11: </Portal>
12: </CompanyCo>
希望您已经认为这是一种更简洁、更灵活的处理集合的方式——我们不必用整数唯一标识键。我还添加了几个新值,recursiveCopy
和 maxDepth
,以展示我们如何不暴露强类型值(一个布尔值和一个整数)。
示例配置
为了实现这一点,我们将创建 SampleConfiguration
类并定义所有我们需要的字段和属性:
1: public class SampleConfiguration {
2: #region fields and properties
3: private ServerCollection servers;
4: private int maxDepth;
5: private bool recursiveCopy;
6:
7: public ServerCollection Servers {
8: get { return servers; }
9: set { servers = value; }
10: }
11:
12: public int MaxDepth {
13: get { return maxDepth; }
14: set { maxDepth = value; }
15: }
16:
17: public bool RecursiveCopy {
18: get { return recursiveCopy; }
19: set { recursiveCopy = value; }
20: }
21: #endregion
22:
23:
24: #region Constructors
25: public SampleConfiguration() {}
26: #endregion
目前为止没什么特别的。但在 LoadValues()
中,事情变得有趣多了:
1: internal void LoadValues(XmlNode node) {
2: XmlAttributeCollection attributeCollection = node.Attributes;
3: recursiveCopy =
Convert.ToBoolean(attributeCollection["recursiveCopy"].Value);
4: maxDepth = Convert.ToInt32(attributeCollection["maxDepth"].Value);
5: foreach (XmlNode c in node.ChildNodes) {
6: switch (c.Name){
7: case "servers":
8: LoadServers(c);
9: break;
10: }
11: }
12: }
现在我们开始看到我们努力的成果。记住,LoadValues
是从我们的 Handler
中调用的,并接收一个 XmlNode
[行: 1],它是我们配置的节点。从这个节点,我们可以访问我们节的所有值。我们做的第一件事是将 recursiveCopy
[行: 2] 和 maxDepth
[行: 3] 分配给我们类的相应字段。由于这些只是 web.config 中 <Portal>
元素的属性,因此它们是从节点的属性集合中读取的。
接下来,我们遍历任何子节点 [行: 5]。这里我们可以加载任何类型的集合、强类型对象或其他特殊处理。在这个例子中,我们将做二合一,因为 LoadServers
方法将用强类型对象填充我们的服务器集合 [行: 8]。
1: private void LoadServers(XmlNode node) {
2: servers = new ServerCollection();
3: foreach (XmlNode server in node.ChildNodes) {
4: switch (server.Name){
5: case "add":
6: Server s = new Server(server.Attributes["name"].Value,
server.Attributes["address"].Value,
Convert.ToBoolean(server.Attributes["isSource"].Value));
7: servers.Add(s);
8: break;
9: case "remove":
10: servers.Remove(server.Attributes["name"].Value);
11: break;
12: case "clear":
13: servers.Clear();
14: break;
15: }
16: }
17: }
LoadServers()
中的代码遍历 <servers>
节点的每个子节点,并执行三件事之一:清除集合(如果子节点是 <clear>
)[行: 12],从集合中移除指定的服务器 [行: 9],或添加它 [行: 6]。虽然我们同时处理集合和强类型对象,但您可以看到通过查看 add [行: 5] 的作用,执行单个强类型对象是多么容易。
我们要做的最后一件事是向我们的类添加一个静态/共享方法,以便可以轻松地从我们的代码中访问此节,这只是锦上添花:
1: public static SampleConfiguration GetConfig{
2: get {return (SampleConfiguration)
ConfigurationSettings.GetConfig("CompanyCo/Portal");}
3: }
使用它
创建我们自己的配置类和处理程序的目标之一是让开发人员更容易。我们通过将所有解析逻辑封装到一个公开整洁属性的类中来实现这一点。这本可以通过使用 appSetting
轻松实现,但我们不会解决其他问题,例如混乱且脆弱的 web.config 和共享节。看看访问值变得多么容易:
1: int maxDepth = SampleConfiguration.GetConfig.MaxDepth;
2: bool recursiveCopy = SampleConfiguration.GetConfig.RecursiveCopy;
3: ServerCollection server = SampleConfiguration.GetConfig.Servers;
您可能会对多次调用 GetConfig
感到不适,但请注意,.NET Framework 会自动缓存已解析的节。因此,LoadValues()
只会被调用一次(或在进程自行回收时调用,所有缓存都如此)。
下载
下载的是一个包含一个 Web 页面的文件,它允许您选择三种方法之一来从 web.config 获取信息并绑定到 Repeater。前两种是您可能解决问题的糟糕方法,第三种使用了我们在此处讨论的优雅解决方案。
历史
- 2004 年 9 月 3 日:初始版本
许可证
本文没有明确的许可附带,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。作者可能使用的许可证列表可在此处找到。