Monday 2 July 2007

Overriding ConfigurationManager

So... we're building a system that will certainly need to be hosted across many machines to provide load balancing and splitting of tasks into granular pieces that can be distributed across the pool.
The problem comes when you need to duplicate configuration across all those machines. Sure we could copy app.exe.config and web.configs across the pool. But management of this soon becomes tendious and error prone. Especially once you start needing to assign clusters to specific customers to support SLAs.

System.Configuration.ConfigurationManager is the normal place to access various settings. But as with several System classes overriding behavior can seem impossible. A lot of the parts you want to override are internal and/or sealed.

Reflector to the rescue!

All the useful methods (like AppSettings) delegate via s_configSystem which is private static.
You'll notice that there's a SetConfigurationSystem method that sets s_configSystem. But of course it's private static (sigh). But at least it takes an interface type, so maybe there's a chance.

OK let's use FileDisassembler (a Reflector plugin) and get the sourse of some System assemblies out into files so we can do some searching.

SetConfigurationSystem is actually called from System.Web.Configuration.HttpConfigurationSystem where they use Type.GetType to create some more internal, sealed types from System.Configuration.Internal that in turn call SetConfigurationSystem. They do this to redirect to the web.config file and deal with the file changing so that the web app is restarted.

So the trick is to create a class that implements the interface IInternalConfigSystem. Three straight forward methods. Then use the technique from HttpConfigurationSystem to inject our class into ConfigurationManager.

Not as hard after all. Here's a really simple example...


public class MyConfigSystem: IInternalConfigSystem
{
public static void Install()
{
MyConfigSystem confSys = new MyConfigSystem();
Type configFactoryType = Type.GetType("System.Configuration.Internal.InternalConfigSettingsFactory, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", true);
IInternalConfigSettingsFactory configSettingsFactory = (IInternalConfigSettingsFactory)Activator.CreateInstance(configFactoryType, true);
configSettingsFactory.SetConfigurationSystem(confSys, false);

Type clientConfigSystemType = Type.GetType("System.Configuration.ClientConfigurationSystem, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", true);
clientConfigSystem = (IInternalConfigSystem)Activator.CreateInstance(clientConfigSystemType, true);
}
private static IInternalConfigSystem clientConfigSystem;

#region IInternalConfigSystem Members

object IInternalConfigSystem.GetSection(string configKey)
{
switch (configKey)
{
case "appSettings":
NameValueCollection nvc = new NameValueCollection();
nvc.Add("fred", "wilma");
return nvc;
case "connectionsStrings":
ConnectionStringSettingsCollection cssc = new ConnectionStringSettingsCollection();
cssc.Add(new ConnectionStringSettings("aName", "someConnectionString", "aProvider"));
return cssc;
default:
return clientConfigSystem.GetSection(configKey);
}
}

void IInternalConfigSystem.RefreshConfig(string sectionName)
{
switch (sectionName)
{
case "appSettings":
break;
case "connectionsStrings":
break;
default:
clientConfigSystem.RefreshConfig(sectionName);
break;
}
}

bool IInternalConfigSystem.SupportsUserConfig
{
get { return true; }
}

#endregion
}

Just ensure that MyConfigSystem.Install() is called before you call any method on Configurationmanager.

A bit of WCF and a bit of caching and we can have whichever sections we like retrieved from a service. We stil need to deal with single point of failure issues. But we have other parts for that :)