Click here to Skip to main content
Click here to Skip to main content

Using custom configuration section and custom actions to update and deploy App.config for different regions dynamically with Msi package

, 23 Jan 2012
Rate this:
Please Sign up or sign in to vote.
Article attempts to solve hassle of maintaining and deploying multiple configuration files for different testing/deployment stages/environments.

Multiple App.config files

Introduction

Article attempts to solve hassle of maintaining and deploying multiple configuration files for different testing/deployment stages/environments.

Target Audience

This article is for those who are in Winform application development/deployment and deploy application(s) using MSIs on different testing stages till production.

What Problem Does This Article Solve?

Multiple App.Config files

If you maintain multiple App.config files per testing/deployment stages, you would have different value(s) for keys in appSettings section per testing/deployment stage. For an example :

//Development environment
<add key="useURL" value="http://developmentURL/useURL" />
//Test/Deployment stage 1 
<add key="useURL" value="http://Stage1URL/useURL" />
//Test/Deployment stage 2 
<add key="useURL" value="http://Stage2URL/useURL" />

In addition to this for a single new line and you need to go through the pain of finding and updating all App.config files. This article attempts to solve this problem by keeping single App.config file and updating the file dynamically for each deployment region during installation of application.

Prerequisites

The article uses custom configuration section and custom actions (i.e. Installer class) with windows application setup projects (i.e. MSI) to update and deploy App.config files for different deployment regions at installation time. Basic knowledge of these is required. If you don't know how to create and use custom configuration classes Click here. If you don't know how to create and use custom actions (i.e. Installer class) Click here.

Background

I have been involved in Winform application development from last few years and recently got the deployment responsibilities. During this time I realized the various pain areas of updating configuration files for different regions i.e development, Testing, production etc. For every region configuration files are required to be updated which is quite error prone activity.

Now here comes the destiny to save me from this pain. I recently got training (God bless my company Smile | :) ) on creating custom configuration classes and the next moment I knew the best place to utilize this knowledge.

I'll be calling testing/deployment stage/environment Deployment Region throughout this article from now.

What's the Idea?

The problem is mostly with the keys' values in appSettings section specific to deployment region. The idea to solve this is to keep a separate section for deployment regions and add key-value pair(s) per specific region. Create a custom action (i.e. Installer) class and override Install method. Add logic to overridden Install method to read custom configuration from App.config, update key values, remove deployment region section detail and save it.

See below how the App.config will look in development:

<deploymentRegionSection>
    <DeploymentRegions>
      <DeploymentRegion RegionName="RegionOne">
        <AppKeys>
          <AppKey Key="targetURLFirst" Value="http://RegionOne/targetURLFirst"/>
        </AppKeys>
      </DeploymentRegion>
      <DeploymentRegion RegionName="RegionTwo">
        <AppKeys>
          <AppKey Key="targetURLFirst" Value="http://RegionTwo/targetURLFirst"/>
          <AppKey Key="targetURLSecond" Value="http://RegionTwo/targetURLSecond"/>
        </AppKeys>
      </DeploymentRegion>
    </DeploymentRegions>
</deploymentRegionSection>
<appSettings>
    <add key="targetURLFirst" value="http://DevelopmentRegion/targetURLFirst"/>
    <add key="targetURLSecond" value="http://DevelopmentRegion/targetURLSecond"/>
</appSettings>

Now when I deploy application in deployment region RegionOne it should look like:

<appSettings>
    <add key="targetURLFirst" value="http://RegionOne/targetURLFirst"/>
    <add key="targetURLSecond" value="http://DevelopmentRegion/targetURLSecond"/>
</appSettings>

And when I deploy application in deployment region RegionTwo it should look like:

<appSettings>
    <add key="targetURLFirst" value="http://RegionTwo/targetURLFirst"/>
    <add key="targetURLSecond" value="http://RegionTwo/targetURLSecond"/>
</appSettings>

Solution step-by-step

Here are the steps how we'll achieve the above (assuming that you already have a windows application and a setup project):

  • Create a class library project which will contain classes to handle custom deployment regions section.
  • Add classes to read deployment region section/elements from configuration file to above project.
  • Add a installer class to winform application. We'll call it SetupHelper.
  • Add code to SetupHelper class to fetch target directory, read custom configuration section and update config file.
  • Update App.config file to contain deploymentRegionSection.
  • Add winform application assembly to Install custom action in setup project
  • Modify CustomActionData property of above action to pass value of TARGETDIR to targetDirectory parameter

Solution in detail

Creating a class library project to read deploymentRegionSection

Add a new class library project to the solution and name it CustomConfigSectionAndActions.DeploymentRegionSectionHandler

Add classes to read deployment region section/elements to CustomConfigSectionAndActions.DeploymentRegionSectionHandler

Add following 5 classes to the project:

  • DeploymentRegionConfigurationSection.cs - To read the configuration section and a property of element collection of deployment regions.
  • DeploymentRegionElementCollection.cs - Collection of deployment region elements.
  • DeploymentRegionElement.cs - Element to contain region specific app keys and a property to define region's name.
  • AppKeyValueElementCollection.cs - Collection of AppKey value elements for a specific region.
  • AppKeyValueElement.cs - Key-value pair element to have region specific key's value.

I'm not showing any code here as it's pretty straightforward and you've source code to download.

Adding SetupHelper class to winform application (i.e.CustomConfigSectionAndActions.WinformApplication)

1. Right click on CustomConfigSectionAndActions.WinformApplication project node and click on Add > New Item select Installer Class and provide SetupHelper as class name
2. Add a private string variable _targetDirectory to this class
3. override Install method

public override void Install(IDictionary stateSaver)
{
    base.Install(stateSaver);

    _targetDirectory = Context.Parameters["targetDirectory"];
    if (!string.IsNullOrEmpty(_targetDirectory))
    {
        UpdateDeploymentRegions();
    }
}
    

Context.Parameters["targetDirectory"] will give target directory path which will be passed by MSI to SetupHelper.

4. Add method to read and update configuration file

private void UpdateDeploymentRegions()
{
    string sourceExePath = Path.Combine(_targetDirectory, "CustomConfigSectionAndActions.WinformApplication.exe");
    string deploymentRegionSectionHandlerDllPath = Path.Combine(_targetDirectory, "CustomConfigSectionAndActions.DeploymentRegionSectionHandler.dll");
    
    //Load configuration
    Configuration sourceConfiguration = ConfigurationManager.OpenExeConfiguration(sourceExePath);
    if (sourceConfiguration != null)
    {
        //Load deployment region configuration section
        DeploymentRegionConfigurationSection deploymentRegionSectionHandler = GetCustomConfiguration<DeploymentRegionConfigurationSection>(deploymentRegionSectionHandlerDllPath, sourceExePath + ".config", "deploymentRegionSection"); 
        
        if (deploymentRegionSectionHandler != null)
        {
            //Find current stage/region
            string currentRegion = CurrentDeploymentRegion();
            //Check if deployment config section contains key to be updated for current region otherwise 
            //default app.config keys will be used
            foreach (DeploymentRegionElement deploymentRegion in deploymentRegionSectionHandler.DeploymentRegions)
            {
                //Found current region in deployment configuration section
                if (deploymentRegion.RegionName == currentRegion)
                {
                    //Find each key and update it
                    foreach (AppKeyValueElement appKey in deploymentRegion.AppKeys)
                    {
                        //Find key in appSettings section of source App.config file
                        KeyValueConfigurationElement sourceKeyValue = sourceConfiguration.AppSettings.Settings[appKey.Key];
                        if (sourceKeyValue != null)
                        {
                            //Update the value
                            sourceKeyValue.Value = appKey.Value;
                        }
                    }
                    //Remove the deployment region section from App.config during deployment
                    //we certainly don't require this
                    sourceConfiguration.Sections.Remove("deploymentRegionSection");
                    //Save all changes
                    sourceConfiguration.Save();
                    break;
                }
            }
        }
        else
        { //Deployment region configuration could not be loaded
        }
    }
}
    

The above method uses ConfigurationManager.OpenExeConfiguration function to load configuration file and passes winform exe's file name to it. If you don't know, App.config file gets renamed when deployed with exe file name, in current sample it will be CustomConfigSectionAndActions.WinformApplication.exe.config.Then it calls GetCustomConfiguration<> generic function to load deploymentRegionSection.

To read a config section ConfigurationManager.GetSection("Section Name") can simply be used but due to some reason CLR doesn't find CustomConfigSectionAndActions.DeploymentRegionSectionHandler.dll assembly (why? I'll address this in Points of Interest section) from target/installation directory and throws "cannot load assembly.." exception.

To resolve this, the code must handle AppDomain.CurrentDomain.AssemblyResolve event and return CustomConfigSectionAndActions.DeploymentRegionSectionHandler.dll assembly. Following is definition of GetCustomConfiguration<> generic function.

protected TConfig GetCustomConfiguration<TConfig>(string configDefiningAssemblyPath, string configFilePath, string sectionName)
        where TConfig : ConfigurationSection
{
    //Add event handler to resolve deployment region section handler assembly loading
    AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ConfigResolveEventHandler);
    //Load deployment region section handler assembly
    _configurationDefiningAssembly = Assembly.LoadFrom(configDefiningAssemblyPath);
    //Create exe configuration file map
    var exeFileMap = new ExeConfigurationFileMap();
    exeFileMap.ExeConfigFilename = configFilePath;
    //Open configuration file
    var customConfig = ConfigurationManager.OpenMappedExeConfiguration(exeFileMap, ConfigurationUserLevel.None);
    //Load deployment region section
    var returnConfig = customConfig.GetSection(sectionName) as TConfig;
    //Remove event handler
    AppDomain.CurrentDomain.AssemblyResolve -= ConfigResolveEventHandler;
    return returnConfig;
}

protected Assembly ConfigResolveEventHandler(object sender, ResolveEventArgs args)
{
    return _configurationDefiningAssembly;
}

After loading deploymentRegionSection it's time to find current region in which the application is being deployed. Add CurrentDeploymentRegion function:

private string CurrentDeploymentRegion()
{
    //Add logic here to find the current deployment region
    return "RegionTwo";
}
For sample purpose I'm returning hard-coded string value. Mostly it's the logged-in domain name from registry but you can add whatever suits your application.

Updating winform appilcation's App.config file

Add sectionGroup to configSection section. Pass deploymentRegionSection value to name property. Pass deployment region configuration section handler's class name with full namespace to type property. see below:

<section name="deploymentRegionSection" 
             type="CustomConfigSectionAndActions.DeploymentRegionSectionHandler.DeploymentRegionConfigurationSection, 
             CustomConfigSectionAndActions.DeploymentRegionSectionHandler"/>

Finally add deploymentRegionSection to configuration file.

<deploymentRegionSection>
    <DeploymentRegions>
      <DeploymentRegion RegionName="RegionOne">
        <AppKeys>
          <AppKey Key="targetURLFirst" Value="http://RegionOne/targetURLFirst"/>
        </AppKeys>
      </DeploymentRegion>
      <DeploymentRegion RegionName="RegionTwo">
        <AppKeys>
          <AppKey Key="targetURLFirst" Value="http://RegionTwo/targetURLFirst"/>
          <AppKey Key="targetURLSecond" Value="http://RegionTwo/targetURLSecond"/>
        </AppKeys>
      </DeploymentRegion>
    </DeploymentRegions>
</deploymentRegionSection>

Make sure you've same key(s) in appSettings section:

<appSettings>
    <add key="targetURLFirst" value="http://DevelopmentRegion/targetURLFirst"/>
    <add key="targetURLSecond" value="http://DevelopmentRegion/targetURLSecond"/>
  </appSettings>

Add winform application to Install custom action in setup project

Go to Custom Actions by right clicking on CustomConfigSectionAndActions.CustomConfigSectionAndActionSetup project node and goto View>Custom Actions

View Custom Actions

Right click on Install node and select "Add Custom Action"

Add custom Action

Select Primary output from CustomConfigSectionAndActions.WinformApplication as action and set CustomActionData property value to /targetDirectory="[TARGETDIR]\". With CustomActionData we can pass parameters to installer class (i.e. SetupHelper).

Set CustomActionData property value

Congratulations! We're done it's time to build your setup project. Go ahead... build the MSI, install it on target machine and check out deployed configuration file.

Points of Interest

Why CLR couldn't load DeploymentRegionSectionHandler.dll assembly when it could load WinformApplicaiton.exe from same target directory?

Initially I created a sample winform application as a POC to update the configuration file and added App.config with deploymentRegionSection. Finally I added UpdateDeploymentRegions() function using ConfigurationManager.GetSection(...) to read deploymentRegionSection. Everything produced expected result. But same code in SetupHelper threw "cannot load ... DeploymentRegionSectionHandler ... assembly" exception.

The dll was already being copied to the target directory during installation so I thought probably I should put it in folder where the Msi file is running. Again same result. I knew the logic how CLR finds an assembly to load but based on that theory either i should've put my DeploymentRegionSectionHandler.dll in GAC or in my application's base directory. I didn't want to add it to GAC and I was assuming that either Msi folder or target directory is application's base directory.

The assumption was wrong. When we double click any Msi file, system passes this file to msiexec.exe (i.e. Windows Installer) because Msi is a package file it's not executable. The system keeps registry of File Extension - Application mapping and for any request, it checks the file extension, then the mapping list. If it finds any application for that extension it passes the file location to that application to process otherwise it prompts user for application he/she want to select to open/process that specific file. To confirm this you can go to Tools>Folder Options>File Types in window explorer and search for MSI entry.

When I extracted AppDomain.CurrentDomain.BaseDirectory it returned C:\WINDOWS\system32\ because msiexec.exe resides there. I copied DeploymentRegionSectionHandler.dll file to system32 and it worked like charm. But we can't do that too.

The only option left was to handle AppDomain's AssemblyResolve event and return assembly's reference. The only problem was to have full path to the dll assembly during installation but as Msi itself was passing targetDirectory it became easy to build the full path by appending CustomConfigSectionAndActions.DeploymentRegionSectionHandler.dll

License

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

About the Author

Hemant__Sharma
Technical Lead L & T Infotech
India India
No Biography provided

Comments and Discussions

 
GeneralMy vote of 1 Pinmemberssudarshanam27-Jan-14 2:30 
QuestionDownload doesn't work PinmemberMember 1015777816-Jul-13 0:26 
GeneralMy vote of 3 PinmemberMel Padden23-Jan-12 22:47 
GeneralRe: My vote of 3 PinmemberHemant__Sharma23-Jan-12 23:39 
GeneralRe: My vote of 3 PinmemberMel Padden24-Jan-12 0:11 
QuestionNice but pretty complex PinmemberAlois Kraus23-Jan-12 19:51 
AnswerRe: Nice but pretty complex PinmemberHemant__Sharma23-Jan-12 20:35 
GeneralRe: Nice but pretty complex PinmemberAlois Kraus24-Jan-12 10:57 
AnswerRe: Nice but pretty complex PinmemberHemant__Sharma24-Jan-12 19:01 

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 | Mobile
Web01 | 2.8.140721.1 | Last Updated 24 Jan 2012
Article Copyright 2012 by Hemant__Sharma
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid