Click here to Skip to main content
15,867,704 members
Articles / Programming Languages / C#

Override Configuration Manager

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
29 Mar 2010CPOL3 min read 37.6K   10   8
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:

C#
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:

C#
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:

C#
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
<?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:

C#
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:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Salem Web Network
United States United States
Robert Williams has been programming web sites since 1996 and employed as .NET developer since its release in 2002.

Comments and Discussions

 
Questionnot able to override the config file Pin
Member 1361331528-Mar-18 19:03
Member 1361331528-Mar-18 19:03 
GeneralJust Impressive Pin
bobsort27-Mar-13 18:01
bobsort27-Mar-13 18:01 
I'm writing .net code from the same time you do but never had a chance to go that deep into details.
I should say your article is just impressive.

I used it to answer my question in Stackoverflow.com and here is the link
How to Update (Add/Modify/Delete) keys in AppSettings section of web.config at runtime
QuestionInjecting values for system.web Pin
dascalos30-Aug-11 4:21
dascalos30-Aug-11 4:21 
AnswerRe: Injecting values for system.web Pin
Williarob30-Aug-11 7:41
Williarob30-Aug-11 7:41 
GeneralRe: Injecting values for system.web Pin
dascalos31-Aug-11 3:08
dascalos31-Aug-11 3:08 
QuestionHow can i get different group values? [modified] Pin
ashutosh30076-Aug-10 0:25
ashutosh30076-Aug-10 0:25 
AnswerRe: How can i get different group values? Pin
Williarob6-Aug-10 2:58
Williarob6-Aug-10 2:58 
QuestionWCF Config ? Pin
lbogni17-May-10 12:16
lbogni17-May-10 12:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.