Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Single web.config file across all environments (dev, test, prod)

0.00/5 (No votes)
15 Feb 2007 2  
Have you ever had the issue where every time you release a website to a new environment, you have custom settings in the web.config that need to change for each environment? This article shows a solution where you set up your web.config once and then let the code do the rest of the work.

Configuration at work

Introduction

Have you ever had the issue where every time you release a website to a new environment, you have custom settings in the web.config that need to change for each environment? For instance, when going from your dev server to a test server, your connection string needs to change. Or from test to prod, your SMTP host needs to change. This article shows a solution where you set up your web.config once and then let the code do the work of deciding which environment it is in.

Background

I have found when deploying a website to a different environment, I often do not copy out the web.config file. I am worried I will forget some of the custom settings for that environment. Sometimes this causes issues because of changes that have been made to the web.config file that are new. I decided I wanted a solution where you can set up your web.config file for each of your environments and then you don't have to worry about the rest. You just release the single web.config file and let the system take care of the custom settings for each environment.

The Problem

I have found myself never overwriting existing web.config files on prod and test servers. I would have to manually modify these web.config files whenever they were modified in the dev with the correct values for test and prod. I really didn't want to have to do this anymore.

The Solution

With ASP.NET 2.0, we have been given a lot of new functionality with how we can work with the web.config file. So I created a custom section in the web.config that defines which servers are in which environments. Next, I created another custom section that defines each environment. Finally, I used some custom classes to access our config settings. Accessing this custom class (EnvironmentSettings) once will fix the config files for the current environment. I used static (Shared in VB) constructors so the code only accesses the config file once. Then you can use the static EnvironmentSettings class to access your config settings.

Some Details

Here is the first section the web.config needs:

<configSections>
 <section name="environmentConfiguration" 
     type="ConfigIt.KeyValueConfigurationSection, ConfigIt"/>
 <section name="dev" type="ConfigIt.KeyValueConfigurationSection, ConfigIt"/>
 <section name="test" type="ConfigIt.KeyValueConfigurationSection, ConfigIt"/>
 <section name="prod" type="ConfigIt.KeyValueConfigurationSection, ConfigIt"/>
</configSections>

configSections goes right into your web.config file. It defines the custom sections we are going to add.

Next, we have a custom section that defines our servers:

<environmentConfiguration>
 <elements>
  <add key="devpcname" value="dev" />
  <add key="testpcname" value="test" />
  <add key="prodpcname" value="prod" />
 </elements>
</environmentConfiguration>

Notice that "environmentConfiguration" was defined in the configSections above. Here is where you put your PC and server names. You don't actually have to put the dev PC/server names in since that is what the code assumes if it can't find the machine name in the list defined here. It is important that you have your test and prod server names here.

Next, we have the dev, test, and prod custom sections:

<dev>
 <elements>
   <add key="SystemEmailAddress" value="administrator@d.com" />
   <add key="SMTPServer" value="devSMTP" />
 </elements>
 <connectionStrings>
   <add name="YourDatabase" 
     connectionString="Database=YourDatabase;Server=devsqlpcname;uid=DevUser;pwd=DevUser;"
     providerName="System.Data.SqlClient" />
  </connectionStrings>
</dev>
<test>
 <elements>
   <add key="SystemEmailAddress" value="administrator@t.com" />
   <add key="SMTPServer" value="testSMTP" />
 </elements>
 <connectionStrings>
   <add name="YourDatabase" 
    connectionString="Database=YourDatabase;Server=testsqlpcname;uid=TestUser;pwd=TestUser;"
    providerName="System.Data.SqlClient" />
 </connectionStrings>
</test>
<prod>
 <elements>
   <add key="SystemEmailAddress" value="administrator@p.com" />
   <add key="SMTPServer" value="prodSMTP" />
 </elements>
 <connectionStrings>
   <add name="YourDatabase" 
    connectionString="Database=YourDatabase;Server=prodsqlpcname;uid=ProdUser;pwd=ProdUser;"
    providerName="System.Data.SqlClient" />
 </connectionStrings>
</prod>

So instead of using appSetting, you can add a new key value pair in the elements section of the correct environment. Notice that each environment has defined a connectionStrings section.

Next, let's look at the normal connectionStrings section in the web.config file:

<connectionStrings configSource="ConnectionStrings.config" />

There is nothing there except a configSource. configSource tells this section that there is an external file that has the connectionStrings section in it. Note: it is important that external config files still have a .config extension. These extensions can not be browsed by a web browser. Here is the ConnectionStrings.config file:

