Mark Gilbert's Blog

Science and technology, served light and fluffy.

One web.config to rule them all

Most of the web-centric work I’ve done in my career and especially in the last four years has involved developing sites that are designed to be deployed to multiple environments: my local workstation, our internal Dev and Staging servers, and up to three environments at the client.  One of my strategies for managing the differences in file paths, email recipients, database connection strings, etc. among all of those environments is to push anything that changes from one environment to another into the web.config.  Then, I have a separate web.config for each environment, and I rename (or have the client’s technical staff rename) the appropriate file for a given environment.

TheSetup
That system has worked well for years.  That is, until a few weeks ago when I was assigned to a new client who insists that there only be a single web.config that covers all environments.  Doing this allows them to simply copy the files from one environment to the next wholesale, and eliminates the need to rename anything.  The other developers who have worked with this client for a while have created a couple of different frameworks for implementing this requirement, but they boil down to the same basic approach:

1) Put all values for all environments into the web.config, but tie them to the corresponding URL for each environment.

2) At runtime, figure out the URL that the site is being executed under, and look up the values for that URL in the web.config.

My colleagues affectionately refer to this scheme as “one web.config to rule them all”.

TheChallenge
A couple of the key components that I incorporate into the sites I work on are ELMAH and Castle ActiveRecord.  Naturally, since my current task involves building three brand new sites, I wanted to drop these in from the beginning.  The challenge was how to use them given this client’s requirement.  The hard part of this was not figuring out how to put 5 environments’ worth of values into a single web.config – that’s already a solved problem in my shop (and I’m sure you could come up with your own approach).  The hard part here was figuring out to programmatically override the values that I would normally put into a web.config.

TheSolution – ELMAH
Let’s start with ELMAH.  Normally, I’d have these sections in my web.config (only the ELMAH-specific portions are shown here):

<configuration>

  <configSections>
    <!-- 
            Error Logging Modules and Handlers (ELMAH) 
            Copyright (c) 2004-7, Atif Aziz.
            All rights reserved.
        -->
    <sectionGroup name="elmah">
      <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah"/>
      <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah"/>
      <section name="security" type="Elmah.SecuritySectionHandler, Elmah"/>
      <section name="errorFilter" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
    </sectionGroup>
  </configSections>

  
  <system.web>
    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
      <add name="ErrorMail" type="Elmah.ElmahMailModule"/>
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
     </httpModules>
  </system.web>

  <elmah>
    <errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/bin/Logs"/>

 


 <errorMail from="me@blah.com"
 to="blah@blah.com"
 subject="Test Error"
 smtpServer="blah.com"/>
 <security allowRemoteAccess="yes"/>
 <errorFilter>
 <test>
 <equal binding="HttpStatusCode" value="404" type="Int32"/>
 </test>
 </errorFilter>
 </elmah>

 

</configuration>
 

The things that differ per environment are:

*) The “logPath” attribute of the elmah/errorLog tag

*) The “to”, “subject”, and “smtpServer” properties of the “elmah/errorMail” tag.

My colleague, Joel, found that you can write a class that inherits from Elmah.ErrorMailModule, override the settings there, and use that in the httpModules block.  First, the class:

Public Class ElmahMailExtension
    Inherits Elmah.ErrorMailModule

    Protected Overrides Function GetConfig() As Object
        Dim o As Object = MyBase.GetConfig()
        o("smtpServer") = "mail.blah.com"
        o("subject") = String.Format("Blah message at {0}", Now.ToLongTimeString())
        o("to") = "me@blah.com"
        Return o
    End Function
End Class

And the web.config modification:

<configuration>
 
  <system.web>

    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
      <add name="ErrorMail" type="MvcApplication1.ElmahMailExtension"/>
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah"/>
     </httpModules>

  </system.web>

</configuration>

Simple.

Overriding the logging settings requires a slightly different tack.  Instead of inheriting from Elmah.ErrorLogModule, I create a class that inherits from Elmah.XmlFileErrorLog:

Imports System.Web.Hosting

Public Class ElmahLogExtension
    Inherits Elmah.XmlFileErrorLog

    Public Sub New(ByVal config As IDictionary)
        MyBase.New(HostingEnvironment.MapPath("~/bin/Logs2"))
    End Sub

    Public Sub New(ByVal logPath As String)
        MyBase.New(logPath)
    End Sub

End Class

I couldn’t find a convenient collection to change values in, so I cheated.  Using JustDecompile, I looked at what the two constructors were doing.  They basically just manipulate the log path passed in.  So, I leave the New(string) variant alone, and modify the New(IDictionary) variant to ignore the incoming “config” parameter, and substitute the path that I want to use.  One of the things that I noticed the XmlFileErrorLog constructor doing was replacing paths with leading “~/” with the full path on the file system.  Full log paths won’t require this.
TheSolution – ActiveRecord

Here is a common ActiveRecord configuration for me (just the ActiveRecord-relevant parts are shown here):

<configuration>

  <configSections>
    
    <section name="activerecord" type="Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler, Castle.ActiveRecord" />
    <section name="nhibernate" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />

  </configSections>

  <activerecord isDebug="false" threadinfotype="Castle.ActiveRecord.Framework.Scopes.HybridWebThreadScopeInfo, Castle.ActiveRecord">
    <config database="MsSqlServer2005" connectionStringName="MyTestDB">
    </config>
  </activerecord>

  <log4net>
  ...

  </log4net>

  <nhibernate>
  ... 
  </nhibernate>

  <connectionStrings>
    <add key="MyTestDB" value="Database=MyDBName;Server=MyServer,1433;User ID=MyUser;Password=MyPassword;" />
  </connectionStrings>

</configuration>

My Application_Start method in Global.asax initializes ActiveRecord:

Sub Application_Start()
    AreaRegistration.RegisterAllAreas()

    Dim MyConfig As IConfigurationSource = Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler.Instance
    Dim MyAssemblies As System.Reflection.Assembly() = New System.Reflection.Assembly() {System.Reflection.Assembly.Load("MvcApplication1")}
    ActiveRecordStarter.Initialize(MyAssemblies, MyConfig)
    AddHandler Me.EndRequest, AddressOf Application_EndRequest

    RegisterRoutes(RouteTable.Routes)
End Sub

The primary piece that I need to override is the connection string.  My first attempts were in a similar vein as with ELMAH – in this case create a class that inherits from Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler, and use that in the web.config.  However, I found an even easier way – simple use a different IConfigurationSource object in the call to ActiveRecordStarter.Initialize – one that is constructed programmatically.  As it turns out, there is even a built-in class to do this – InPlaceConfigurationSource:

Sub Application_Start()
    AreaRegistration.RegisterAllAreas()

Dim MyConfig As InPlaceConfigurationSource = InPlaceConfigurationSource.Build(DatabaseType.MsSqlServer2005, “Database=MyTest;Server=blahsqlsrvr,1433;User ID=blah;Password=blah;”) MyConfig.ThreadScopeInfoImplementation = GetType(Framework.Scopes.HybridWebThreadScopeInfo)

    Dim MyAssemblies As System.Reflection.Assembly() = New System.Reflection.Assembly() {System.Reflection.Assembly.Load("MvcApplication1")}
    ActiveRecordStarter.Initialize(MyAssemblies, MyConfig)
    AddHandler Me.EndRequest, AddressOf Application_EndRequest

    RegisterRoutes(RouteTable.Routes)
End Sub

Setting the ThreadScopeInfoImplemention property allows me to reproduce the “threadinfotype” property of the <activerecord> block in the web.config.

Using this allows me to completely dump the <activerecord> block and the configSection/section that references it:

<configuration>

  <configSections>
    
    <!--<section name="activerecord" type="Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler, Castle.ActiveRecord" />-->
    <section name="nhibernate" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />

  </configSections>

  <!--<activerecord isWeb="true" isDebug="false" threadinfotype="Castle.ActiveRecord.Framework.Scopes.HybridWebThreadScopeInfo, Castle.ActiveRecord">
    <config database="MsSqlServer2005" connectionStringName="MyTestDB">
    </config>
  </activerecord>—>

  ...  
</configuration> 

TheConclusion

I’m not sold on the “one web.config to rule them all” approach to maintaining environment settings, but at least I don’t have to give up my favorite frameworks as a result.

Advertisements

September 28, 2011 - Posted by | Castle ActiveRecord, Visual Studio/.NET

5 Comments

  1. […] my previous post I threw out this little bit of hand-waving: The hard part of this was not figuring out how to put 5 […]

    Pingback by One web.config to rule them all – eh, not so fast « Mark Gilbert’s Blog | October 17, 2011

  2. FYI, we have/had the same problem of needing to vary the connection string for Castle AR based upon the IIS host header (essentially the site URL). We happen to store the connection strings in the registry.

    Here is some example code from global.asax for how we did it:

    var properties = new Dictionary();
    properties.Add(“connection.driver_class”, “NHibernate.Driver.SqlClientDriver”);
    properties.Add(“dialect”, “NHibernate.Dialect.MsSql2008Dialect”);
    properties.Add(“connection.provider”, “NHibernate.Connection.DriverConnectionProvider”);
    properties.Add(“connection.connection_string”, RegistryConfigHelper.GetConnectionString(hostHeader, ApplicationConstants.ConnectionStandard));
    properties.Add(“proxyfactory.factory_class”, “NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle”);

    var source = new InPlaceConfigurationSource();
    source.IsRunningInWebApp = true;
    source.Add(typeof(ActiveRecordBase), properties);
    Assembly asm1 = typeof(Models.ProposalRequest).Assembly;
    ActiveRecordStarter.Initialize(new Assembly[] { asm1 }, source);

    Comment by Eric Tarasoff | January 27, 2012

  3. PS – in looking at your code example above, I don’t see that you set the IsRunningInWebApp setting (equivalent to “isWeb = true” in the web.config). I think you’ll want to do this if you’re not already for proper thread handling of your underlying NHibernate session.

    Comment by Eric Tarasoff | January 27, 2012

    • Good catch, Eric, thank you. I’ll have to work that into the next release.

      Comment by markegilbert | January 27, 2012

      • No problem! Glad I was able to help. Thanks for the article.

        Comment by Eric Tarasoff | January 30, 2012


Sorry, the comment form is closed at this time.

%d bloggers like this: