应用程序在运行时需要读取配置信息。在.NET环境下这种需求同样必不可少。.NET提供一种基于XML的统一配置文件。就ASP.NET而言,此文件名为web.config。应用程序在运行时能够读取该文件的具体内容。.NET同时还提供了一系列的调用,这些调用可使读取配置文件信息的过程变得相当简单,不过要实现这一点,还需要编写一些C#代码。本文中,我将要提出的解决方案可以进一步简化此过程,同时还提供了一种分层的提取机制,此方案在很大程度上与文件的XML格式无关。

此外,编程人员不必知道很多有关XML 类以及其他一些XML细节的信息。"配置服务"就是灵活体系结构的入门。如果能够正确利用配置服务,我们就可以此为基础逐步实现下列服务:

·统一的工厂服务

·相关的数据访问服务

·声明的基于信息集的分层数据集服务

·声明的组件服务

如果本文中解释配置服务时提到这些服务,指的是上述这些服务的组合。

下面就让我们开始吧,首先来看一个简单的web.config 文件,该文件中具有一些配置实体。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
     <section name="SimpleConfiguration"
              type="MySectionHandler,MyAssembly">
     </section>
   </configSections>
<SimpleConfiguration>
  <Module1>
    <section1>
      <key1>value1</key1>
      <key2>value2</key2>
    </section1>
  </Module1>
  <Module2>
    <section1>
      <key1>value1</key1>
      <key2>value2</key2>
    <section1>
  </Module2>
</SimpleConfiguration>


.NET 应该如何处理此文件


如果建立了上述配置文件,您的目的就是要读取该文件中SimpleConfiguration部分的实体信息。要想实现这一点,就应该在.NET中添加以下代码:

ArrayList modules =
(ArrayList)System.Configuration.ConfigurationSettings.
    GetConfig("SimpleConfiguration");


此行代码可以引发一系列操作。进行上述调用时,.NET 将调用名为SimpleConfiguration的类(在文件中的 configSections部分)并且通过将一个XML结点传递给那个创建方法来调用熟知的Create方法。不论此Create方法返回何种类型的对象,该对象都将会传递给GetConfig(...)的调用者。为了给出一个更加完整的示例,下面就介绍一个此类型处理器的示例代码:

public class MySectionHandler : IConfigurationSectionHandler
{
    public object Create(object parent, object configContext, 
XmlNode section)
   {
      ... Read your xmlnode section
      ... return an ArrayList
   } // end of method
} // end of class


客户端与处理同种对象的section 处理器之间必须达成一致。在这种情况下,该对象就是指ArrayList。

如何简化这种方法


上述方法有不少优点。调用者可获得一个C# 对象,此对象与每一个XML section相对应。您可以拥有数目不限的XML section。另一方面,为了简便起见,您需要做到:

1.为每一个 section 编写一个类。

2.解析 XML section 以便将其转变成相应的对象。

客户端常常希望拥有一个更简单的配置接口。下面就是这样一个简单的示例:

string value = AppServices.getValue(
"/SimpleConfiguration/Module1/section1/key1");

一旦我们有了这样一个可以使用的API,就不必再创建新的section处理器了。至少这一点对于简单的配置而言是可以实现的。尽管我认为这只是是简化操作的开始,但我还是对能进一步简化配置而感到惊讶。现在,还是让我们先来对此API再进行一次扩充。

您经常会发现一些配置关键字是必选的。请务必牢记这一点,如果一个关键字未找到,AppServices.getValue(...)将会抛出一个异常,而且,客户端可能会指定一个默认值来应对找不到关键字的情况。下面是此API的重载版本,这个版本中将上面所述的情况考虑在内:

string value = AppServices.getValue(
"/SimpleConfiguration/Module1/section1/key1",
"defaultvalue");


API的这个版本在找不到关键字时,不但不会引发异常,相反将会返回已设置的默认值。例如,您可以进行如下操作:

string value =
AppServices.getValue(
"/SimpleConfiguration/Module1/section1/key1", null);
if (value == null)
{
    // you have just avoided an exception if you care to do so
}


通过降低错误率来使关键字不区分大小写也是完全可能的。此方法与Xpath相类似。注意到这一点,下面是该API的又一个版本:

string xpathExpression = ".";
string value = AppServices.getXPathValueString(
xpathExpression);


有时候您可能会遇到一个给定的key对应多个结点。这种情况下,您可能希望收到的结点为XML结点。要实现这一点,可以进行如下操作:

XMLNode node = AppServices.getXPath(xpathExpression);


我们再回到基本的AppServices.getValue(key)方法。有时,虽然这里的key是在XML文件中指定的,但它的内容却可能是空白符号。这种情况下,可将该key视为根本不存在。同样,也可以选择让API抛出一个异常。如果要兼顾到此种变化,可以调用另一函数:

string value = AppServices.getValueHonourWhiteSpace(key);


概括一下,截止目前我们已经得到了下列这些函数:

Public class AppServices
{
    // Configuration Service
    public static getValue(string key);
      // throws an exception if the key is not found or has an 
     // empty string
    public static getValueHonourWhiteSpace(string key)
      // throws an exception if the key is not found
    public static getValue(string key, string default);
      // returns the default if the key is not found or has an 
     // empty string
    public static getValueHonourWhiteSpace(string key,string default)
      // returns the default if the key is not found
    //Xpath support
    public static getXPathValue(string key);
       // throws an exception if the key is not found or has an 
      // empty string
    public static getXPathValueHonourWhiteSpace(string key)
      // throws an exception if the key is not found
    public static getXPathValue(string key, string default);
      // returns the default if the key is not found or has an 
      // empty string
    public static getXPathValueHonourWhiteSpace(string key, string default)
     // returns the default if the key is not found
    // Other future services
}


我们在这里所采取的一切方法就是为了真正实现简化。具体实现时,可根据情况选择实现上述API之中的一部分。

实现配置服务


将配置服务应用到一个实际的示例,我们将会对相关内容有更清晰的理解。在这个示例中,我们将在Web应用程序中发送电子邮件。可能您已经有了如下所示的一个API :

public static void sendMail(string from, string to, string subject, string body, 
string format);


如果使用此 API,则客户端代码将是:

public static trivialMailExample()
    {
      string from="support@johndoeinc.com";
      string to = "johndoe2@customerinc.com";
      string subject = "Your order is ready";
      string body="You can pick it up at facility1";
      string format = "plain-text";
      sendMail(from,to,subject,body,format);
    }


正如您所看到的,很难对电子邮件的所有参数进行编码以便嵌入到Web站点中。如果我们想从配置文件中读取这样的一些参数又会怎么样呢?

...
<SimpleConfiguration>
    <EmailInfo>
      <from>support@johhdoeinc.com
      <subject>Your order is ready. Order number {0}</subject>
      <body> <![[CDATA
<html>
<body>
    <p>Here are your order details</p>
    {0}
</body>
</html>
      ]]>
      </body>
      <format>html</format>
    </EmailInfo>
</SimpleConfiguration>


只要此配置适当,就可以将先前的方法更改为下面所示的方法:

public static trivialMailExampleEnabledForConfig(string toCustomer, string 
orderId)
    {
      string from = AppServices.getValue("/SimpleConfiguration/from"); 
        // an error not to have it
      string subject = 
string.Format(AppServices.getValue("/SimpleConfiguration/from"), orderId);
      string body = 
string.Format(AppServices.getValue("/SimpleConfiguration/from"), orderId);
      string format = 
AppServices.getValue("/SimpleConfiguration/from","plain-text"); 
      // defaults to plain-text
      sendMail(from,toCustomer,subject,body,format);
    }

更改配置文件是比较容易的一件事情,这样就可以修改电子邮件的具体外表形式。请注意,如何使用string.Format(...) 将动态值替换到从配置文件中读取出来的模板中。另请注意,如何使用CDATA 部分将HTML嵌入到 XML 文件之间。

将公共静态服务转变成接口


在讲述如何实现这些方法之前,我们先来解释如何将一个静态方法转变成为一个接口。具体理由如下:AppServices 是一个接口集,其中的每个接口均表示一种服务。例如:

public AppServices
{
    public static IConfig getConfigurationService();
    pulbic static IFacgtory getFactoryService();
    public static ILog getLoggingService();
    ...any other application level services
}


我们来看看 IConfig可能包含的内容:

public interface IConfig
{
    public static getValue(string key);
      // throws an exception if the key is not found or has an 
      // empty string
    public static getValueHonourWhiteSpace(string key)
      // throws an exception if the key is not found
    public static getValue(string key, string default);
      // returns the default if the key is not found or has an 
      // empty string
    public static getValueHonourWhiteSpace(string key, string default)
      // returns the default if the key is not found
}


我们可能拥有另一支持XPath 的接口,如下所示:

public interface IConfigXPath
{
    //Xpath support
    public static getXPathValue(string key);
      // throws an exception if the key is not found or has an 
     // empty string
    public static getXPathValueHonourWhiteSpace(string key)
      // throws an exception if the key is not found
    public static getXPathValue(string key, string default);
      // returns the default if the key is not found or has an 
      // empty string
    public static getXPathValueHonourWhiteSpace(string key, string default)
      // returns the default if the key is not found
}


现在,我们继续操作,将会得到一个如下所示的实现:

public class DefaultConfig : IConfig, IXPathConfig
{
    ... Implements all the methods
}


现在对 AppServices 进行编码,如下所示:

public class AppServices
{
    private IConfig m_config = new DefaultConfig(); 
      // Potentially one can get this from a factory
    public static getIConfig() { return m_config; }
    ... and others
}


为什么要将静态方法转变成接口呢?这是因为对于一些情况,区分大小写是很有帮助的,但是对于另外一些情况,就没有必要区分大小写。而接口则可以很好地支持这两种情况:

public class CaseSensitiveConfig : IConfig
{
    //... implement your keys with case sensitive
}
public class CaseInsensitiveConfig : IConfig
{
    //... implement your keys with case insensitive
}


应用程序服务在运行时将根据实际情况返回一个适当的实现。如果您想知道如何实现,可以参考工厂服务。或者您可以阅读一些参考文献。下面我们演示一个更完整的示例:

public class CaseInsensitiveMultiFileConfig : IConfig
{
    // Implement your keys with case insensitivity and 
    // read from multiple config files
}


我们一直在强调配置文件的重要性,用不了多久,这种想法就会得到广泛应用。XML 配置文件将会随处可见。在一个团队中,可能需要将配置信息拆分成多个文件,以便增强团队成员之间的合作。

到目前为止,我们已经讨论了配置服务的优点以及如何使用配置服务的内容。这些内容应该说是经验的积累。接下来我们来看一些具体的实现。

实现


方法是足够简单了。所有的配置均可以在一个section中设置。诸如 DefaultConfig 这样的类可以读取此section中的数据内容。一旦获取了所需的数据内容,DefaultConfig 类就可以遍历XML文档中的每一个结点,并在数据字典或哈希表中为每一个关键字和值设置对应的一项。这里所提到的数据字典将可以响应客户端发送的关键字请求。

Section 处理器代码如下:

public class SimpleConfigurationSectionHandler :IConfigurationSectionHandler
    {
      public object Create(object parent, object configContext, XmlNode section)
      {
        return section;
      } // end of method
    } // end of class


DefaultConfig代码:

public class DefaultConfig : IConfig {
// keep a dictionary of values
private IDictionary m_keyValuePairs
// implement methods of IConfig using the above 
// dictionary details left to you
//  Constructor, where it reads your SimpleConfiguration 
// XML node using the section handler above
    public DefaultConfig()
    {
      // read the xml section for general config
      // Section name: SimpleConfiguration
      XmlNode xmlNode =
        (XmlNode)System
          .Configuration
          .ConfigurationSettings
          .GetConfig("SimpleConfiguration");
      if(xmlNode != null)
      {
      m_keyValuePairs = createDictionary(m_genConfigXmlNode);
      }
    }
}
// Here is the createdictionary
    private IDictionary createDictionary(XmlNode genConfigXmlNode)
    {
      IDictionary ht = new Hashtable();
      if(genConfigXmlNode != null && 
        genConfigXmlNode.ChildNodes != null &&
        genConfigXmlNode.ChildNodes.Count > 0)
    {
      // walk through each node
      // if it is a leaf node add it to the hash table
      // if it is not  continue the process
      walkTheNode(ht,"",genConfigXmlNode);
      }
      return ht;
    }
//  Here is how you walk the nodes recursively 
// to get your keys
private void walkTheNode(IDictionary ht, string parent, XmlNode node)
{
    if(node != null)
   {
     if (node.NodeType == XmlNodeType.Comment)
    {
      return;
    }
    if (node.HasChildNodes == false)
    {
      if (node.NodeType == XmlNodeType.Text)
      {
        // no children
        string leaf = node.Value;
        ht.Add(parent.ToLower(),leaf);
        // end of the recursive call
       return;
      }
     else if (node.NodeType == XmlNodeType.CDATA)
     {
        XmlCDataSection cdataSection = (
        System.Xml.XmlCDataSection)node;
        string leaf = cdataSection.Data;
        ht.Add(parent.ToLower(),leaf);
        // end of the recursive call
        return;
     }
     else
        {
        string key = parent + "/" + node.Name;
        string val = "";
        ht.Add(key.ToLower(), val);
        return;
        }
     }
     else
    {
      string newparent = parent + "/" + node.Name;
      // has children
      // walk all the children
     for(int i=0;i<node.ChildNodes.Count;i++)
     {
        // recursive call
       walkTheNode(ht,newparent,node.ChildNodes[i]);
     }
    }
  }
}


提示:编码时请特别关注如何处理 CDATA 和空文本结点。

在下一篇文章中,我们将在这种简单的配置服务基础之上对其进行扩展构成工厂服务FactoryService。工厂服务可以使您的体系结构变得更加灵活。