<?xml version="1.0" encoding="utf-8"?>
<connectionStrings>
   <clear />
   <add name="YourDatabase" connectionString=""
      providerName="System.Data.SqlClient" />
</connectionStrings>

Notice that the connectionString attribute is empty. It is going to be overwritten by the correct value in the environment sections defined above.

OK, at this point, you may be asking what is this all about? Why do we need an external file? Well, I can tell you, the first time I tried this, I didn't use an external file. There was a problem though. The web.config file would get updated, but the WebConfigurationManager.ConnectionStrings did not reload even through the file changed. Now I have my suspicions that, that is because it is the IIS worker process that is actually changing the config file, but I don't really know for sure. So when using an external config file, you can set a property called "RestartOnExternalChanges". When this is set to true, then WebConfigurationManager.ConnectionStrings does get updated.

OK, you still may wonder why you care about this variable? Can't you just use EnvironmentSettings.ConnectionStrings? The answer is yes, you can, but... If you are currently using a roleManager, inside of the providers, there is an attribute called connectionStringName. That uses the WebConfigurationManager.ConnectionStrings, not your custom section. The same is true for profile and membership. So if you are using any of those, it is important that the WebConfigurationManager gets updated with the correct connection strings. Also, if you are using a SqlDataSource, you set the connectionString property in the HTML to "<%$ ConnectionStrings:NorthwindConnectionString %>"; you are not using your custom section ConnectionStrings. So you are in the same situation there.

The SMTP (email server) name is in the same type of situation, if you are using the ASP.NET 2.0 login controls. All of the email functionality is assuming you have set up your web.config file with the correct structure that includes an SMTP email server name (host).

So how does it work? I have already discussed the web.config file with the custom sections, now let us move on to the code. I have created a separate Class Library project so that it is easy to add this function to any web project. Inside of the class library, there are two classes.

The first is the EnvironmentSettings class. The static constructor in this class is what looks at the custom sections, decides which machine the code is running on, gets the correct custom environment, and then compares the custom settings for connection strings and SMTP with what the web.config currently has. If they don't match up the custom settings, overwrite the current setting in the config file and the file is saved. Since this is a static (Shared in VB) construction, it should only happen once per appdomain. In the download samples, you will see I have added a Global.asax file that has an application_Start event. In this event, I access the static class so the constructor is called.

The other class in the Class Library is KeyValueConfigurationSection. This class defines the custom sections we have added to the web.config file. It is also the class we point to the configSections part of the web.config. Here is what the static constructor looks like in C# (I have VB.NET source code download available as well):

//Constructor

static EnvironmentSettings()
{
  KeyValueConfigurationSection section = null;

  // Get the current configuration file.

  Configuration config = 
  System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("~",null,null,
         System.Environment.MachineName,
         System.Environment.MachineName + @"\WebConfigUser","WebConfigPassword");

  //Next Get which environment we are in based
  //off what the machine name this code is running on

  section = config.GetSection("environmentConfiguration") 
                                   as KeyValueConfigurationSection;
  string machineName = System.Environment.MachineName.ToLower();
  if (section.Elements[machineName] != null &&
      section.Elements[machineName].Value != null &&
      section.Elements[machineName].Value != string.Empty)
     {
       _environment = section.Elements[machineName].Value;
     }
  else
     { //Default to dev

       _environment = "dev";
     }

  _elements = ((KeyValueConfigurationSection)config.GetSection(_environment)).Elements;
  _connectionStrings = 
    ((KeyValueConfigurationSection)config.GetSection(_environment)).ConnectionStrings;

  //NOTE we only want to save the config file if something changed...

  Boolean somethingChanged = false;

  //Update the email stuff if environment specific value is different

  _systemNet = (System.Net.Configuration.NetSectionGroup)
                     config.GetSectionGroup("system.net");
  if (_systemNet.MailSettings.Smtp.Network.Host != null && 
      _elements["SMTPServer"] != null &&
      _systemNet.MailSettings.Smtp.Network.Host != 
                 _elements["SMTPServer"].Value)
    {
      _systemNet.MailSettings.Smtp.Network.Host = _elements["SMTPServer"].Value;
      somethingChanged = true;
    }

  //Update the connection strings if the environment specific value is different

  foreach (ConnectionStringSettings css in _connectionStrings)
  {
    Boolean foundIt = false;
                
    foreach (ConnectionStringSettings css2 in config.ConnectionStrings.ConnectionStrings)
    {
      if (css.Name.Trim() == css2.Name.Trim())
        {
          foundIt = true;
          if (css2.ConnectionString != css.ConnectionString)
            {                          
              css2.ConnectionString = css.ConnectionString;
              config.ConnectionStrings.SectionInformation.RestartOnExternalChanges = true;
                            
              somethingChanged = true;
            }
        }
    } //foreach css2

    if (!foundIt)
      {
        config.ConnectionStrings.ConnectionStrings.Add(css);
        config.ConnectionStrings.SectionInformation.RestartOnExternalChanges = true;
        somethingChanged = true;
      }

  } //foreach css

  if (somethingChanged)
    {                         
      config.Save(ConfigurationSaveMode.Modified);
    }
}

The last step is how the website updates the web.config file. The answer is you need to create a local user on the machines this website will be running on. Then you give that user read and write rights so it can update the web.config file. Note: you could just give ASPNET user rights, but I would not suggest this since it is kind of a security no-no. Next, you need to add the IIS_WPG work process group to have read and write rights to the folder as well.

Here are some examples of accessing configuration properties in my sample page_load event in C#:

string databaseName = "YourDatabase";
//Getting config items from the EnvironmentSettings static var...

lbEnvior.Text = "You are running in the " + 
                EnvironmentSettings.Environment + " Environment";
lbEnvirSQL.Text = EnvironmentSettings.ConnectionStrings[databaseName].ConnectionString;
lbEnvirSMTP.Text = EnvironmentSettings.SystemNet.MailSettings.Smtp.Network.Host;

//Getting config items from WebConfigurationManager

lbWebConfigManSQL.Text = 
  System.Web.Configuration.WebConfigurationManager.
  ConnectionStrings[databaseName].ConnectionString;
        
//Getting config items from the config file directly

System.Configuration.Configuration config = 
System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("~");
lbWebConfigDirectSQL.Text = 
  config.ConnectionStrings.ConnectionStrings[databaseName].ConnectionString;
System.Net.Configuration.NetSectionGroup _systemNet = 
(System.Net.Configuration.NetSectionGroup)config.GetSectionGroup("system.net");
lbWebConfigDirectSMTP.Text = _systemNet.MailSettings.Smtp.Network.Host; 

//Getting Config items from ConfigurationManager NOTE this is not suggested in a Web site.

lbConfigManSQL.Text = ConfigurationManager.ConnectionStrings[databaseName].ConnectionString;

The Steps

Note: you will need admin rights for some of these steps:

  1. Create a local user (I picked WebConfigUser) on the machine the site is going to run on. Right click My Computer. Click Manage. Click the plus sign on Local Users and Groups. Right click on the Users folder. Click New User. Type in the user name and full name and password. Then, un-check User must change password at login and check Password never expires. Click Create.
  2. Go to the directory the web.config is in. Right click the folder, choose Properties. Select the Security tab. Click the Add button. You may need to click the Locations button so that your location is your computer, not the domain. Click OK. Check the Modify check box. Next, Add the IIS_WPG group. Make sure to check the Modify check box. Click OK.
  3. In the EnvironmentSettings class constructor, there is a call to OpenWebConfiguration. Inside of this call, you need to have the user name and password of the User local user you just created.
  4. In web.config, add a configSections. You should be able to copy the one in the download sample files.
  5. In web.config, add the environmentConfiguration custom section. This is where you list your servers.
  6. In web.config, add the dev, test, prod, or whatever you want to call your environments. Make sure each environment section has a ConnectionStrings section in it.
  7. In Web.config, point the existing ConnectionStrings section to an external file. Create the external file with the correct code in it. Same with the SMTP section. The download sample code should work here. You just need to change the connection name and connection string to match your system.
  8. Add the Class Library project (ConfigItC# or ConfigItVB) to your web project solution. Add a reference to it in the web project.
  9. Add a global.asax file and add an application_start method. You can use the download sample one.

Trimmed Down Version

If you really don't think you will ever be using the WebConfigurationManager.ConnectionStrings or the SMTP stuff, you can always change the EnvironmentSettings class. Remove OpenWebconfiguration and all the code that compares stuff and saves config files.

Conclusion

I hope you find this solution to managing your web.config files across environments helpful. It was kind of a pain to figure out, but now that it is finally working, it really saves me a bunch of headaches. I do want to acknowledge Steve Rowe for his help with some of the details of setting up custom configuration settings.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here