Override Configuration Manager
Override Configuration Manager
Recently, I have been working on ways to solve configuration issues in large, multi environment solutions. In the beginning, I simply wanted to store shared app settings and connection strings with a class library so I didn't have to keep copying common configuration settings from project to project within the same solution. Taking that a step further, I thought it would be great to auto detect the runtime environment and use the right app settings and connection strings from that shared configuration file. This all works great, but it has two major drawbacks: firstly, third party tools such as Elmah, and built in tools such as the Membership, Profile and Role Providers look no further than the built in ConfigurationManager
object for appSettings and connection strings which forces us to subclass (Dynamically setting the Elmah connection string at runtime) or override their initialization (Setting Membership-Profile-Role provider's connection string at runtime) in order for them to work with our new settings. Not all third party tools will be as easy to fix. Secondly, all the developers working on the project must be trained to use the new techniques and always remember to use Core.Configuration.AppSettings["key"]
instead of ConfigurationManager
because ConfigurationManager.AppSettings["key"]
may be null
or hold the wrong value.
With that in mind, the next logical step was to find a way to override the built in ConfigurationManager
ensuring that the Core.Configuration
settings are fully integrated. In short: any call to ConfigurationManager.AppSettings
or ConfigurationManager.ConnectionStrings
should return the correct setting, whether that setting comes from the local web/app.config or the Core.Config. In order to do this, it is assumed that if a setting appears both in the local app/web.config and the Core.Config files, then the value in the Core.Config file will be the value returned.
Download the latest version of the Williablog.Core
project here.
Add a reference to it from your project (either to the project or the DLL in the bin folder) and the first line in void Main()
of your console Application or (if a web application) Application_Start()
in Global.asax should be:
Williablog.Core.Configuration.ConfigSystem.Install();
This will reinitialize the Configuration
forcing it to rebuild the static cache of values, but this time we are in control, and as a result, we are able to effectively override the ConfigurationManager
. Here is the code:
namespace Williablog.Core.Configuration
{
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Configuration.Internal;
using System.Reflection;
using Extensions;
public sealed class ConfigSystem : IInternalConfigSystem
{
private static IInternalConfigSystem clientConfigSystem;
private object appsettings;
private object connectionStrings;
/// <summary>
/// Re-initializes the ConfigurationManager,
/// allowing us to merge in the settings from Core.Config
/// </summary>
public static void Install()
{
FieldInfo[] fiStateValues = null;
Type tInitState = typeof
(System.Configuration.ConfigurationManager).GetNestedType
("InitState", BindingFlags.NonPublic);
if (null != tInitState)
{
fiStateValues = tInitState.GetFields();
}
FieldInfo fiInit = typeof
(System.Configuration.ConfigurationManager).GetField
("s_initState", BindingFlags.NonPublic | BindingFlags.Static);
FieldInfo fiSystem = typeof
(System.Configuration.ConfigurationManager).GetField
("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static);
if (fiInit != null && fiSystem != null && null != fiStateValues)
{
fiInit.SetValue(null, fiStateValues[1].GetValue(null));
fiSystem.SetValue(null, null);
}
ConfigSystem confSys = new ConfigSystem();
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);
}
#region IInternalConfigSystem Members
public object GetSection(string configKey)
{
// get the section from the default location (web.config or app.config)
object section = clientConfigSystem.GetSection(configKey);
switch (configKey)
{
case "appSettings":
if (this.appsettings != null)
{
return this.appsettings;
}
if (section is NameValueCollection)
{
// create a new collection because the
// underlying collection is read-only
var cfg = new NameValueCollection((NameValueCollection)section);
// merge the settings from core with the local appsettings
this.appsettings =
cfg.Merge(Core.Configuration.ConfigurationManager.AppSettings);
section = this.appsettings;
}
break;
case "connectionStrings":
if (this.connectionStrings != null)
{
return this.connectionStrings;
}
// create a new collection because the underlying
// collection is read-only
var cssc = new ConnectionStringSettingsCollection();
// copy the existing connection strings into the new collection
foreach (ConnectionStringSettings connectionStringSetting in
((ConnectionStringsSection)section).ConnectionStrings)
{
cssc.Add(connectionStringSetting);
}
// merge the settings from core with the local connectionStrings
cssc = cssc.Merge(ConfigurationManager.ConnectionStrings);
// Cannot simply return our ConnectionStringSettingsCollection
// as the calling routine expects a ConnectionStringsSection result
ConnectionStringsSection connectionStringsSection =
new ConnectionStringsSection();
// Add our merged connection strings to the
// new ConnectionStringsSection
foreach (ConnectionStringSettings connectionStringSetting in cssc)
{
connectionStringsSection.ConnectionStrings.Add
(connectionStringSetting);
}
this.connectionStrings = connectionStringsSection;
section = this.connectionStrings;
break;
}
return section;
}
public void RefreshConfig(string sectionName)
{
if (sectionName == "appSettings")
{
this.appsettings = null;
}
if (sectionName == "connectionStrings")
{
this.connectionStrings = null;
}
clientConfigSystem.RefreshConfig(sectionName);
}
public bool SupportsUserConfig
{
get { return clientConfigSystem.SupportsUserConfig; }
}
#endregion
}
}
The code to actually merge our collections is implemented as Extension methods:
namespace Williablog.Core.Extensions
{
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
public static class IEnumerableExtensions
{
/// <summary>
/// Merges two NameValueCollections.
/// </summary>
/// <param name="first"></param>
/// <param name="second"></param>
/// <remarks>Used by <see cref="
/// Williablog.Core.Configuration.ConfigSystem">ConfigSystem</c>
/// to merge AppSettings</remarks>
public static NameValueCollection Merge
(this NameValueCollection first, NameValueCollection second)
{
if (second == null)
{
return first;
}
foreach (string item in second)
{
if (first.AllKeys.Contains(item))
{
// if first already contains this item,
// update it to the value of second
first[item] = second[item];
}
else
{
// otherwise add it
first.Add(item, second[item]);
}
}
return first;
}
/// <summary>
/// Merges two ConnectionStringSettingsCollections.
/// </summary>
/// <param name="first"></param>
/// <param name="second"></param>
/// <remarks>Used by <see cref="Williablog.Core.Configuration.ConfigSystem
/// ">ConfigSystem</c> to merge ConnectionStrings</remarks>
public static ConnectionStringSettingsCollection Merge
(this ConnectionStringSettingsCollection first,
ConnectionStringSettingsCollection second)
{
if (second == null)
{
return first;
}
foreach (ConnectionStringSettings item in second)
{
ConnectionStringSettings itemInSecond = item;
ConnectionStringSettings existingItem =
first.Cast<ConnectionStringSettings>().FirstOrDefault
(x => x.Name == itemInSecond.Name);
if (existingItem != null)
{
first.Remove(item);
}
first.Add(item);
}
return first;
}
}
}
If we create a console application to test with, complete with its own app.config file that looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="WebServiceUrl"
value="http://webservices.yourserver.com/YourService.asmx"/>
<add key="SmtpServer" value="smtp.yourmailserver.com"/>
<add key="LocalOnly" value="This is from the local app.config"/>
</appSettings>
<connectionStrings>
<add name="AppData" connectionString="data source=Audi01;
initial catalog=MyDB;User ID=User;Password=Password;"
providerName="System.Data.SqlClient"/>
<add name="ElmahDB" connectionString="Database=ELMAH;
Server=Audi02;User=User;Pwd=Password;" providerName="System.Data.SqlClient"/>
</connectionStrings>
</configuration>
And run it with the following code:
static void Main(string[] args)
{
ConfigSystem.Install();
Console.WriteLine(System.Configuration.ConfigurationManager.AppSettings["SmtpServer"]);
Console.WriteLine(System.Configuration.ConfigurationManager.AppSettings["LocalOnly"]);
Console.WriteLine
(System.Configuration.ConfigurationManager.ConnectionStrings["AppData"]);
}
The output is:
smtp.yourlocalmailserver.com
This is from the local app.config
data source=Ford01;initial catalog=MyDB;User ID=User;Password=Password;
With the exception of the middle one (LocalOnly
), all of these settings come from Williablog.Core.Config, not the local app.config proving that the config files were successfully merged.
The ConfigSystem
class could be modified to retrieve the additional appsettings from the registry, from a database or from any other source you care to use.
I'd like to thank the contributers/authors of the following articles which I found very helpful: