Environmental Config Transforms, Part 1
Configuration file transforms have been around for quite a while and most companies use them to generate different config files for each environment but Visual Studio does not really support the functionality most companies need.In this article I’m going to talk about why the existing Visual Studio implementation is flawed and how to fix it so that you can have true environmental transforms that are actually useful.
For purposes of this article we will assume a simple web application (although it applies to any application) that will be deployed to 2 different environments – test and production. We will also assume that developers debug the application on their local machine and therefore should not have to do any special tasks to debug the application locally. As such we’ll have a 3th, default, environment – debug.
Update 28 Aug 2016: Refer to this link for updated code supporting Visual Studio 2015.
Per Configuration Transforms
One very big issue with the existing VS implementation of transforms is that it is tied to the configuration (Debug, Release) of the project being built. For example if you’re building a project in debug mode then only the debug transform is run. There are several problems with this approach. The first problem is that VS is tying environmental settings to build configurations. I don’t know about you but since my earliest years in the business I have heard (and followed) the philosophy that each (potentially releasable) build has to be tested. Therefore if we have 3 environments we would need to have 3 different configurations (one for each environment) and a separate transform for each. Furthermore we’d have to build the application 3 times even though just the transform should change. When you start getting into large applications with many different projects it becomes clear that managing configuration per environment just doesn’t add up. A single build should be deployable to any environment along with its transformed config file.
Another issue with this approach is verification of transforms. If I make a change to the root config file or any transform I’d like to ideally build and verify all the transforms still work. I shouldn’t have to build my application for each environment to verify one change.
Clearly the VS approach of tying environmental transforms to build configurations just does not make sense. Unfortunately that is the default. Fortunately the rest of this article will show you how to create environmental transforms that avoid these issues. Specifically this article will allow the following:
- Have 1 transform for each environment (not configuration)
- Validate all transforms at build time
- Provide the final, transformed configs for each environment to simplify deployment
- Make it very easy to add environmental transforms to new projects
For this article I’ve created a simple ASP.NET application that simply displays the environment name. It was generated using VS 2012 and then stripped of everything except the default page with a label for the environment name.
Per Environment Transforms
To get started we need to agree upon an approach. Following the default VS approach we will create a separate config file for each environment called web.environment.config
. Ideally these transforms will be stored under the config file. Within each environmental transform will be the standard transforms for that environment. For example in development we will likely leave debugging information turned on but in the production environment we will turn off all debugging.
Assuming you have an application open you would simply add a new config transform for each environment (test, production). Since the transform syntax can be a little different the easiest approach is to simply copy and paste the existing web.debug.config
transform. Since we do not want per configuration transforms then the default transforms can be removed. Here’s what you should have in the final project.
- Web.config
- Web.Production.Config
- Web.Test.Config
For clarification I’ll add a simple app setting entry that contains the environmental name so it is easier to identify which transform was used.
<appSettings> <add key="Name" value="Debug" /> </appSettings>
And the corresponding Test transform
<appSettings> <add key="Name" value="Test" xdt:Transform="SetAttributes" xdt:Locator="Match(key)"/> </appSettings>
Running the application at this point would print out “Debug” because VS cannot find a transform for the configuration. More importantly without a manual process we do not have the environmental transforms to test.
Configuration File Names
Before moving on it is important to note that there are two different types of config files we have to deal with. Web-based projects use a web.config
file. The filename does not change between design and run time. Web config files are simple to deal with. On the other hand Windows-based projects use app.config
at design time but the file is copied and renamed to myapp.exe.config
where myapp
is the name of the executable. To support the transformation of either type of file we have to write extra code to select the correct base name and transform the file to the correct final name. This will add some extra code later on but does not overly complicate the process any.
Building the Transforms
Whenever we do a build we want the transforms to run, irrelevant of configuration. This provides both the ability to validate any change to the config or the transforms at build time and to allow us to have the fully transformed configs so that we can deploy the build and its config to any environment, without the need for another build.
To trigger something at build time you are going to have to use MS Build. A project file is really nothing more than a set of MS Build tasks that run. To do something during a build you need only call a pre-defined task, or write your own. If the task is involved or is going to be used in many projects then it is best to store it separately in a targets file. .NET ships with lots of them. For building the transforms we will store the appropriate task in a targets file that can be easily reused in any number of project files.
Creating the Targets File
A description of how targets files and MS Build work is beyond the scope of this article. You can refer to the download file for the full file. I am just going to highlight the process of building the transforms. Here is the base flow:
- Get the source file to be transformed
- Get the list of transform files to be applied
- For each transform file apply it to the source file to get the output file
- Save the output file
All this work will require a custom task (TransformXmlFiles
). MS Build supports different approaches to building tasks but I’m opting for an inline task to simplify deployment. As such the task code is part of the file and contained within a UsingTask
.
First we need to set up some parameters for the transformation task. MS Build tasks are parameter-based so we define the following:
- SourceFile – The config file that will be transformed (
web.config
orapp.config
) - TransformFiles – The list of files to process
- OutputDirectory – The directory where the final transformed files will be stored.
- TargetFile – The name of the final transform file
- ProjectName – The project name
- ToolsDirectory – The path to the VS installation
To trigger a task the project file must either explicitly call the task or the task must hook into the build process. Since I want the .targets file to be as non-intrusive as possible in the project file I have opted to automatically run the transformation task after a build. Using this approach I can completely configure the task within the .targets file and project files need only import the file to get the behavior. Here’s the relevant section.
<!-- Because of the MS Build "bug" VSToolsPath is needed but it isn't always available so handle that case now --> <PropertyGroup> <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion> <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)</VSToolsPath> </PropertyGroup> <!-- Get the config transform files --> <ItemGroup> <WebConfigTransformFiles Include="web.*.config" Exclude="web.config" /> <AppConfigTransformFiles Include="app.*.config" Exclude="app.config" /> </ItemGroup> <!-- Runs after a successful build --> <Target Name="TransformConfigurationFiles" AfterTargets="AfterBuild"> <TransformXmlFiles TransformFiles="@(WebConfigTransformFiles)" SourceFile="web.config" TargetFile="web.config" OutputDirectory="$([System.IO.Path]::Combine($(OutDir), 'Configs'))" ProjectName="$(MSBuildProjectName)" ToolsDirectory="$(VSToolsPath)" /> <TransformXmlFiles TransformFiles="@(AppConfigTransformFiles)" SourceFile="app.config" TargetFile="app.config" OutputDirectory="$([System.IO.Path]::Combine($(OutDir), 'Configs'))" ProjectName="$(MSBuildProjectName)" ToolsDirectory="$(VSToolsPath)" /> </Target>
Notice that the transform task is called twice, once for each type of config. This is simpler (in a task) than trying to determine which type of config to transform. Notice also that in each case the parameters are set accordingly to ensure the final transform is generated. The targets file is set based upon whether we are transforming a web or app config.
The output directory is set to the project’s output directory with a subdirectory of Configs
. Additionally the project name is determined by the project property. This results in a hierarchy where all the configs are stored as subfolders based upon the project and environment name. This allows a single solution to have multiple projects with multiple environmental transforms and each one goes into a separate directory for ease of deployment.
MS Build “Bug”
All that remains is the actual logic for transforming the config file.
if (TransformFiles != null && TransformFiles.Length > 0) { //The reference assembly path is only used for compilation so force the assembly to load so it is available when we need it var assemblyWebPublishing = Assembly.LoadFrom(Path.Combine(ToolsDirectory, @"WebMicrosoft.Web.Publishing.Tasks.dll")); dynamic transform = assemblyWebPublishing.CreateInstance("Microsoft.Web.Publishing.Tasks.TransformXml"); //var transform = new TransformXml(); transform.BuildEngine = this.BuildEngine; transform.HostObject = this.HostObject; foreach (var inputFile in TransformFiles) { //Get the env name var fileParts = Path.GetFileNameWithoutExtension(inputFile.ItemSpec).Split('.'); var envName = fileParts.LastOrDefault(); //Build output directory as base output directory plus environment plus project (if supplied) var outDir = Path.Combine(OutputDirectory, envName); if (!String.IsNullOrEmpty(ProjectName)) outDir = Path.Combine(outDir, ProjectName); //Build the output path var outFile = Path.Combine(outDir, TargetFile); //Make sure the directory exists if (!Directory.Exists(outDir)) Directory.CreateDirectory(outDir); //Transform the config transform.Destination = outFile; transform.Source = SourceFile; transform.Transform = inputFile.ItemSpec; transform.Execute(); }; };
The actual transformation is triggered by calling the same code that VS itself uses. That functionality resides in a web publishing assembly shipped with VS. Note: If you use TFS Build with a project using this .targets file then either VS needs to be installed on the build agent or the assembly needs to be copied to a location that MSBuild will use.
Unto itself this is trivial except for a “bug” in MSBuild. MSBuild allows you to specify assembly references for inline tasks and it will honor the path during compilation. Unfortunately when the task is then run the assembly will not be found because the runtime path is not being configured. I reported this bug at Connect but whatever engineer was assigned to the ticket completely missed the point of the bug and marked the bug as by design. Connect can be a nightmare to use because of the lackluster support that MS sometimes gives it. Rather than fighting that battle I simply worked around the issue by loading the assembly manually. Note the use of dynamic to work around the compiler issues.
Using the .targets file
That completes the .targets file. Now all we need to do is put it someplace the build can find it and then add an import into the project file. The standard location for shared .targets files is MSBuildExtensions32Path
which is created when .NET is installed and contains most of the standard .targets file. To keep things simple I’ll just reference the .targets file that is in the solution directly. For real solutions the .targets file should be stored in the standard location.
Now just add an import into the project file (I prefer at the bottom).
<Import Project="$(SolutionDir)TargetsP3Net.targets"/>
Rebuild and in the output directory should be the Configs
directory with the transformed files in subdirectories. To verify the validation is occurring you can add a bad transform in one of the transform files and you should get a compilation warning.
Next Time
We now have the ability to generate environmental transforms during a VS build for any type of project. We’ve solved the problem we started with but we can go further. The current solution, while slick, is a little much to do for every new project. Next time we’ll wrap this up in a deployable package and add a template to automate the setup of this stuff so we can add template and run on any new projects.
Incredible points. Great arguments. Keep up
the amazing spirit.
WOW just what I was looking for. Came here by searching for
C#
Great info. Lucky me I ran across your blog
by accident (stumbleupon). I’ve saved as a favorite for later!
I’m not sure where you are getting your info, but good topic. I needs to spend some time learning more or understanding more. Thanks for excellent info I was looking for this information for my mission.
Very nice post. I absolutely appreciate this website. Thanks!
Excellent web site you have here.. It’s hard to find good quality writing like yours nowadays. I seriously appreciate individuals like you! Take care!!
Pretty! This was a really wonderful article.
Thanks for supplying these details.
I was extremely pleased to find this site. I want to to thank you for your time due to this fantastic read!
! I definitely liked every part of it and I have you saved to fav to check out new information on your web site.