Dealing with the Configuration Nightmare
Typically during an overview of some new technology they will say "and it's totally configurable" at which point my inner sarcastic voice pipes up in response "great, now we have two problems".
Configuration files are getting bigger and more complex. In some ways this is a good thing as there's a move towards a declarative approach, but in other ways it's unneeded as the cost of compilation has gone down. The real problems with the move towards configurable everything is the configuration files often lack designer support, and do not get verified at compile time.
I can't help with the lack of designers or verification, but I do have some solutions for managing the proliferation of config files and multiple environments.
In a typical enterprise development project, you may have as many as five environments to execute against.
- localDev - the PC you develop and unit test on
- sharedDev - a shared environment with minimal expectations around quality
- UAT/QA - a shared an environment with explicit quality expectations - typically this means that developers publish a list of known bugs to testers before the release and the new bugs discovered by testers will be addressed
- Production - this system that users use
- COB/DR - the backup system that users will use "in case of"
I've seen a number of approaches for dealing with configuration files and multiple environments
0. Edit the configuration file by hand right after you deploy.
If this approach works for you, then god bless. You can stop reading this post. On the other hand, if this turns your hair gray, or if you're deploying technology has has tamper protection (such as ClickOnce), then keep reading
1. Put all environmental information into the single configuration file. Typically this approach it is combined with a UI addition allowing the user the option to select which environment to use.
This approach has some merits. Everything is in a single location, so it's easy to find and maintain. On the other hand this approach makes it way too easy to run un-tested development code against a production environment, and doesn't completely solve the configuration problem - and that it doesn't tell a server component what environment it's currently running in.
2. Keep 5 separate configuration files, one for each environment.
This approach looks the DRY principle in the eye, and says " what are you going to do about it?" It's fine if you're feeling tough, and in the mood for a maintenance headache. For example if you add a new logging sink, you need to edit 5 files.
3. Treat your configuration files as code to be compiled
Benefits: no duplication of effort, automated, safe, unambiguous, auditable.
Cons: Takes about 1 developer day to setup.
Here's how it works. The configuration files for each assembly are always kept in localDev configuration. A new file, call it Deployment.Properties is the added of root of the solution. For each assembly, a new file is applied called DeployTransforms.Properties is added.
The Deployment.Properties file is kept in MSBuild format and represents the golden definition of environment settings. It might look something like this
<PropertyGroup Condition="'$(DeployEnv)' == 'sharedDev'"> <AccountsDBServer>accountsdev.company.com</AccountsDBServer> </PropertyGroup> <PropertyGroup Condition="'$(DeployEnv)' == 'UAT'"> <AccountsDBServer>accountsUAT.company.com</AccountsDBServer> </PropertyGroup> <PropertyGroup Condition="'$(DeployEnv)' == 'Prod'"> <AccountsDBServer>accounts.company.com</AccountsDBServer> </PropertyGroup> <PropertyGroup> <AccountsDBCS>Data Source='$(AccountsDBServer)';Integrated Security=True;</AccountsDBCS> </PropertyGroup>
The DeployTransforms.Properties file, also kept in MSBuild format defines how the golden values from the Deployment.Properties are applied to the [App|Web].config file.
It might look like this:
<ItemGroup> <ConfigTransform Include="/configuration/connectionStrings/add[@name='AccountsConnectionString']/@connectionString"> <Value>$(AccountsDBCS)</Value> </ConfigTransform> </ItemGroup>
Of course you would add additional properties and config transforms as needed.
The final steps are to include into your project the DeployTransforms.Properties as well as a DeployTransforms target. That might look like this:
<Import Project="ConfigTransform.properties" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="ConfigTransform.targets" /> <Target Name="Publish" DependsOnTargets="$(NonClickOnceDependsOn)">
and ConfigTransform.targets
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" /> <PropertyGroup> <PublishDependsOn Condition="'$(PublishableProject)'=='true'"> DeployTransform; $(PublishDependsOn); RollbackConfigFile; </PublishDependsOn> <NonClickOnceDependsOn> NonClickOnceDeployTransform; </NonClickOnceDependsOn> <ConfigFile>App.config</ConfigFile> <ConfigFileName>$(ConfigFile)</ConfigFileName> </PropertyGroup> <Target Name="DeployTransform" > <Copy SourceFiles="$(ConfigFile)" DestinationFolder="$(IntermediateOutputPath)" /> <Attrib Files="$(ConfigFile)" ReadOnly="false"/> <CallTarget Targets="UpdateConfigFile" /> </Target> <Target Name="NonClickOnceDeployTransform" > <CallTarget Targets="SetConfigFileName" /> <CallTarget Targets="UpdateConfigFile" /> </Target> <Target Name="SetConfigFileName"> <CreateProperty Value="@(AppConfigWithTargetPath->'$(OutDir)%(TargetPath)')"> <Output TaskParameter="Value" PropertyName="ConfigFileName" /> </CreateProperty> <CreateProperty Value="$(TeamBuildPublishDir)$(AssemblyName).exe.config" Condition="$(ConfigFileName)=='' and $(TeamBuildPublishDir) !=''"> <Output TaskParameter="Value" PropertyName="ConfigFileName" /> </CreateProperty> <CreateProperty Value="$(OutputPath)$(AssemblyName).exe.config" Condition="$(ConfigFileName)=='' and $(TeamBuildPublishDir) ==''"> <Output TaskParameter="Value" PropertyName="ConfigFileName" /> </CreateProperty> <CreateProperty Value="$(TeamBuildPublishDir)\_PublishedWebsites\$(MSBuildProjectName)\Web.config" Condition="$(WebApplication)=='true' and $(TeamBuildPublishDir) !=''"> <Output TaskParameter="Value" PropertyName="ConfigFileName" /> </CreateProperty> <CreateProperty Value="App.config"> <Output TaskParameter="Value" PropertyName="SrcConfigFileName" /> </CreateProperty> <CreateProperty Value="Web.config" Condition="$(WebApplication)=='true'"> <Output TaskParameter="Value" PropertyName="SrcConfigFileName" /> </CreateProperty> </Target> <Target Name="UpdateConfigFile"> <Error Text="%(ConfigTransform.Identity) has no value for DeployEnv: '$(DeployEnv)'" Condition="'%(ConfigTransform.Value)' == ''" /> <CallTarget Targets="CopyConfigFile"/> <XmlUpdate Prefix="n" Namespace="http://schemas.microsoft.com/developer/msbuild/2003" XmlFileName="$(ConfigFileName)" Xpath="%(ConfigTransform.Identity)" Value="%(ConfigTransform.Value)" /> </Target> <Target Name="CopyConfigFile" Outputs="$(ConfigFileName)" Inputs="ConfigTransform.properties;..\DeployEnv.properties"> <Copy SourceFiles="$(SrcConfigFileName)" DestinationFiles="$(ConfigFileName)" /> </Target>
This approach is invoked via msbuild /t:Publish /p:deployEnv=(UAT|Prod...)
The outputs of the build are now configured for the intended environment.