Creating an Object Configuration Section in .Net 2.0

by Jon Wojtowicz

Some time ago Craig Andera wrote an excellent article entitled The Last Configuration Section Handler I'll Ever Need which outlines a really nice and flexible configuration section handler. I decided to look at how this same pattern could be applied in .Net 2.0.



The .Net 2.0 configuration classes have changed significantly. Some of the changes are being able to use external configuration files, loading remote files, accessing each section of the configuration file programmatically and saving changes back to the configuration files without going through many hoops to name a few. One big change is the deprecation of the IConfiguratioSectionHandler interface. While this will still work in the current framework the new class to use is the ConfigurationSection.

The ConfigurationSection class is the base class for all the new configuration sections. This class inherits from ConfigurationElement and provides for either a declarative programming style using property attributes to control the section structure or a strictly programmable model for complete control over the section structure.

The ObjectConfigurationSection

The base requirement was to duplicate the original XmlSerializerSectionHandler being able to deserialized an XML serialized object. The decision was made to add the ability to load the section from a remote file and signal for a reload when the file changed. The IConfigurationSectionHandler had a single method to implement, Create. The ConfigurationSection class confers an expectation that the section will be able to serialize itself when the Save method is called. This requires not only handling the deserialization but also the serialization both to the current configuration file and to a remote file.

Using Lutz Roeder's .Net Reflector to look at the decompiled MSIL for the Configuration Section class and stepping through a sample application the following sequence of calls was determined. For deserialization the DeserializeSection method was called to perform some basic validation. From there a call was made into DeserializeElement which would actually perform the deserialization of the section.

The serialization sequence was similar with an initial call into SerializeSection. This method creates the XmlWriter for creating the XML. The next call is made to SerializeToXmlElement which would write the outer section tags with the specified element name. From this method a call is made to SerializeElement which performs the actual serialization.

The default SerializeSection method creates a new instance of the configuration section and call UnMerge on the new instance. This allows for removing items that were inherited from the parent before serializing the section. Using that same call would have required copying the field values in this method. Since the ObjectConfigurationSection should not be inherited from previously defined sections the call to UnMerge was not used in thenew class.

The complete code listing for the object section handler is as follows.

/// <summary>
/// A configuration section for containing an arbitrary serialized object.
/// </summary>
public class ObjectConfigurationSection : ConfigurationSection
{
private string typeName;
private object data;
private string fileName;
private FileSystemWatcher watcher;

/// <summary>
/// The name of an external file containing the serialized object.
/// </summary>
public string FileName
{
get { return fileName; }
set { fileName = value; }
}

/// <summary>
/// Create an instance of this object.
/// </summary>
public ObjectConfigurationSection(): base()>
{}

/// <summary>
/// The name of the type that is serialized.
/// </summary>
public string TypeName
{
get { return typeName; }
}

/// <summary>
/// The contained object.
/// </summary>
public object Data
{
get { return data; }
set
{
data = value;
Type t = data.GetType();
typeName = t.FullName + ", " + t.Assembly.FullName;
}
}

#region Overrides

/// <summary>
/// Retrieves the contained object from the section.
/// </summary>
/// <returns>The contained data object.</returns>
protected override object GetRuntimeObject()
{
SetWatcher();
return data;
}

/// <summary>
/// Deserializes the configuration section in the configuration file.
/// </summary>
/// <param name="reader">The reader containing the XML for the section.</param>
protected override void DeserializeSection(System.Xml.XmlReader reader)
{
if (! reader.Read() || (reader.NodeType != XmlNodeType.Element))
{
throw new ConfigurationErrorsException("Configuration reader expected to find an element", reader);
}
this.DeserializeElement(reader, false);
}

/// <summary>
/// Deserializes the configuration element in the configuration file.
/// </summary>
/// <param name="reader">The reader containing the XML for the section.</param>
/// <param name="serializeCollectionKey">true to serialize only the collection key properties; otherwise, false.
/// Ignored in this implementation. </param>
protected override void DeserializeElement(XmlReader reader, bool serializeCollectionKey)
{
reader.MoveToContent();
// Check for invalid usage
if (reader.AttributeCount > 1)
throw new ConfigurationErrorsException("Only a single type or fileName attribute is allowed.");
if (reader.AttributeCount == 0)
throw new ConfigurationErrorsException("A type or fileName attribute is required.");
// Determine if we need to get the section from the inline XML or from an external file.
fileName = reader.GetAttribute("fileName");
if (fileName == null)
{
DeserializeData(reader);
reader.ReadEndElement();
}
else
{
if (! reader.IsEmptyElement)
throw new ConfigurationErrorsException("The section element must be empty when using the fileName attribute.");
using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read))
{
XmlReader rdr = new XmlTextReader(file);
rdr.MoveToContent();
DeserializeData(rdr);
rdr.Close();
}
}
}

/// <summary>
/// Serializes the configuration section to an XML string representation.
/// </summary>
/// <param name="parentElement">The parent element of this element.</param>
/// <param name="name">The name of the section.</param>
/// <param name="saveMode">The mode to use for saving.</param>
/// <returns>The string representation of the section.</returns>
protected override string SerializeSection(ConfigurationElement parentElement, string name, ConfigurationSaveMode saveMode)
{
StringWriter sWriter = new StringWriter(System.Globalization.CultureInfo.InvariantCulture);
XmlTextWriter xWriter = new XmlTextWriter(sWriter);
xWriter.Formatting = Formatting.Indented;
xWriter.Indentation = 4;
xWriter.IndentChar = ' ';
this.SerializeToXmlElement(xWriter, name);
xWriter.Flush();
return sWriter.ToString();
}

