Custom Xml Serialization and Storage of
Classes in Standard Configuration Files

by Peter A. Bromberg, Ph.D.

Peter Bromberg

"When you play music you discover a part of yourself that you never knew existed." -- Bill Evans

In my regular "day job" I engineer a fair amount of highly customized Windows Services. Some of these services run multiple simultaneous threads, each thread needing a set of configuration settings to enable it to know the details about it's specific operation. While each thread of operation in a particular service does essentially the same thing, it still needs to do so based on a completely different group of settings.

Most programmers know that after you have done something a few times, you start thinking about refactoring and adding "helper" objects to make your job easier. You want to provide yourself with object-oriented, code-reuse promoting classes that you can cobble together into a new project whenever needed. This sample code of serializing, storing and retrieving custom configuration sections based on a specified settings class is an example of one of these efforts.



My philosophy in architecting applications always starts with "Don't reinvent the wheel". Because of this, I normally take some time to Google or MSN my subject matter to see how many others may have done something. In this case, I found an article on CodeProject.com by Brady Gaster that gave me a couple of good ideas.

The particular example I present here comes from a SQLProcessor Engine I wrote that essentially pulls custom messages from a Message Queue, creates and then executes stored procedure calls from them. That's its sole purpose in life - to listen on a specified queue, grab any messages that arrive, and quickly process them. Each message contains specially desigined patterns containing everything needed to set up the sproc and connection, populate the parameter names and values, and execute the sproc using ADO.NET. These messages are dropped off into the queue by a different process that needs to be able to deposit large numbers of messages over a very short period of time, has brief periods of relative quietness, and then may start up again. This process cannot take the time to attempt to make stored procedure calls when it gets busy - hence we use the MSMQ Queues as a sort of buffer mechanism.

So I start out with a SQLProcessorEngine class that describes the settings for each of these engine threads:

using System;
using System.Xml.Serialization;

namespace XmlSerializationConfig
{
 /// <summary>
 /// Example class to place into the 
 /// configuration file.
 /// </summary>
 /// 
 [XmlRoot("SQLProcessorEngine")]
 public class SQLProcessorEngine
 {
  private string _engineName = String.Empty;
  private string _connectionString = String.Empty;
  private string _msmqPath = String.Empty;
  private string _errorQueuePath = String.Empty;
  private string _pairDelimiter = String.Empty;
  private string _nameValueDelimiter = String.Empty;
  private string _sysLogIp = String.Empty;
  private string _loggingLevel = String.Empty;
  private string _statusMessageInterval = String.Empty;

  public SQLProcessorEngine()
  {
  }

  [XmlAttribute(DataType = "string", AttributeName = "EngineName")]
  public string EngineName
  {
   get { return _engineName; }
   set { _engineName = value; }
  }

  [XmlAttribute(DataType = "string", AttributeName = "ConnectionString")]
  public string ConnectionString
  {
   get { return _connectionString; }
   set { _connectionString = value; }
  }

  [XmlAttribute(DataType = "string", AttributeName = "MsmqPath")]
  public string MsmqPath
  {
   get { return _msmqPath; }
   set { _msmqPath = value; }
  }

  [XmlAttribute(DataType = "string", AttributeName = "ErrorQueuePath")]
  public string ErrorQueuePath
  {
   get { return _errorQueuePath; }
   set { _errorQueuePath = value; }
  }

  [XmlAttribute(DataType = "string", AttributeName = "PairDelimiter")]
  public string PairDelimiter
  {
   get { return _pairDelimiter; }
   set { _pairDelimiter = value; }
  }

  [XmlAttribute(DataType = "string", AttributeName = "NameValueDelimiter")]
  public string NameValueDelimiter
  {
   get { return _nameValueDelimiter; }
   set { _nameValueDelimiter = value; }
  }

  [XmlAttribute(DataType = "string", AttributeName = "SysLogIp")]
  public string SysLogIp
  {
   get { return _sysLogIp; }
   set { _sysLogIp = value; }
  }

  [XmlAttribute(DataType = "string", AttributeName = "LoggingLevel")]
  public string LoggingLevel
  {
   get { return _loggingLevel; }
   set { _loggingLevel = value; }
  }

