Click here to Skip to main content
Click here to Skip to main content
Technical Blog

Tagged as

Override Configuration Manager

, 29 Mar 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
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: Williablog.Core.zip (110.11 kB).

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;
        /// <span class="code-SummaryComment"><summary>
</span>        /// Re-initializes the ConfigurationManager,
        /// allowing us to merge in the settings from Core.Config
        /// <span class="code-SummaryComment"></summary>
</span>        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
    {
        /// <span class="code-SummaryComment"><summary>
</span>        /// Merges two NameValueCollections.
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="first"></param>
</span>        /// <span class="code-SummaryComment"><param name="second"></param>
</span>        /// <span class="code-SummaryComment"><remarks>Used by <see cref="
        /// Williablog.Core.Configuration.ConfigSystem">ConfigSystem</c>
</span>        /// to merge AppSettings<span class="code-SummaryComment"></remarks>
</span>        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;
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Merges two ConnectionStringSettingsCollections.
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="first"></param>
</span>        /// <span class="code-SummaryComment"><param name="second"></param>
</span>        /// <span class="code-SummaryComment"><remarks>Used by <see cref="Williablog.Core.Configuration.ConfigSystem
        /// ">ConfigSystem</c> to merge ConnectionStrings</remarks>
</span>        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:

License

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

Share

About the Author

Williarob
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

 
GeneralJust Impressive Pinmemberbobsort27-Mar-13 19:01 
QuestionInjecting values for system.web Pinmemberdascalos30-Aug-11 5:21 
AnswerRe: Injecting values for system.web PinmemberWilliarob30-Aug-11 8:41 
GeneralRe: Injecting values for system.web Pinmemberdascalos31-Aug-11 4:08 
QuestionHow can i get different group values? [modified] Pinmemberashutosh30076-Aug-10 1:25 
AnswerRe: How can i get different group values? PinmemberWilliarob6-Aug-10 3:58 
QuestionWCF Config ? Pinmemberlbogni17-May-10 13:16 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141216.1 | Last Updated 29 Mar 2010
Article Copyright 2010 by Williarob
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid