One of the selling points of the .Net Framework is that applications developed on the platform should be easy to deploy. However, I've found that moving a .Net application from development, to QA, and on to production can be somewhat of a pain if settings in my web.config are specific to the environment where the deployed app lives. For instance, each environment may have a different connection string, email server setting, or web service URL that it needs to interact with.
The approach that I used to take, which served me fine for a while, was to create a folder hierarchy in my file system and source control tree along with the rest of the code base. I'd have a folder named after the source environment and destination environment (i.e. QA and PROD), and I would maintain separate web.config files.
This was a good enough solution, but I soon tired of having to verify each setting in each of the three files that all shared the same name. What I wanted was a single web.config file that I could use to store the settings for each environment. I thought it would be nice if it would allow me to store a default setting that I could override in one or more environments (for instance - if I wanted to use a test web service in both dev and QA, but a different one in production).
To start, I created a web environment helper class. This one is specific to my particular environment, and lets me determine at run time where my application is executing:
public enum InsarioWebEnvironment {Dev,QA,Prod}
// Provides acces to information about the environment where the web application is currently executing.
public class WebEnvironment {
static InsarioWebEnvironment currentEnvironment = GetCurrentEnvironment();
public static InsarioWebEnvironment Current {
get { return currentEnvironment; }
}
static InsarioWebEnvironment GetCurrentEnvironment() {
// In case we are executing unit tests within NUnit
if (HttpContext.Current==null) return InsarioWebEnvironment.Dev;
string aServer = HttpContext.Current.Request.Url.Host.ToLower();
if ((aServer=="localhost") || (aServer=="127.0.0.1")) return InsarioWebEnvironment.Dev;
if (aServer.EndsWith("local") || (aServer.IndexOf("dev.insario.com")>0)) return InsarioWebEnvironment.QA;
return currentEnvironment = InsarioWebEnvironment.Prod;
}
}
Now whenever I want to figure out where my code is executing, I can ask for WebEnvironment.Current, and get back an enum of type InsarioWebEnvironment. This is useful for logic flows within my code - for instance, I could prefill a web form with test user data when in InsarioWebEnvironment.Dev, so that I don't have to fill in a web form over and over again. But the usability is extended even further with the problem domain of this article. To allow me to grab a custom setting, I can .ToString() the InsarioWebEnvironment to get back a token for use within my IConfigurationSectionHandler..
To illustrate my original requirement for how I want to be able to declare and override web.config settings, I threw together this sample XML section that might be found in my web.config:
<MySettings>
<SMTPNotification>
<MailServerName>internalMailServer</MailServerName>
<MailServerNamePROD>mail.prodmailserver.net</MailServerName>
</SMTPNotification>
</MySettings>
As you can see, I want to use a different server to route mail messages depending on which environment I'm in. When I'm operating in a Dev or QA environment, I'll use our internal mail server and while operating in production, I'll use a different one. The next step is to build a class that implements the IConfigurationSectionHandler interface. This is a very basic example of how I could go about doing this: The method below is a utility method I use to read specific settings from within a custom app setting in my IConfigurationHandler.
string ReadNode(string nodeNameToRead, XmlElement configNode, bool isRequired) {
string currentEnvironmentToken = WebEnvironment.Current.ToString().ToUpper();
//Read environment-specific value first. This will look for "MailServerNameENV", where ENV is the current environment (DEV, QA, or PROD)
XmlElement elementNode = (XmlElement)configNode.SelectSingleNode(nodeNameToRead + currentEnvironmentToken);
// if not found, use the default value for "MailServerName"
if (elementNode==null)
elementNode=(XmlElement)configNode.SelectSingleNode(nodeNameToRead);
if (elementNode==null && isRequired==false)
return string.Empty;
if (elementNode.HasChildNodes && elementNode.FirstChild.Value!=null)
return elementNode.FirstChild.Value;
return string.Empty;
}
Here is how I might use it:
try { this.ApplicationName = ReadNode("MailServerName", configNode, true); }
catch(Exception anException){
throw new ConfigurationException("Web.config is missing . Please refer to the developer's guide.", anException);
}
That's it! Now, when my app runs in any environment other than prod, my configuration handler will return internalMailServer for the mail server name. When I run in prod, it'll return mail.prodmailserver.net.
posted on Saturday, April 23, 2005 8:56 PM