# Saturday, January 05, 2008
« Deserializeelement Cannot Process Elemen... | Main | Finding the Root Source Control Folder U... »

Environment specific configuration can be a real pain to manage effectively.  I think I've tried every approach imaginable and it seems there is no perfect solution.  I just switched my many environment specific configuration files (I actually got to delete ~25 config files from SVN!) over to a handful of Windsor configuration files that use conditional elements and includes like so:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <properties>
    <?if LIVE?>
      <billingGatewayHost>webserver1</billingGatewayHost>
      <sessionConnectionString>server=dbserver1;database=ASPStateNET2;integrated security=SSPI</sessionConnectionString>
    <?elseif TEST?>
      <billingGatewayHost>webserver3</billingGatewayHost>
      <sessionConnectionString>server=dbserver3;database=ASPStateNET2;integrated security=SSPI</sessionConnectionString>
    <?end?>
  </properties>
</configuration>

 

I had so many configuration files because I had 5 environments, plus a session state, http module, appSettings, and some other stuff broken out into separate files using the standard .NET include section file mechanism.

I think I like the conditionals better than separate files for each environment which requires a postbuild process either at build or deployment time to merge or copy XML.  Also, it should be pretty obvious to any developer what's going on here.  Unfortunately the standard ASP.NET configuration mechanism doesn't have anything like this, and what's worse is that some components - like session state as in my case - can only be configured through the web.config.

That means no overriding the configuration at runtime or using Windsor.  No, the internal SqlSessionStateStore class in the FCL reads directly from the web.config using an internal static helper class.  This means no inheritance or injecting my configuration, even with the help of reflection.

ASP.NET does support custom session state providers, however I just want to use the SqlSessionStateStore class with a different configuration mechanism (I would really like to wire the thing up using Windsor); and creating a brand new provider is a lot of work.

Since all of my properties (i.e. connection strings) are in an external Windsor configuration file, it makes the most sense to also put my conditional session state connection strings in there as well (if you haven't picked up on it yet, I'm using SQL Server to store my session data, hence the SQL connection string for session).

Here's how I ended up getting the SqlSessionStateStore to use the connection string from my Windsor config...  And here is the hack in the global.asax.cs... 

private static volatile bool appDomainIsShuttingDown = false;
 
public override void Application_Start(Object sender, EventArgs e)
{
    base.Application_Start(sender, e);
 
    if (IoC.Resolve<SessionStateConnectionStringSetter>().SyncWebConfigSettingIfDifferent())
    {
        HttpRuntime.UnloadAppDomain();
        appDomainIsShuttingDown = true;
    }
}

 

On application start my base Application_Start method gets my DI container up and running.  After that I ask the container for a SessionStateConnectionStringSetter instance which is injected with the proper session DB connection string from the container.  All this class does is modify the web.config if the connection string it has differs from the one in the web.config.  After that, if the web.config has been modified, the current app domain is shutdown and the appDomainIsShuttingDown flag is set to true. 

In case you're not sure, static variables are scoped to the current app domain by default, so the next request for the web app will have the appDomainIsShuttingDown variable set to false (it's default value).  I think there is a threading bug in there if multiple application start events get fired off simultaneously, so I should probably add a lock around the web.config update.  I'll have to look into that.  Anyway, here's the class that updates the web.config with the new session state connection string.

public class SessionStateConnectionStringSetter
{
    private readonly string sessionStateConnStr;
 
    public SessionStateConnectionStringSetter(string sessionConnectionString)
    {
        this.sessionStateConnStr = sessionConnectionString;
    }
 
    public bool SyncWebConfigSettingIfDifferent()
    {
        ExeConfigurationFileMap map = new ExeConfigurationFileMap();
        map.ExeConfigFilename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web.config");
 
        Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
        SessionStateSection session = (SessionStateSection)config.GetSection("system.web/sessionState");
 
        if (session.SqlConnectionString != sessionStateConnStr)
        {
            session.SqlConnectionString = sessionStateConnStr;
            config.Save();
            return true;
        }
 
        return false;
    }
}

 

Further along in the ASP.NET pipeline the Application_BeginRequest method executes and the current user request (the one that initiated the web.config to be updated in the first place) is redirected - back to the same page they asked for originally.  This is done so the user doesn't see a nasty exception that will likely happen if processing continues with the original incorrect session DB connection string.

protected void Application_BeginRequest(object sender, EventArgs e)
{
    // This will cause the user to re-request the page using a new appdomain
    if (appDomainIsShuttingDown)
        Response.Redirect(Request.Url.ToString());
}

 

The thing to note about all this, is that the web.config update should only ever happen on the very first request for the application after a deployment, which not too surprisingly will almost always be the person doing the deployment (assuming they check their work). 

I wish I could find a better way to do this, but as far as I can tell there's no other way to set this programmatically at runtime.  I may just break down and add a special case to my NSIS installer to update the web.config session connection string from my Windsor configuration.  I really just wanted to see if I could get this to work without a build or deploy step...

Name
E-mail
(will show your gravatar icon)
Home page

Comment (Some html is allowed: a@href@title, strike) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

Enter the code shown (prevents robots):

Live Comment Preview