Friday, May 27, 2011

Manually caching configuration sections to avoid the .NET 4 bug

There is a bug in the .NET 4 ConfigurationManager that has been causing me some headache. If you are trying to run .NET 4 code from a network drive and are getting SecurityExceptions, you might be facing it. When you want to read a section from your App.config - I'll creatively call it MySection - you typically do this:

MySection mySection = ConfigurationManager.GetSection("MySection") as MySection;

In your XML configuration, that section is registered with a type - often referred to as the section handler, e.g. MySectionHandler, which is implemented in SomeAssembly.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="MySection" type="MySectionHandler, MyAssembly"/>
    </configSections>
    <MySection>
        <!-- some custom configuration here -->
    </MySection>
</configuration>

That type is what glues the XML to your original call to ConfigurationManager. It tells the .NET framework how to transform the configuration text into .NET objects - that process is called deserialization. The "serial" text is transformed into a higher-level, non-serial data structure. Let's have a look at a common section handler implementation:

public class MySectionHandler: IConfigurationSectionHandler
{
    public object Create(object parent, object configContext, XmlNode section)
    {
        MySection result = // deserialize instance of MySection
        return result;
    }
}

[XmlRoot("MySection")]
public class MySection
{
    // some custom configuration here
}

In this scenario, your call to ConfigurationManager.GetSection() causes the run-time to read in the App.config XML as text, pass it in to MySectionHandler, which in turn returns the deserialized object to the original caller. Now, the problem is that in .NET 4, ConfigurationManager.GetSection() has a bug. When you are running you application from a network location, it seems to consider the configuration files "unsafe" and will give you security exceptions when attempting to read the configuration. Microsoft support recommends a workaround:

MySection mySection = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).GetSection("MySection"); 


While this alternative call seems very much the same, there are two fundamental differences
  1. It only works for sections that are derived from ConfigurationSection
  2. OpenExeConfiguration does not cache deserialized sections
ConfigurationSection vs. IConfigurationSectionHandler

The first turns out to be a problem for those who create section handlers by implementing the IConfigurationSectionHandler interface (as I did in the above example). This pattern roots in the early days of the .NET framework and is obsolete since .NET 2.0. Nevertheless, it is very convenient. All you need to do is have a class implement the Create() method and return an object. How it creates the object is secondary, but typically you will use an XmlSerializer with the XmlNode parameter for deserialization. Also, since this is just an interface, you have complete freedom regarding your class design.

In order to make use of the recommended workaround you have to derive your section implementation from ConfigurationSection. Using the .NET configuration API you can define your section with a bit more power and flexibility than using the XML serialization API. You can have the run-time validate that the values in your configuration are of a certain numerical range or default to specific strings - handy stuff. The downside is, though, that it's usage is quite verbose. You need to implement new classes with lots of abstract methods, which gets quite bloated once you are working with complex XML, especially when you compare it with the much leaner IConfigurationSectionHandler pattern.

Manual vs. automatic caching

While using the Configuration API might be a bit clunky, it's something you can get used to. The second issue you will encounter when using the above workaround is a bigger problem. OpenExeConfiguration() is designed to allow modifications to the underlying configuration. Therefor, by design, it cannot cache the sections it deserializes. If you take code that expects the caching of ConfigurationManager.GetSection and just replace it with calls to OpenExeConfiguration().GetSection(), you can end up repeating the file I/O and deserialization process over and over again - which can lead to a serious performance impact.

Given the scenario of my above example, you could verify the caching with the following pseudo-test:

object first =  ConfigurationManager.GetSection("MySection");
object second = ConfigurationManager.GetSection("MySection");
// first == second will give 'true'

first = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).GetSection("MySection"));
second = 
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).GetSection("MySection"));
// first == second will give 'false'

The obvious way to get around that is to hold on to a reference to your Configuration object. But that's not applicable all the time and feels a bit dirty - you end up with references to the same object all over the place. Also, since this is just a bug in the .NET framework that is supposed to be fixed with SP1, you would be making lots of modifications to your code that will be useless as soon as SP1 is released. Instead, you could write an extension method that will use the recommended workaround call, but include simple "manual" caching.


A workaround for the workaround

First, we create a simple extension method that will be available on Configuration instances.

using System.Configuration;

public static class ConfigurationExtensions
{
    private static readonly CachedConfigurationSectionLoader cachedSections = new CachedConfigurationSectionLoader();

    public static ConfigurationSection GetSectionCached(this Configuration configuration, string sectionName)
    {
        return cachedSections.GetSection(sectionName);
    }
}

The extension method makes use of a class called CachedConfigurationSectionLoader. It simply maintains a ConcurrentDictionary to store the deserialized objects in a clean and thread-safe manner.

using System.Configuration;
using System.Collections.Concurrent;

public class CachedConfigurationSectionLoader
{
    private readonly Configuration exeConfiguration;
    private readonly ConcurrentDictionary<string, ConfigurationSection> configurationSectionCache = new ConcurrentDictionary<string, ConfigurationSection>();

    public CachedConfigurationSectionLoader()
        : this(ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None))
    { }

    // use this for custom configuration objects
    public CachedConfigurationSectionLoader(Configuration configuration)
    {
        this.exeConfiguration = configuration;
    }

    public void ClearCache()
    {
        this.configurationSectionCache.Clear();
    }

    public ConfigurationSection GetSection(string sectionName)
    {
        return this.GetSection(sectionName, this.exeConfiguration);
    }

    public ConfigurationSection GetSection(string sectionName, Configuration configuration)
    {
        Func<string, ConfigurationSection> configurationSectionFactory = (s) => exeConfiguration.GetSection(s);
        ConfigurationSection result = configurationSectionCache.GetOrAdd(sectionName, configurationSectionFactory);

        return result;
    }
}

Usage of the extension method looks almost exactly like the suggested workaround, and is not far away from our original calls to ConfigurationManager.GetSection():

MySection mySection = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).GetSectionCached("MySection");

Now you can change all of your existing calls to ConfigurationManager.GetSection() in place, without having to redesign your classes just to get around this bug. Reverting these changes after SP1 is quite simple. You could just change all the calls back to as they were before the workaround, or - if you really want to keep changes minimal - simply replace the extension method with this:

public static class ConfigurationExtensions
{
    public static ConfigurationSection GetSectionCached(this Configuration configuration, string sectionName)
    {
        // redirect to built-in cached function
        return ConfigurationManager.GetSection(sectionName) as ConfigurationSection;
    }
}

Additional resources:
That nasty bug in the .NET 4 ConfigurationManager.GetSection
Serialization and Deserialization in .NET
Unraveling the Mysteries of .NET 2.0 Configuration
The awesome concurrent collections in .NET 4

Thursday, May 5, 2011

Conditional References in Visual Studio projects

Visual Studio does a good job in hiding the MSBuild configurations from it's users. MSBuild is quite flexible and powerful - but the VS user interface only gives you control over a tiny piece of it. One handy feature I believe deserves more attention in the IDE is conditional references. When you add a reference to one of your projects, by default it will be static. No matter how you modify your solution configuration or platform settings.


But some libraries are platform dependent. If you build your solution as 64bit, you will not want to use a version of a library that is restricted to 32bit platforms. Visual Studio does not give you a way to put conditions on your references. Fortunately, MSBuild does. When referencing an assembly in VS, a Reference (or ProjectReference) element is added to your project file's XML containing details of the assembly and a HintPath to it's (supposed) location. 

<Reference Include="MyAssembly, processorArchitecture=MSIL">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>path\to\MyAssembly.dll</HintPath>
</Reference>

Setting the conditions

Using your favorite text editor, you can add a Condition attribute to any reference in your project file, telling MSBuild when to use it (or not). This allows you to reference assemblies of different architecture and make sure they are used according to which platform you are targeting. In this case, a reference could look like the following in your project file: 

<Reference Include="MyAssembly, processorArchitecture=MSIL"
           Condition="'$(Platform)' == 'x86'">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>path\to\32bit\MyAssembly.dll</HintPath>
</Reference>
<Reference Include="MyAssembly, processorArchitecture=MSIL" 
           Condition="'$(Platform)' == 'x64'">
    <SpecificVersion>False</SpecificVersion>
    <HintPath>path\to\64bit\MyAssembly.dll</HintPath>
</Reference>  

This works because in MSBuild the reference elements are Item elements. Many element types can have condition attributes. If you look at other parts of your project file you will probably see that the project configuration and platform sections also use conditions similar to the above example. There are other ways to define conditional logic in MSBuild, but this is probably the quickest and most understandable. If you plan to do more funky stuff, have a look at <Choose> and <Otherwise>.

Visual Studio will show a warning if a reference's condition evaluates to "False"


The Platform variable

Note that in my example I am querying the Platform variable - not Configuration. I have noticed a practice on teh webz that suggests you create new configurations named "Debug64" and "Release64", then switch references based on the Configuration variable. While this technically works the same way, it is logically incorrect. Configuration and Platform settings are two different concerns in Visual Studio and it is unwise to mix them. It's messy, redundant and generates loads of unnecessary lines in your solution and project files.

Assuming that your project has nicely separated concerns you should not have to make this change in more than one or three places. If you are facing a large number of projects that need this additional logic, this XML operation can easily be automated via a script or macro.

Tags