Thursday 20 August 2009

Configuration files in GAC assemblies

We had an interesting problem recently where-by we were rolling out integration libraries and installing them into the GAC on our windows servers.

The issue was that the only way to have the dlls respect configuration files was to copy the configuraiton file into the physical GAC path of the assembly, which can prove a bit tricky.

In the end we opted to use a slightly bespoke solution of packaging the configuration file in with the DLL and write a procedure for extracting that, this way we could ensure a single DLL file and have multiple configuration files for each environment.

To achieve this:

1. You need to create your App.config file and set the Build Action to Embedded Resource, this ensures that the config file is embedded into the resulting DLL.

2. Here's a snippet of the code to read the configuration and create a usable Congiruation instance (we ended up wrapping this up into a class, see link at the bottom of the article):


///
/// Ensures configuration exists.
///

public Configuration GetConfiguration()
{
var filename = EnsureConfigFile();
var map = new ExeConfigurationFileMap
{
ExeConfigFilename = filename
};

if (File.Exists(filename))
{
return ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
}

return null;
}
///
/// Ensures that the configuration file is available to be read.
///

/// The configuration filename.
private string EnsureConfigFile()
{
var standardConfigFile = string.Empty;
if (StandardConfigFileExists(out standardConfigFile))
{
return standardConfigFile;
}

var assembly = ConfigurationFileAssembly ?? Assembly.GetCallingAssembly();
var configFileName = string.Concat(assembly.GetName().Name, ".config");
var filename = Path.Combine(Path.GetTempPath(), assembly.GetName().FullName);

if (!Directory.Exists(filename))
Directory.CreateDirectory(filename);

filename = Path.Combine(filename, configFileName);

var info = new FileInfo(filename);
if (!info.Exists || info.LastWriteTime.Date <= DateTime.Now.AddDays(1) || (Debugger.IsAttached))
{
// Be aware that this assembly.GetManifestResourceStream is case sensitive so you need an "App.config" file.
var resource = string.Format("{0}.App.config", assembly.GetName().Name);
var stream = assembly.GetManifestResourceStream(resource);
if (stream != null)
{
var reader = new StreamReader(stream);
File.WriteAllText(filename, reader.ReadToEnd());
}
else
{
throw new Exception("Unable to access App.config as an embedded resource.");
}
}
return filename;
}


Now that you have an App.config file that is an embedded resource you can replace it when you do your builds with your environment-specific config file before you build your project:



Now, there is one handicap with this and that's that you can't update the configuration file once it has been built and compiled, but this isn't such a bad thing as it enforces you to follow a correct build/release process when deploying the assemblies.

ServiceConfiguration.cs