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