  [XmlAttribute(DataType = "string", AttributeName = "StatusMessageInterval")]
  public string StatusMessageInterval
  {
   get { return _statusMessageInterval; }
   set { _statusMessageInterval = value; }
  }
 }
}

My configuration file will have a custom ConfigSection to hold serialized "instances" of these, like so:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <configSections>
  <section name="SQLProcessorEngines" 
   type="XmlSerializationConfig.ConfigHandler, XmlSerializationConfig" />
 </configSections>
 <SQLProcessorEngines>
<SQLProcessorEngine  EngineName = "One"
  ConnectionString = "server=10.10.243.18;database=sss;user id=x;password=x"
  MsmqPath = "FORMATNAME:DIRECT=TCP:10.10.243.18\Private$\sqlprocessor" 
  ErrorQueuePath = "FORMATNAME:DIRECT=TCP:10.10.243.18\Private$\sqlerror"
  PairDelimiter = "~"
  NameValueDelimiter = "|"
  SysLogIp = "10.10.9.18"
  LoggingLevel = "Verbose"
  StatusMessageInterval =  "10" />
  
   <SQLProcessorEngine  EngineName = "Two"
  ConnectionString = "server=10.10.9.23;database=sss;user id=xpassword=;"
  MsmqPath = "FORMATNAME:DIRECT=TCP:10.10.9.23\Private$\sqlprocessor" 
  ErrorQueuePath = "FORMATNAME:DIRECT=TCP:10.10.9.23\Private$\sqlerror"
  PairDelimiter = "~"
  NameValueDelimiter = "|"
  SysLogIp = "10.10.9.23"
  LoggingLevel = "Exceptions"
  StatusMessageInterval =  "5" />   
 </SQLProcessorEngines>
 
 <appSettings>
<add key="test" value = "12345" />
</appSettings>
</configuration>

Now that we have the infrastructure for our sample covered, I need two new classes to manipulate this data. One, AppConfig.cs, comes from a previous article here and is used to add and save appSettings elements from a standard config file. I've added a new method, CreateNewSectionBlock, to allow for the adding of a new SQLProcessorEngine section element by specifying the section name and passing in a fully populated instance of the SQLProcessorEngine class:

using System;
using System.Configuration;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;

namespace XmlSerializationConfig
{
 public class AppConfig : AppSettingsReader
 {
  public string docName = String.Empty;
  private XmlNode node = null;
  public XmlDocument configDoc = new XmlDocument();
  public bool IsConfigLoaded = false;

  public AppConfig()
  {
   LoadConfigDoc();
   IsConfigLoaded = true;
  }

