SYSK 260: Custom Configuration Settings Just Like Ones Used in .NET Framework Classes
.NET 2.0 settings designer that generates a strongly typed class representing the applicationSetting and userSettings sections is a great improvement over the old appSettings or custom xml serialization code… But, sometimes you just need a bit more…
If you see the configuration below, what would you expect the output to be?
<add serverName="server1">
<services>
<add serviceName="Dhcp" started="true" />
<add serviceName="MSSqlServer" started="true" />
<add serviceName="vds" started="true" />
</services>
</add>
<add serverName="server2">
<services copyFrom="server1">
<override serviceName="MSSqlServer" started="false" />
<remove serviceName="vds" />
</services>
</add>
Of course, you’d expect something like this:
· On server1, services Dhcp, MSSqlServer and vds should be started
· On server2, Dhcp should be started, MSSqlServer should notbe started, and we don’t care about vds.
Now, the question is, how do you create a strongly typed configuration classes that would figure out all the add, remove and override keywords, and allow the client to use it in a very intuitive way… something like this:
ServiceMonitorSettingsSection settings = ServiceMonitorSettingsSection.Settings;
foreach (ServerSettings server in settings.Servers)
{
foreach (ServiceSettings service in server.Services)
{
System.Diagnostics.Debug.WriteLine(string.Format("Service {0} is expected to be {1}started on server {2}",
service.ServiceName, service.Started == true ? "" : "not ", server.ServerName));
}
}
Below is the code that does just that!
Here is the full configuration file:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="ServiceMonitorSettings" type="WindowsApplication1.ServiceMonitorSettingsSection, WindowsApplication1" />
</configSections>
<ServiceMonitorSettings>
<servers>
<add serverName="server1">
<services>
<add serviceName="abc" started="true" />
<add serviceName="abc2" started="true" />
</services>
</add>
<add serverName="server3">
<services copyFrom="server1">
<override serviceName="abc" started="false" />
<remove serviceName="abc2" />
</services>
</add>
</servers>
</ServiceMonitorSettings>
</configuration>
And here is the actual code:
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Xml;
namespace WindowsApplication1
{
#region ServiceMonitorSettingsSection
public sealed class ServiceMonitorSettingsSection : ConfigurationSection
{
private readonly ConfigurationProperty _servers =
new ConfigurationProperty("servers", typeof(ServerCollection), null,
ConfigurationPropertyOptions.IsRequired);
public static ServiceMonitorSettingsSection Settings
{
get
{
ServiceMonitorSettingsSection result = null;
try
{
result = (ServiceMonitorSettingsSection)System.Configuration.ConfigurationManager.GetSection("ServiceMonitorSettings");
if (result == null)
throw new ApplicationException("Configuration file is missing 'ServiceMonitorSettings' section", null);
else
{
// Implement copyFrom
foreach (ServerSettings serverSettings in result.Servers)
{
string copyFrom = serverSettings.Services.CopyFrom;
if (copyFrom != null && copyFrom.Trim().Length > 0)
{
ServerSettings from = result.Servers[copyFrom];
if (from != null)
{
foreach (ServiceSettings fromService in from.Services)
{
serverSettings.Services.Add(fromService.Clone());
}
// All done -- set to read only
serverSettings.Services.SetInitialized();
}
else
{
throw new ApplicationException(string.Format("Invalid copyFrom setting. Server {0} doesn't exist in configuration file", copyFrom), null);
}
}
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debugger.Break();
// TODO: log and rethrow
}
return result;
}
}
public ServiceMonitorSettingsSection()
{
}
[ConfigurationProperty("servers", Options = ConfigurationPropertyOptions.IsRequired)]
public ServerCollection Servers
{
get
{
return (ServerCollection)base[_servers];
}
}
}
#endregion
#region ServerCollection
[ConfigurationCollection(typeof(ServerSettings))]
public sealed class ServerCollection : ConfigurationElementCollection
{
public ServerCollection()
: base(StringComparer.OrdinalIgnoreCase)
{
}
public new ServerSettings this[string serverName]
{
get
{
// Force the get by key, not index
object key = serverName;
return (ServerSettings)base.BaseGet(key);
}
}
public ServerSettings this[int index]
{
get
{
return (ServerSettings)base.BaseGet(index);
}
}
protected override ConfigurationElement CreateNewElement()
{
return new ServerSettings();
}
protected override Object GetElementKey(ConfigurationElement element)
{
return ((ServerSettings)element).ServerName;
}
}
#endregion
#region ServerSettings
public sealed class ServerSettings : ConfigurationElement
{
internal static readonly ConfigurationValidatorBase NonEmptyStringValidator = new StringValidator(1);
private readonly ConfigurationProperty _serverName =
new ConfigurationProperty("serverName", typeof(string), String.Empty, null, null,
ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsKey);
private readonly ConfigurationProperty _services =
new ConfigurationProperty("services", typeof(ServiceCollection), new ServiceCollection(), null, null,
ConfigurationPropertyOptions.None);
public ServerSettings()
{
}
[ConfigurationProperty("serverName", Options = ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsKey, DefaultValue = "")]
public string ServerName
{
get { return (string)base[_serverName]; }
set { base[_serverName] = value; }
}
[ConfigurationProperty("services", Options = ConfigurationPropertyOptions.None)]
public ServiceCollection Services
{
get { return (ServiceCollection)base[_services]; }
set { base[_services] = value; }
}
}
#endregion
#region ServiceCollection
[ConfigurationCollection(typeof(ServiceSettings))]
public sealed class ServiceCollection : ConfigurationElementCollection
{
private string _copyFrom = null;
private Dictionary<string, ServiceSettings> _overrides = new Dictionary<string, ServiceSettings>();
private Dictionary<string, ServiceSettings> _removes = new Dictionary<string, ServiceSettings>();
public ServiceCollection()
: base(StringComparer.OrdinalIgnoreCase)
{
}
// Called by Copy
internal void Add(ServiceSettings element)
{
this.BaseAdd(element);
}
protected override void SetReadOnly()
{
// Ignore, so we can set it after overrides are done...
}
internal void SetInitialized()
{
// Implement override
foreach (string serviceName in _overrides.Keys)
{
if (this[serviceName] != null)
{
this[serviceName].CopyFrom(_overrides[serviceName]);
}
}
// Removes marked items
foreach (string serviceName in _removes.Keys)
{
base.BaseRemove(serviceName);
}
base.SetReadOnly();
_overrides = null;
_removes = null;
}
protected override bool OnDeserializeUnrecognizedAttribute(string name, string value)
{
if (string.Compare(name, "copyFrom", true) == 0)
{
_copyFrom = value;
// Handled, for now... The actual copying will be done in ServiceMonitorSettingsSection.Settings
return true;
}
else
return base.OnDeserializeUnrecognizedAttribute(name, value);
}
public string CopyFrom
{
get { return _copyFrom; }
set { _copyFrom = value; }
}
public new ServiceSettings this[string serviceName]
{
get
{
// Force the get by key, not index
object key = serviceName;
return (ServiceSettings)base.BaseGet(key);
}
}
public ServiceSettings this[int index]
{
get
{
return (ServiceSettings)base.BaseGet(index);
}
}
protected override ConfigurationElement CreateNewElement()
{
return new ServiceSettings();
}
protected override Object GetElementKey(ConfigurationElement element)
{
return ((ServiceSettings)element).ServiceName;
}
protected override bool OnDeserializeUnrecognizedElement(String elementName, XmlReader reader)
{
bool handled = false;
if (elementName == "override")
{
ServiceSettings elem = new ServiceSettings(reader);
_overrides.Add(elem.ServiceName, elem);
handled = true;
}
else if (elementName == "remove")
{
ServiceSettings elem = new ServiceSettings(reader);
_removes.Add(elem.ServiceName, elem);
handled = true;
}
else
return base.OnDeserializeUnrecognizedElement(elementName, reader);
return handled;
}
}
#endregion
#region ServiceSettings
public sealed class ServiceSettings : ConfigurationElement
{
private readonly ConfigurationProperty _serviceName =
new ConfigurationProperty("serviceName", typeof(string), String.Empty, null, null,
ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsKey);
private readonly ConfigurationProperty _started =
new ConfigurationProperty("started", typeof(bool), true, ConfigurationPropertyOptions.None);
public ServiceSettings()
{
}
public ServiceSettings(XmlReader reader)
{
base.DeserializeElement(reader, false);
}
[ConfigurationProperty("serviceName", Options = ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsKey, DefaultValue = "")]
public string ServiceName
{
get { return (string)base[_serviceName]; }
set { base[_serviceName] = value; }
}
[ConfigurationProperty("started", Options = ConfigurationPropertyOptions.None, DefaultValue = true)]
public bool Started
{
get { return (bool)base[_started]; }
set { base[_started] = value; }
}
public void CopyFrom(ServiceSettings item)
{
foreach (ConfigurationProperty prop in item.Properties)
{
this[prop.Name] = item[prop];
}
}
public ServiceSettings Clone()
{
return this.MemberwiseClone() as ServiceSettings;
}
}
#endregion
}