- Posted by Ian Suttle on March 2, 2008
- Filed under .Net Framework
I wrote this up a while back but had it on a site of mine most wouldn't have come across. I've moved it to my blog in hopes it'll be of some help to someone.
I manage the development of a rather flexible ecommerce site called Direct2Drive. When I say “flexible” I’m speaking in terms of its template and customization abilities. We resell a hosted solution for rebranding this site and catalog for outside vendors allowing them to sell our product catalog items without the hassle of managing the supplier relationships, billing, and fulfillment. Think Amazon.com hosting the Toys R Us site. Much of the configuration for this system is handled in the web.config file which isn’t optimal for a number of reasons. What I would consider the most notable problem with this is the web.config is frequently being updated to store the settings of each site we host and is therefore a central failure point for all sites. This configuration worked well for us when we had only a couple of sites. As we grow, the risk grows.
So what to do about it? I wanted to store configuration data for each site in a separate configuration file so as we update each site no other site would be affected. I wasn’t able to locate a really good means of addressing this issue within the .NET 2.0 framework (or .NET 1.1 of course) so I created my own WebConfigurationManager for custom web.config files – enter WebConfigurationManager2 (yes, very creative name).
WebConfigurationManager2 allows a developer to access a web.config file located in an alternate directory and treat it as a Configuration object. More specifically, WebConfigurationManager2 stores a collection of configuration files. I'd have found it more useful if Microsoft didn't actually care what the filename was... they do though. In this case it must be named web.config.
Standard access of the application’s web.config utilizes caching to optimize access of configuration information. I’ve implemented the same in WebConfigurationManager2 with invalidation occurring either within ten minutes (no reason, just used ten for now) or on change of the loaded configuration file. Well, it’s not exactly the same. I actually store each configuration in a static Dictionary object and implement caching on arbitrary data and the file. This approach allows me a couple of benefits:
- I don’t have to worry about boxing and unboxing the configuration from the cache – bit of a performance boost.
- If I were to store the Configuration itself in cache I would either have a Dictionary in cache or multiple cache entries; one per Configuration object. If I did the former and placed the Dictionary object in the cache then invalidation would remove the entire Dictionary therefore removing all Configuration objects. If I went with the latter, well, it just seems sloppy to have multiple objects of the same type in separate cache spaces. I guess that’s just me being picky.
Let’s check out the code.
using System;
using System.Collections.Generic;
using System.Web.Caching;
using System.Web.Configuration;
namespace IanSuttle.Util.Config
{
public class WebConfigurationManager2
{
private static Dictionary<string, Configuration> _configs = new Dictionary<string, Configuration>();
#region Indexers
/// <summary>
/// Gets the <see cref="System.Configuration.Configuration"/> from the specified path.
/// </summary>
/// <value></value>
public Configuration this[string configPath]
{
get { return GetConfig(configPath); }
}
/// <summary>
/// Gets the <see cref="System.String"/> from the specified configPath web.config's appSettings key.
/// </summary>
/// <value></value>
public string this[string configPath, string key]
{
get { return GetConfig(configPath).AppSettings.Settings[key].Value; }
}
#endregion Indexers
#region Public methods
/// <summary>
/// Gets the web.config file located at the specified path.
/// </summary>
/// <param name="path">The directory where the web.config is located.</param>
/// <returns></returns>
public static Configuration GetConfig(string configPath)
{
//attempt to load from dictionary
if (_configs.ContainsKey(configPath))
return _configs[configPath];
//attempt to load from disk and add to dictionary
Configuration config = LoadConfig(configPath);
if (config != null)
{
_configs.Add(configPath, config);
return config;
}
//doesn't exist, error out
throw new ConfigurationErrorsException("A web configuration file could not be found at " + configPath);
}
#endregion Public methods
#region Private methods
/// <summary>
/// Loads the web.config from the given path. If a web.config is found it is stored in cache and monitored for chagnes.
/// </summary>
/// <param name="path">The directory where the web.config is located.</param>
/// <returns></returns>
private static Configuration LoadConfig(string configPath)
{
Configuration config = WebConfigurationManager.OpenWebConfiguration(configPath);
//If the config was loaded we want to monitor it for changes (use cache)
//I decided to not store the config in the cache itself for performance (boxing/unboxing from cache)
// and complexity (dictionary in cache, only wanting to expire a single config vs.
// the entire dictionary on invalidation).
if (config != null)
{
string configFilePath = HttpContext.Current.Server.MapPath(configPath + "\\web.config");
HttpContext.Current.Cache.Add("config_" + configPath, configPath, new CacheDependency(configFilePath), Cache.NoAbsoluteExpiration, new TimeSpan(0, 10, 0), CacheItemPriority.Normal, new CacheItemRemovedCallback(ConfigCacheRemoved));
}
return config;
}
/// <summary>
/// Called when a configuration cache item is invalidated.
/// </summary>
/// <param name="name">The name of the cache item.</param>
/// <param name="value">The value of the cache item.</param>
/// <param name="reason">The reason the cache was invalidated.</param>
private static void ConfigCacheRemoved(string name, object value, CacheItemRemovedReason reason)
{
//if the item exists in the dictionary, remove it because it changed
if (_configs.ContainsKey(value.ToString()))
_configs.Remove(value.ToString());
}
#endregion Private methods
}
}
You’ll notice I’ve added a couple of ease of use enhancements including indexers which load configurations on demand if they don’t exist. One of the indexers also provides a shortcut for obtaining an appSettings value. You don’t have to use these indexers and if you wanted to access other sections such as connectionStrings you could do that directly through the Configuration object which can be obtained through this class as well.
Have you encountered a requirement like this? If so, how did you address it? I’d also love to hear any feedback you may have regarding my approach.