Introduction
During the last eight months, I've been writing a LOT of windows services at work. Recently, I had a need to write a multi-service assembly. All of the services retrieved the app.config file, but each one required a different service-related value for the same setting. In other words, the value for the key "Test1" for Service1 would be different than the value for the same key for Service2.
Now, I didn't need anything exhaustive, and after doing a little (okay - VERY little) research via Google, I decided it would just be faster to do it my way. It just seemed to me that adding SectionGroups and Sections required to much delicate manual modification of the app.config file, and also required moderate changes to my already existing code base. I guess I'm admitting that there's a more "correct" way to support multiple versions of settings in a single app.config file (using SectionGroups and Sections), but the maintenance headaches would exceed the usefulness in no time.
What I Started With
When I first started writing .Net apps (just eight months ago), I decided it might be a good idea to write a class that handled all of the app.config file stuff. The reasoning is that (in our applications) the app.config is loaded when the service starts (or restarts), and is generally not changed (or reloaded) while the service is running. I acknowledge the fact that some of you might in fact modify your app.config files on the fly. While that's solidly outside the scope of this article, it shouldn't be a difficult matter to modify this code to support your requirements in that regard. Personally, I feel that modifying the app.config on the fly is not good form (that's what the built-in user settings class is for), so I therefore leave that as an exercise for the programmer.
For purposes of this example, here's the app.config file I'm using:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- =============================================================================
The following values are common among all services. Applicabale settings might
be an smtp server address, and things like that.
============================================================================== -->
<add key="SmtpServer" value="12.345.678.9"/>
<add key="FromAddress" value="my.address@mydomain.com"/>
<add key="NotifyAddress" value="notified@notifydomain.com"/>
<add key="FfmpegPath" value="c:\program files\ffmpeg\ffmpeg.exe"/>
<add key="DeferValidation" value="false"/>
<add key="AllowDebugLaunch" value="false"/>
<!-- =============================================================================
The following values are unique among all services. Each service might have it's
own value for Test1. Values you might want to include here are things like the
event log name, a timeout value, service-specific folders, and things like that.
============================================================================== -->
<add key="Service1_EventLogName" value="Service1_Log"/>
<add key="Service1_UploadPath" value="C:\Upload\Service1"/>
<add key="Service1_AllowDebugLaunch" value="true"/>
<add key="Service2_EventLogName" value="Service2_Log"/>
<add key="Service2_UploadPath" value="C:\Upload\Service2"/>
<add key="Service2_DeferValidation" value="true"/>
<add key="Service2_UniqueSetting" value="12345"/>
</appSettings>
</configuration>
The AppConfigBase Class
I started off by creating a class that handled reading and validating key values. This keeps a lot of minutia hidden away and goes a long way towards keeping the derived classes much less cluttered. Here's the prototype for the class:
public class AppConfigBase : ICloneable
{
#region Icloneable code
private int baseState;
protected AppConfigBase(AppConfigBase that)
public virtual object Clone()
#endregion Icloneable code
protected bool m_ignoreConfigPrefix = true;
protected string m_configPrefix = "";
protected StringBuilder m_errors = new StringBuilder("");
public string Errors { get { return m_errors.ToString(); } }
public string ApplicationPath { get { return Environment.CurrentDirectory; } }
public string ConfigPrefix { get { return m_configPrefix; } }
public bool IgnoreConfigPrefix
{
get { return m_ignoreConfigPrefix; }
set { m_ignoreConfigPrefix = value; }
}
public AppConfigBase()
{
}
protected void ResetErrors()
protected string KeyName(string name)
protected string GetValue(string key)
protected int GetIntValue(string key)
protected double GetDoubleValue(string key)
protected bool GetBoolValue(string key)
protected string FixupPath(string path)
protected bool PathExists(string key, string path, bool mustExist)
protected bool FileExists(string key, string path, bool mustExist)
protected bool ValueIsEmpty(string key, string value, bool mustHaveValue)
protected bool PingHost(string host)
protected bool ValidHostOrAddress(string key, string value, bool canBeNull)
protected bool ValidEmailAddress(string key, string value)
protected bool NumberInRange(string key, int number, int minNumber, int maxNumber)
}
Values are retrieved from the app.config file with the following functions, with each retrieval being a two-stage process. The class first attempts to load specified key with the configPrefix
prepended to it. If a value is not found in the app.config with that key name (prefix + specified name), the function then attempts to load the specified key without a prefix prepended to it. This saves the programmer from having to handle this in derived classes.
protected string KeyName(string name)
{
return (IgnoreConfigPrefix) ? name : string.Format("{0}{1}", ConfigPrefix, name);
}
protected string GetValue(string key)
{
string value = "";
if (ConfigurationManager.AppSettings[KeyName(key)] != null)
{
value = ConfigurationManager.AppSettings[KeyName(key)].ToString().Trim();
}
else
{
bool m_oldIgnore = m_ignoreConfigPrefix;
m_ignoreConfigPrefix = true;
if (ConfigurationManager.AppSettings[KeyName(key)] != null)
{
value = ConfigurationManager.AppSettings[KeyName(key)].ToString().Trim();
}
m_ignoreConfigPrefix = m_oldIgnore;
}
return value;
}
The only thing you should really need to do in this class is add more GetXXXValue
variants and/or validation functions, such as ensuring that an integer value is a power of 2, or an integer value is evenly divisible by 1024.
The AppConfig Class
I use this class to load settings that are common, or that are always "overridden" for the service in question. Most of your setting retrieval and validation should be done here. Here's the class. Note how un-cluttered this class is because of being derived from AppConfigBase
. Note also that the settings may be considered invalid because you probably don't have ffmpeg in the specified location.
public class AppConfig : AppConfigBase, ICloneable
{
#region IClonable support
private float derivedState;
protected AppConfig(AppConfig that)
: base(that)
{
this.derivedState = that.derivedState;
}
public override object Clone()
{
return new AppConfig(this);
}
#endregion IClonable support
#region common settings/properties
protected string m_smtpServer = "";
protected string m_fromAddress = "";
protected string m_notifyAddress = "";
protected string m_ffmpegPath = "";
protected bool m_deferValidation = false;
protected bool m_allowDebugLaunch = false;
public string SmtpServer { get { return m_smtpServer; } }
public string FromAddress { get { return m_fromAddress; } }
public string NotifyAddress { get { return m_notifyAddress; } }
public string FfmpegPath { get { return m_ffmpegPath; } }
public bool DeferValidation { get { return m_deferValidation; } }
public bool AllowDebugLaunch { get { return m_allowDebugLaunch; } }
#endregion common settings/properties
public AppConfig()
{
}
public virtual void Reload()
{
m_smtpServer = GetValue("SmtpServer");
m_fromAddress = GetValue("FromAddress");
m_notifyAddress = GetValue("NotifyAddress");
m_ffmpegPath = GetValue("FfmpegPath");
m_deferValidation = GetBoolValue("DeferValidation");
m_allowDebugLaunch = GetBoolValue("AllowDebugLaunch");
if (!DeferValidation)
{
ValidateData();
}
}
public virtual void ValidateData()
{
this.ValidHostOrAddress("SmtpServer", m_smtpServer, false);
this.ValidEmailAddress("FromAddress", m_fromAddress);
this.ValidEmailAddress("NotifyAddress", m_notifyAddress);
this.PathExists("FfmpegPath", FfmpegPath, true);
}
}
The AppConfigService1 Class
This class is derived from AppConfig
, and demonstrates the loading of service-specific versions of certain keys. Notice that the constructor accepts a string that represents the prefix used for the service's settings. Also, notice that this class calls the base class Reload()
method (which calls its own Validate()
method), and then loads/validates settings specific to the service.
public class AppConfigService1 : AppConfig, ICloneable
{
#region IClonable support
private float derivedState;
protected AppConfigService1(AppConfigService1 that) : base(that)
{
this.derivedState = that.derivedState;
}
public override object Clone()
{
return new AppConfigService1(this);
}
#endregion IClonable support
#region service-specific settings/properties
protected string m_logName = "";
protected string m_uploadPath = "";
public string LogName { get { return m_logName; } }
public string UploadPath { get { return m_uploadPath; } }
#endregion common settings/properties
public AppConfigService1(string configPrefix)
{
IgnoreConfigPrefix = false;
m_configPrefix = configPrefix;
Reload();
}
public override void Reload()
{
base.Reload();
m_logName = GetValue("LogName");
m_uploadPath = GetValue("UploadPath");
if (!this.DeferValidation)
{
ValidateData();
}
}
public override void ValidateData()
{
ValueIsEmpty("LogName", m_logName, true);
PathExists("UploadPath", m_uploadPath, true);
}
}
The AppConfigService2 Class
Like AppConfigService1
, this class is derived from AppConfig
. Unlike AppConfigService1
, this class loads an additional setting that is only used by Service2. While it's not necessary to prefix unique values, I think that it's important to adhere to the prefixed key naming convention when adding settings that are used only by one service. Programmers performing maintenance on your code in the future will certainly appreciate it (whether they admit it or not).
public class AppConfigService2 : AppConfig, ICloneable
{
#region IClonable support
private float derivedState;
protected AppConfigService2(AppConfigService2 that) : base(that)
{
this.derivedState = that.derivedState;
}
public override object Clone()
{
return new AppConfigService2(this);
}
#endregion IClonable support
#region service-specific settings/properties
protected string m_logName = "";
protected string m_uploadPath = "";
public string LogName { get { return m_logName; } }
public string UploadPath { get { return m_uploadPath; } }
#endregion common settings/properties
protected int m_uniqueSetting = -1;
public int UniqueSetting { get { return m_uniqueSetting; } }
public AppConfigService2(string configPrefix)
{
IgnoreConfigPrefix = false;
m_configPrefix = configPrefix;
Reload();
}
public override void Reload()
{
base.Reload();
m_logName = GetValue("LogName");
m_uploadPath = GetValue("UploadPath");
m_uniqueSetting = GetIntValue("UniqueSetting");
if (!this.DeferValidation)
{
ValidateData();
}
}
public override void ValidateData()
{
ValueIsEmpty("LogName", m_logName, true);
PathExists("UploadPath", m_uploadPath, true);
NumberInRange("UniqueSetting", m_uniqueSetting, 1, 65536);
}
}
IConeable Code
I needed a deep copy capability for these classes due to the way we code our services, and I found this code on the net. For more info about it go to the link provided. The short version is that it utilizes a copy constructor to create a deep copy of the original object.
Using The Classes
Simply instantiate the service-specific classes with the appropriate prefix specification, and you're off to the races. Using this code couldn't be much simpler.
AppConfigService1 ac1 = new AppConfigService1("Service1_");
AppConfigService2 ac2 = new AppConfigService2("Service2_");
The sample application provided in the source code is just a console application that instantiates these classes, and writes the settings to the console (see the screen shot at the beginning of this article).
Visual Studio Complaint
When you add an applocation configuration file to your project, why doesn't the IDE automatically add a reference for System.Configuration
? It seems to me that it would be a simple thing to do and I can't think of a single reason why you wouldn't want it to happen. This is just another example of Microsoft turning off their brains with regards to making our developing lives just a little bit easier. Granted, this is a very minor issue, but still...
In Closing
Like I said before - there are more .Net-centric solutions that can be pursued, and these solutions involve using section groups and sections, and (like this solution) a bit of additional coding. This is merely the way I do it. Chances are pretty good that if you don't often write Windows services, you will probably never have cause to write a service that implements multiple services. However, this set of classes can be used elsewhere, and at least the base class can be used to support your own app.config loading needs. You could canibalize (and adjust) the AppConfigBase class and just use it's validation functions in combination with the section group/section paradigm.
Lastly, this project was written with Visual Studio 2008, but the programmer (you) shouldn't have any problem at all making it compile under VS2005. Simply copy the appropriate files to your VS2005 project and go for it (don't forget to change the namespace to something appropriate for your application).
History
17 May 2008: Original article posted.