/// <summary>
/// Serializes the section into the configuration file.
/// </summary>
/// <param name="writer">The writer to use for serializing the class.</param>
/// <param name="elementName">The name of the configuration section.</param>
/// <returns>True if successful, false otherwise.</returns>
protected override bool SerializeToXmlElement(XmlWriter writer, string elementName)
{
if (writer == null)
return false;
writer.WriteStartElement(elementName);
bool success = true;
if (fileName == null || fileName == string.Empty)
{
success = SerializeElement(writer, false);
}
else
{
writer.WriteAttributeString("fileName", fileName);
using (FileStream file = new FileStream(fileName, FileMode.Create, FileAccess.Write))
{
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.IndentChars = ("\t");
settings.OmitXmlDeclaration = false;
XmlWriter wtr = XmlWriter.Create(file, settings);
wtr.WriteStartElement(elementName);
success = SerializeElement(wtr, false);
wtr.WriteEndElement();
wtr.Flush();
wtr.Close();
}
}
writer.WriteEndElement();
return success;
}

/// <summary>
/// Serialize the element to XML.
/// </summary>
/// <param name="writer">The XmlWriter to use for the serialization.</param>
/// <param name="serializeCollectionKey">Flag whether to serialize the collection keys. Not used in this override.</param>
/// <returns>True if the serialization was successful, false otherwise.</returns>
protected override bool SerializeElement(XmlWriter writer, bool serializeCollectionKey)
{
if (writer == null)
return false;
writer.WriteAttributeString("type", typeName);
XmlSerializer serializer = new XmlSerializer(data.GetType());
serializer.Serialize(writer, data);
return true;
}
#endregion

#region Private Methods

/// <summary>
/// Deserializes the data from the reader.
/// </summary>
/// <param name="reader">The XmlReader containing the serilized data.</param>
private void DeserializeData(System.Xml.XmlReader reader)
{
typeName = reader.GetAttribute("type");
Type t = Type.GetType(this.typeName);
reader.Read();
reader.MoveToContent();
XmlSerializer serializer = new XmlSerializer(t);
this.data = serializer.Deserialize(reader);
}

/// <summary>
/// Determines if a FileSystemWatcher needs to be set on the file
/// to watch for external changes.
/// </summary>
private void SetWatcher()
{
if (this.SectionInformation.RestartOnExternalChanges
&& fileName != null
&& fileName != string.Empty)
{
if (watcher == null)
{
FileInfo configFile = new FileInfo(fileName);
watcher = new FileSystemWatcher(configFile.DirectoryName);
watcher.Filter = configFile.Name;
watcher.NotifyFilter = NotifyFilters.LastWrite;
}
watcher.Changed += new FileSystemEventHandler(OnConfigChanged);
watcher.EnableRaisingEvents = true;
}
}

/// <summary>
/// Handle a change event from the FileSystemWatcher.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void OnConfigChanged(object sender, FileSystemEventArgs e)
{
watcher.EnableRaisingEvents = false;
watcher.Changed -= new FileSystemEventHandler(OnConfigChanged);
ConfigurationManager.RefreshSection(this.SectionInformation.Name);
}

#endregion

}

The design only allows for the fileName or type attribute to be present in the root element. Specifying the fileName will cause the the remote file to be loaded. Setting the restartOnExternalChanges to true will cause a FileSystemWatcher to be enabled to watch the file for changes. If the remote file changes the class will tell the ConfigurationManager to reload the next time the object data is requested.

Usage Examples

The following code will save a ConfigObject in the local exe configuration file.

Configuration< config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
ObjectConfigurationSection section = new ObjectConfigurationSection();
config.Sections.Add(sectionName, section);
section.SectionInformation.ForceSave = true;
ConfigObject o = new ConfigObject();
section.Data = o;
config.Save();

The following will save a ConfigObject in a remote file specified by the user.

ObjectConfigurationSection section = new ObjectConfigurationSection();
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
config.Sections.Add(sectionName, section);
section.SectionInformation.ForceSave = true;
ConfigObject o = new ConfigObject();
section.Data = o;
section.FileName = fileName;
config.Save();

The following will retrieve the data from the configuration section.

ConfigObject o = (ConfigObject)ConfigurationManager.GetSection(sectionName);

Working with the new ConfigurationSection class is not much more difficult than working with the IConfigurationSectionHandler interface. The new class provides additional functionality and flexibility that was not previously available. Maybe this will truly be the last section handler you'll need.

Download the Visual Studio.NET Solution that accompanies this article

Jon Wojtowicz is a C# MVP and a Systems Analyst at a large insurance company in Chattanooga, TN where he currently provides developer support and internal training. He has worked as a consultant working with Microsoft Technologies. This includes ASP, COM, VB6 and .Net, both C# and VB.Net since Beta 1. He has been an MCSD since 1999 and an MCT since 2000. Prior to getting a degree in Computer science he worked as a process engineer focusing on process automation, programmable controllers and equipment installations. In his spare time he likes woodworking and gardening.
Article Discussion: