Amazon.com Widgets All posts tagged 'connection string'

WilliaBlog.Net

I dream in code

About the author

Robert Williams is an internet application developer for the Salem Web Network.
E-mail me Send mail
Go Daddy Deal of the Week: 30% off your order at GoDaddy.com! Offer expires 11/6/12

Recent comments

Archive

Authors

Tags

Code Project Associate Logo

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.


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 that 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;

 

        /// <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 it's 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 retrive 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:

http://stackoverflow.com/questions/158783/is-there-a-way-to-override-configurationmanager-appsettings

http://andypook.blogspot.com/2007/07/overriding-configurationmanager.html


Posted by Williarob on Monday, March 29, 2010 7:13 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Auto detect the runtime environment and use the right app settings and connection strings

There are many ways to manage the problem of connection string and app settings substitution in the web.config / app.config files when publishing to different environments (e.g. QA and Production servers). In the past I have made use of the Web Deployment project's ability to replace the appsettings and connectionstrings sections, I have experimented with batch files, Build events, conditional compilation and used the extremely powerful FinalBuilder. However, my prefered solution is to have a single shared .config file with all the possible settings in it (so you only have to open one file to change any of the settings) then have the executing application automatically detect the environment and use the correct settings every time.

The technique dicussed below builds on that of an earlier article which described how to centralize your shared application settings and connection strings in a common class library. It also assumes that you know the machine names of your development, QA and production servers. Obviously servers get replaced from time to time and websites sometimes get moved from one server to another, but it has been my experience that there is usually some sort of common naming convention used on servers and web farms, and knowing that convention should be good enough. Even this is not the case, the Development, QA and Production server names are stored in an app setting so you can easily change them at any time if necessary. For this example, the assumption is that the development servers are all named something like "Squirrel01", "Squirrel02", the QA boxes are "Fox01", "Fox02", and the production (farm) boxes are "Rabbit01x", "Rabbit01y", "Rabbit02x", "Rabbit02y", etc. With this in mind, it is necessary only to look for the words "Rabbit", "Fox" or "Squirrel" in the machine name we are running on to identify the current environment and know which section of our config file to use. If none of these names is found, we shall assume the app is running on the localhost of a developer's computer, and use those settings. I should point out that it is possible to for a server to be configured in such a way as to prevent Environment.MachineName from returning a value, in which case this technique simply will not work, so before you start trying to integrate this code into your solution, I recommend you craete a quick test.aspx page or console app that simply does a Response.Write(Environment.MachineName)/Console.WriteLine(Environment.MachineName) and run it on your servers.

First, let's setup our .config file:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

  <configSections>

    <sectionGroup name="Localhost" type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">

      <section name="appSettings" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />

      <section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />

    </sectionGroup>

 

    <sectionGroup name="Dev" type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">

      <section name="appSettings" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />

      <section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />

    </sectionGroup>

 

    <sectionGroup name="Qa" type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">

      <section name="appSettings" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />

      <section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />

    </sectionGroup>

 

    <sectionGroup name="Production" type="Williablog.Core.Configuration.EnvironmentSectionGroup, Williablog.Core">

      <section name="appSettings" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />

      <section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false" />

    </sectionGroup>

  </configSections>

 

  <Localhost>

    <appSettings>

      <add key="WebServiceUrl" value="http://webservices.squirrel01.yourserver.com/YourService.asmx"/>

      <add key="SmtpServer" value="smtp.yourlocalmailserver.com"/>

    </appSettings>

    <connectionStrings>

      <add name="AppData" connectionString="data source=Ford01;initial catalog=MyDB;User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>

      <add name="ElmahDB" connectionString="Database=ELMAH;Server=Ford02;User=User;Pwd=Password;" providerName="System.Data.SqlClient"/>

    </connectionStrings>

  </Localhost>

 

  <Dev>

    <appSettings>

      <add key="WebServiceUrl" value="http://webservices.squirrel01.yourserver.com/YourService.asmx"/>

      <add key="SmtpServer" value="smtp.yourlocalmailserver.com"/>

    </appSettings>

    <connectionStrings>

      <add name="AppData" connectionString="data source=Ford01;initial catalog=MyDB;User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>

      <add name="ElmahDB" connectionString="Database=ELMAH;Server=Ford02;User=User;Pwd=Password;" providerName="System.Data.SqlClient"/>

    </connectionStrings>

  </Dev>

 

  <Qa>

    <appSettings>

      <add key="WebServiceUrl" value="http://webservices.Fox01.yourserver.com/YourService.asmx"/>

      <add key="SmtpServer" value="smtp.yourlocalmailserver.com"/>

    </appSettings>

    <connectionStrings>

      <add name="AppData" connectionString="data source=BMW01;initial catalog=MyDB;User ID=User;Password=Password;" providerName="System.Data.SqlClient"/>

      <add name="ElmahDB" connectionString="Database=ELMAH;Server=BMW02;User=User;Pwd=Password;" providerName="System.Data.SqlClient"/>

    </connectionStrings>

  </Qa>

 

  <Production>

    <appSettings>

      <add key="WebServiceUrl" value="http://webservices.yourserver.com/YourService.asmx"/>

      <add key="SmtpServer" value="smtp.yourmailserver.com"/>

    </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>

  </Production>

 

  <appSettings>

    <!-- Global/common appsettings can go here -->

    <add key="Test" value="Hello World"/>

 

    <add key="DevelopmentNames" value="SQUIRREL"/>

    <add key="ProductionNames" value="RABBIT"/>

    <add key="QANames" value="FOX"/>

    <add key="EnvironmentOverride" value=""/>

    <!-- /Dev | /Localhost | /Production | (blank)-->

 

  </appSettings>

</configuration>

As you can see, the first thing we do in the config file is declare four section groups, "LocalHost", "Dev", "Qa" and "Production". I chose to create a custom SectionGroup since this allowed me to strongly type the expected sections within it, greatly simplifying the code required to access those sections. All the EnvironmentSectionGroup class does, is inherit ConfigurationSectionGroup and declare two properties:

namespace Williablog.Core.Configuration

{

    using System.Configuration;

 

    public class EnvironmentSectionGroup : ConfigurationSectionGroup

    {

 

        #region Properties

 

        [ConfigurationProperty("appSettings")]

        public AppSettingsSection AppSettings

        {

            get

            {

                return (AppSettingsSection)Sections["appSettings"];

            }

        }

 

        [ConfigurationProperty("connectionStrings")]

        public ConnectionStringsSection ConnectionStrings

        {

            get

            {

                return (ConnectionStringsSection)Sections["connectionStrings"];

            }

        }

 

        #endregion

 

    }

}

Next, we create the sections for localhost, development, qa and production, each of which has its own appSettings and connectionStrings sections. These are of the same type as the connectionStrings and appSettings found in any .config file, meaning we don't need to write any additional code to fully utilise these sections - no traversing of primitive xmlNodes or anything like that to get the connectionstrings from that section. Finally we add the expected, normal appsettings section which in this case will provide the global or common appsettings that are shared by all environments. It is here that we store the server names that will help us identify where the app is currently executing. The EnvironmentOverride setting is an added bonus -it allows you to use all of qa or production settings while running on localhost which helps you debug those "well it works on my machine" situations without having to manually change all of the settings for localhost.

Building on the BasicSettingsManager we built earlier we simply add some code to determine the machine name we are running on and return the appSettings and connectionStrings sections appropriate to that environment:

namespace Williablog.Core.Configuration

{

    using System;

    using System.Collections.Specialized;

    using System.Configuration;

    using System.IO;

    using System.Linq;

 

    public class AdvancedSettingsManager

    {

        #region fields

 

        private const string ConfigurationFileName = "Williablog.Core.config";

 

        /// <summary>

        /// default path to the config file that contains the settings we are using

        /// </summary>

        private static string configurationFile;

 

        /// <summary>

        /// Stores an instance of this class, to cut down on I/O: No need to keep re-loading that config file

        /// </summary>

        /// <remarks>Cannot use system.web.caching since agents will not have access to this by default, so use static member instead.</remarks>

        private static AdvancedSettingsManager instance;

 

        /// <summary>

        /// Settings Environment

        /// </summary>

        private static string settingsEnvironment;

 

        private static EnvironmentSectionGroup currentSettingsGroup;

 

        #endregion

 

        #region Constructors

 

        private AdvancedSettingsManager()

        {

            ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();

 

            fileMap.ExeConfigFilename = configurationFile;

 

            Configuration config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);

 

            settingsEnvironment = "Localhost"; // default to localhost

 

            // get the name of the machine we are currently running on

            string machineName = Environment.MachineName.ToUpper();

 

            // compare to known environment machine names

            if (config.AppSettings.Settings["ProductionNames"].Value.Split(',').Where(x => machineName.Contains(x)).Count() > 0)

            {

                settingsEnvironment = "Production";

            }

            else if (config.AppSettings.Settings["QANames"].Value.Split(',').Where(x => machineName.Contains(x)).Count() > 0)

            {

                settingsEnvironment = "Qa";

            }

            else if (config.AppSettings.Settings["DevelopmentNames"].Value.Split(',').Where(x => machineName.Contains(x)).Count() > 0)

            {

                settingsEnvironment = "Dev";

            }

 

            // If there is a value in the EnvironmentOverride appsetting, ignore results of auto detection and set it here

            // This allows us to hit production data from localhost without monkeying with all the config settings.

            if (!string.IsNullOrEmpty(config.AppSettings.Settings["EnvironmentOverride"].Value))

            {

                settingsEnvironment = config.AppSettings.Settings["EnvironmentOverride"].Value;

            }

 

            // Get the name of the section we are using in this environment & load the appropriate section of the config file

            currentSettingsGroup = config.GetSectionGroup(SettingsEnvironment) as EnvironmentSectionGroup;

        }

 

        #endregion

 

        #region Properties

 

        /// <summary>

        /// Returns the name of the current environment

        /// </summary>

        public string SettingsEnvironment

        {

            get

            {

                return settingsEnvironment;

            }

        }

 

        /// <summary>

        /// Returns the ConnectionStrings section

        /// </summary>

        public ConnectionStringSettingsCollection ConnectionStrings

        {

            get

            {

                return currentSettingsGroup.ConnectionStrings.ConnectionStrings;

            }

        }

 

        /// <summary>

        /// Returns the AppSettings Section

        /// </summary>

        public NameValueCollection AppSettings

        {

            get

            {

                NameValueCollection settings = new NameValueCollection();

                foreach (KeyValueConfigurationElement element in currentSettingsGroup.AppSettings.Settings)

                {

                    settings.Add(element.Key, element.Value);

                }

 

                return settings;

            }

        }

 

        #endregion

 

        #region static factory methods

 

        /// <summary>

        /// Public factory method

        /// </summary>

        /// <returns></returns>

        public static AdvancedSettingsManager SettingsFactory()

        {

            // If there is a bin folder, such as in web projects look for the config file there first

            if (Directory.Exists(AppDomain.CurrentDomain.BaseDirectory + @"\bin"))

            {

                configurationFile = string.Format(@"{0}\bin\{1}", AppDomain.CurrentDomain.BaseDirectory, ConfigurationFileName);

            }

            else

            {

                // agents, for example, won't have a bin folder in production

                configurationFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigurationFileName);

            }

 

            // If we still cannot find it, quit now!

            if (!File.Exists(configurationFile))

            {

                throw new FileNotFoundException(configurationFile);

            }

 

            return CreateSettingsFactoryInternal();

        }

 

        /// <summary>

        /// Overload that allows you to pass in the full path and filename of the config file you want to use.

        /// </summary>

        /// <param name="fullPathToConfigFile"></param>

        /// <returns></returns>

        public static AdvancedSettingsManager SettingsFactory(string fullPathToConfigFile)

        {

            configurationFile = fullPathToConfigFile;

            return CreateSettingsFactoryInternal();

        }

 

        /// <summary>internal Factory Method

        /// </summary>

        /// <returns>ConfigurationSettings object

        /// </returns>

        internal static AdvancedSettingsManager CreateSettingsFactoryInternal()

        {

            // If we havent created an instance yet, do so now

            if (instance == null)

            {

                instance = new AdvancedSettingsManager();

            }

 

            return instance;

        }

 

        #endregion

    }

}

As before you can then access the appSettings of the Core.Config from any of your projects like so:

Console.WriteLine(Williablog.Core.Configuration.AdvancedSettingsManager.SettingsFactory().AppSettings["Test"]);

To make this work, you will need to add a reference to System.Configuration. If the config file and Settings manager code is to be part of a class library, you will need to set the "Copy to Output Directory" property of your .config file to "Copy always"and add a reference to System.Configuration to each of your projects.

Download the Williablog.Core project: Williablog.Core.zip (100.77 kb)


Posted by Williarob on Thursday, March 18, 2010 9:00 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Setting Membership-Profile-Role provider's connection string at runtime

Why would you want to do this? Well perhaps your application uses a separate API to retrieve its specific connection string (via key, for security purposes), or perhaps you have different connection strings for production and development and want to be able to test a value (such as Environment.MachineName) at runtime and load the appropriate connection string name into your membership and role providers. Listing 1 shows how a typical web.config might list its connectionstrings and providers.

Listing 1

<connectionStrings>

    <!-- DEV -->

    <add name="DEV.DB" connectionString="Database=NORTHWIND;Server=xxx;User=xxx;Pwd=xxx;" providerName="System.Data.SqlClient"/>

 

    <!-- Live -->

    <add name="LIVE.DB" connectionString="Database=NORTHWIND;Server=xxx;User=xxx;Pwd=xxx;" providerName="System.Data.SqlClient"/>

</connectionStrings>

 

<!-- ... -->

 

<membership defaultProvider="MyProvider">

      <providers>

        <add connectionStringName="DEV.DB" applicationName="/Test"

    description="MyProvider" requiresUniqueEmail="false" enablePasswordRetrieval="false"

    enablePasswordReset="false" requiresQuestionAndAnswer="false"

    passwordFormat="Hashed" name="MyProvider" type="Williablog.Net.Examples.Providers.SqlMembershipProvider" />

       

      </providers>

    </membership>

 

    <profile defaultProvider="MyProvider">

      <providers>

        <add connectionStringName="DEV.DB" applicationName="/Test" description="" name="MyProvider" type="Williablog.Net.Examples.Providers.SqlProfileProvider"/>

      </providers>

 

      <properties>

        <add name="FirstName" type="System.String" allowAnonymous="false"/>

        <add name="LastName" type="System.String" allowAnonymous="false"/>

      </properties>

    </profile>

 

    <roleManager enabled="true" defaultProvider="MyProvider" domain="xxx">

      <providers>

        <add connectionStringName="DEV.DB" applicationName="/Test"

    description="" name="MyProvider" type="Williablog.Net.Examples.Providers.SqlRoleProvider" />

      </providers>

    </roleManager>

 

    <webParts>

      <personalization defaultProvider="MyProvider">

        <providers>

          <add connectionStringName="DEV.DB" name="MyProvider" type="Williablog.Net.Examples.Providers.SqlPersonalizationProvider"/>

        </providers>

      </personalization>

    </webParts>

So how can you change the connection strings at runtime?

When shopping around for an answer, I came across this entry in the Microsoft Forums which suggests downloading the SampleProviderToolkitSampleProviders from Microsoft, modifying the SQLConnectionHelper class to include your special handling of the connection string. And this was initially the road I went down, however, the SampleProviderToolkitSampleProviders does not include a SqlPersonalizationProvider example and it was when I decided to simply inherit the built in SqlPersonalizationProvider and make my own that I discovered an easier solution (Listing 2) that can be applied to any provider (as long as your connection strings can be made available in the connectionstrings section of the web.config.):

Listing 2

Imports System

Imports System.Collections.Specialized

 

Namespace Williablog.Net.Examples.Providers

 

    Public Class SqlMembershipProvider

        Inherits System.Web.Security.SqlMembershipProvider

 

        Public Overrides Sub Initialize(ByVal name As String, ByVal config As System.Collections.Specialized.NameValueCollection)

            ' intercept the setting of the connection string so that we can set it ourselves...

            Dim specifiedConnectionString As String = config.Item("connectionStringName")

            config.Item("connectionStringName") = GetYourRunTimeConnectionStringNAme(specifiedConnectionString)

 

            ' Pass doctored config to base classes

            MyBase.Initialize(name, config)

        End Sub

    End Class

 

End Namespace

All you really need to do is override the Initialize method, change the connectionstring and then pass it to the base class. That's it! Simply set the type attribute of your provider in the web.config to your inherited class (as in Listing 1), and use this same technique on all the sqlproviders you need to use. In this example, config.Item("connectionStringName") would return "DEV.DB" and GetYourRunTimeConnectionStringName() would be a function that retrieves the right connectionstringname by checking the machinename. For this example, if it is the name of a production server it would return "Live.DB" and if it were a development machine it would return "DEV.DB" and that is the name of the connection string that would be passed to the base provider classes, and which would be used at runtime.

Of course, this technique is only viable if you can store your connection strings in the web.config. If you have to get them at runtime using an API or a webservice, you would still be better off following the directions on the Microsoft Forum, which I will reproduce here simply because links have a way of breaking over time. Download the ProviderToolkitSamples and modify the SQLConnectionHelper Class. Specifically, the GetConnectionString function which looks something like Listing 3.

Listing 3

internal static string GetConnectionString(string specifiedConnectionString, bool lookupConnectionString, bool appLevel)

        {

            if (specifiedConnectionString == null || specifiedConnectionString.Length < 1)

                return null;

 

            string connectionString = null;

 

            /////////////////////////////////////////

            // Step 1: Check <connectionStrings> config section for this connection string

            if (lookupConnectionString)

            {

                ConnectionStringSettings connObj = ConfigurationManager.ConnectionStrings[specifiedConnectionString];

                if (connObj != null)

                    connectionString = connObj.ConnectionString;

 

                if (connectionString == null)

                    return null;

            }

            else

            {

                connectionString = specifiedConnectionString;

            }

 

            return connectionString;

        }

    }

Obviously, you can change the content of this function to do whatever you need it to do in order to generate or retrieve your connection string. I tested this technique and it works beautifully with all the provided sample Providers, but you are on your own if you need a connection string from outside the web.config for your SqlPersonalizationProvider.


Posted by Williarob on Friday, May 02, 2008 4:48 PM
Permalink | Comments (1) | Post RSSRSS comment feed