  public bool CreateNewSectionBlock(string sectionName, object o)
  {
   XmlSerializer serializer = new XmlSerializer(o.GetType());
   StringWriter stringWriter = new StringWriter();
   // Create an instance of an xml text writer, with no formatting. 
   // Then write something (actually nothing) to the writer 
   // in order to set the internal state to something other than the 
   // start state, which omits the xml declaration. 
   XmlTextWriter xmlWriter = new XmlTextWriter(stringWriter);
   xmlWriter.Formatting = Formatting.None;
   xmlWriter.WriteRaw("");
   // Faking the existence of custom namespaces has a nice side effect 
   // of leaving namespaces out entirely. 
   XmlSerializerNamespaces faker = new XmlSerializerNamespaces();
   faker.Add("", null);
   serializer.Serialize(xmlWriter, o, faker);
   if (!IsConfigLoaded)
    LoadConfigDoc();
   AppConfig cfg = new AppConfig();
   XmlDocumentFragment frag = this.configDoc.CreateDocumentFragment();
   frag.InnerXml = stringWriter.ToString();
   node = this.configDoc.SelectSingleNode("//" + sectionName);
   node.AppendChild(frag);
   this.saveConfigDoc(configDoc, Environment.CurrentDirectory +
@"\" + this.docName); return true; } public bool SetValue(string key, string value) { if (!IsConfigLoaded) LoadConfigDoc(); // retrieve the appSettings node node = configDoc.SelectSingleNode("//appSettings"); if (node == null) { throw new InvalidOperationException("appSettings section not found"); } try { // XPath select setting "add" element that contains this key XmlElement addElem =
(XmlElement) node.SelectSingleNode("//add[@key='" + key + "']"); if (addElem != null) { addElem.SetAttribute("value", value); } // not found, so we need to add the element, key and value else { XmlElement entry = configDoc.CreateElement("add"); entry.SetAttribute("key", key); entry.SetAttribute("value", value); node.AppendChild(entry); } //save it saveConfigDoc(configDoc, docName); return true; } catch { return false; } } private void saveConfigDoc(XmlDocument cfgDoc, string cfgDocPath) { try { XmlTextWriter writer = new XmlTextWriter(cfgDocPath, null); writer.Formatting = Formatting.Indented; cfgDoc.WriteTo(writer); writer.Flush(); writer.Close(); return; } catch { throw; } } public bool removeElement(string elementKey) { try { if (!IsConfigLoaded) LoadConfigDoc(); // retrieve the appSettings node node = configDoc.SelectSingleNode("//appSettings"); if (node == null) { throw new InvalidOperationException("appSettings section not found"); } // XPath select setting "add" element that contains this key to remove node.RemoveChild(
node.SelectSingleNode("//add[@key='" + elementKey + "']")); saveConfigDoc(configDoc, docName); return true; } catch { return false; } } private void LoadConfigDoc() { // load the config file docName = ((Assembly.GetEntryAssembly()).GetName()).Name; docName += ".exe.config"; configDoc.Load(docName); } } }

Note the documentation in the class shows how to "Fake" a custom namespace and get rid of all the extraneous related attributes which are not needed for a "POCF" (Plain Old Configuration File).

My other class is called ConfigHandler, and it handles reading and serialization of the settings from the custom ConfigurationSection:

using System;
using System.Configuration;
using System.Xml;
using System.Xml.Serialization;

namespace XmlSerializationConfig
{
 /// <summary>
 /// The custom configuration class. This will read the custom 
 /// configuration section and for each child node, return an
 /// instance of the object, deserialized. 
 /// </summary>
 public class ConfigHandler : IConfigurationSectionHandler
 {
  private SQLProcessorEngine[] __engines = new SQLProcessorEngine[0];
  public object Create(object parent,
                       object configContext,
                       XmlNode section)
  {
   XmlNodeList nodes = section.SelectNodes("//SQLProcessorEngine");
   for (int i = 0; i < nodes.Count; i++)
   {
    XmlNodeReader rdr = new XmlNodeReader(nodes[i]);
    XmlSerializer ser = new XmlSerializer(typeof (SQLProcessorEngine));
    SQLProcessorEngine eng = (SQLProcessorEngine) ser.Deserialize(rdr);
    Add(eng);
   }
   return __engines;
  }

  /// <summary>
  /// Adds an Engine to the Collection. 
  /// </summary>
  /// <param name="eng"></param>
  private void Add(SQLProcessorEngine eng)
  {
   SQLProcessorEngine[] tmp =
new SQLProcessorEngine[__engines.Length + 1]; Array.Copy(__engines, 0, tmp, 0, __engines.Length); tmp[__engines.Length] = eng; __engines = tmp; } public static SQLProcessorEngine[] GetEngines() { return (SQLProcessorEngine[])
ConfigurationSettings.GetConfig("SQLProcessorEngines"); } } }

 

Finally, I've added a Client Console app that "exercises" the whole thing. First it displays the appSettings "Test" element. Then, it reads the two default SQLProcessorEngine sections and displays a few class field values:

 

private static void ReadEnginesFromConfiguration()
{
SQLProcessorEngine[] engineArr = ConfigHandler.GetEngines();

foreach (SQLProcessorEngine p in engineArr)
{
Console.WriteLine("{0} {1}", p.EngineName, p.MsmqPath);
}
}

To complete the exercise, it ADDS a new SQLProcessor engine block from a new SQLProcessorEngine class instance, serializes it into the config file, and saves the file with the new block we've added. Then, it displays the OuterXml of the finished config file:

You can use this same technique with web.config configuration files as well. All you would really need to do is modify the path to read Server.MapPath("web.config");

Just bear in mind that when you save a modified web.config, your ASP.NET application will restart.

Download the Solution that accompanies this article

 



Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: