Sunday, March 28, 2010

Using config transforms outside web projects

I saw a tweet from mdulghier yesterday that stated that "unfortunately the msdeploy config transforms don't work with app.configs (just web.configs)." I was initially disappointed, but then thought why is this? So I dug into the reasons behind this.

I found out configuration transforms are not a function of msdeploy, they are a function of msbuild. Although not enabled through VS.NET 2010, you can modify non-web based applications to take advantage of configuration transforms. Below are the manual steps to enable configuration transforms in a console application. Other project types should work as well, but I have not tested it.

12:25pm Update: Added missing instruction regarding ProjectConfigFileName

1. Add a new property ProjectConfigFileName that points to your App.Config file
<PropertyGroup>
  <ProjectConfigFileName>App.config</ProjectConfigFileName>
</PropertyGroup>

2. Add a version of App.Config for each configuration, i.e., App.Debug.config To have them nested under App.Config, edit your csproj file,
<None Include="App.Debug.config">
  <DependentUpon>App.config</DependentUpon>
</None>

3. Import Microsoft.Web.Publishing.targets into your csproj file right after the Microsoft.CSharp.targets import.
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.targets" />

4. Call the TransformXml task in your AfterBuild target. Note, the BeforeBuild and AfterBuild targets are commented out by default.
<Target Name="AfterBuild">
    <TransformXml Source="@(AppConfigWithTargetPath)"
                  Transform="$(ProjectConfigTransformFileName)"
                  Destination="@(AppConfigWithTargetPath->'$(OutDir)%(TargetPath)')" />
</Target>

The complete console application csproj file,
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
    <ProductVersion>8.0.30703</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{AAC877C9-6210-4DF8-9BF5-8EB0D902CCAF}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>ConsoleApplication1</RootNamespace>
    <AssemblyName>ConsoleApplication1</AssemblyName>
    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
    <TargetFrameworkProfile>Client</TargetFrameworkProfile>
    <FileAlignment>512</FileAlignment>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
    <PlatformTarget>x86</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
    <PlatformTarget>x86</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup>
    <ProjectConfigFileName>App.config</ProjectConfigFileName>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="App.config" />
    <None Include="App.Debug.config">
      <DependentUpon>App.config</DependentUpon>
    </None>
    <None Include="App.Release.config">
      <DependentUpon>App.config</DependentUpon>
    </None>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.targets" />
  <Target Name="AfterBuild">
    <TransformXml Source="@(AppConfigWithTargetPath)"
                  Transform="$(ProjectConfigTransformFileName)"
                  Destination="@(AppConfigWithTargetPath->'$(OutDir)%(TargetPath)')" />
  </Target>
</Project>

10 comments:

tom said...

Nice implementation!
I tested this, and it's just great.

Only one disadvantage: In "Setup Projects" this does not work.

It appears that the primary output for config-files of a project is still bound to the original app.config.

Can this be overriden? Or is this default behavior of Visual Studio?
Do u have a solution for this?

Phil Bolduc said...

The core of the work is being completed by the msbuild task TransformXml. As this is just a regular msbuild task, it could be called separately. However, I am not an expert with msbuild and I originally had issues on how to build and supply the parameters to the task. I cannot see any reason this task could not be called on arbitrary inputs to produce output you desire. Sorry I cannot give you a silver bullet answer.

alambert said...

This is awesome. Going to try this with Azure configuration files. Thank you!

Chris G. said...

I have had no luck attempting to utilize the information here. To get anywhere I had to rename the .Release and .Debug config files to start with "Web" or it would complain that it couldn't find it. It seems ok with the main changes in the .csproj file but any attempts to put a transform into the Web.Debug.config file shows a dreaded blue underline stating that its not allowed. (the xdt:Transform from below. Yes, I included xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transfrom" in the root node)





When I run the application (Console app) the value of "Rabbit" is still its default from the main App.config. Every other solution I've found online for this problem ends up with a similar problem... Is there some gotcha that I'm unaware of that no one mentioned?

Chris G. said...

Well, apparently this doesn't escape xml...


<appSettings xdt:Transform="Replace">
<add key="Rabbit" value="127.0.0.1" />
</appSettings>

Zidad said...

Hi Chris,

I had the same problem, found the solution by going through the 'targets' file:

You can override the configuration file name as a property (web.config -> app.config).

Add this to your first property group:

<ProjectConfigFileName>App.config</ProjectConfigFileName>

Chirag said...

Really nice post. It is working for us. Keep up the good work.

Peter Geyfman said...
This comment has been removed by the author.
Peter Geyfman said...

I have tried this and no matter what I do, when I build, I still have the main app.config's values in my MYApp.exe.config. What might I be doing wrong? Does main app.config need to not have the values that are being overridden?

Also, both the ProjectConfigFileName property and the TransfromXML target nodes are getting squiggly underlines and the environment is complaining that they are invalid child elements. Can that be my issue? If that is what's the fix?

Thank you in advance for answering.

I have stumbled upon several different posts relating to this technique and tried their ways of implementing this all to no avail. Very frustrating.

Zidad said...

Peter, can you output a message in the AfterBuild to check what the values are?

Something like this:

<Message Text="Source: $(AppConfigWithTargetPath)"/>

<Message Text="Transformname: $(ProjectConfigTransformFileName)"/>


Are you sure you've included this?:
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.targets" />

Can you check your transforms are actually producing the correct result (by checking it out in a web project for example)?