Msbuild: Migrating tasks from old csproj to new csproj format

Created on 24 Nov 2017  路  8Comments  路  Source: dotnet/msbuild

I'm currently in a process of migrating old csprojs to a new format. In some of them I have a msbuild task to replace app.config with app.debug/release.config (depending on build configuration).
In old csproj code to achieve that looks as follows:

<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
  <Target Name="AfterCompile" Condition="Exists('App.$(Configuration).config')">
    <!--Generate transformed app config in the intermediate directory-->
    <TransformXml Source="App.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" />
    <!--Force build process to use the transformed configuration file from now on.-->
    <ItemGroup>
      <AppConfigWithTargetPath Remove="App.config" />
      <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
        <TargetPath>$(TargetFileName).config</TargetPath>
      </AppConfigWithTargetPath>
    </ItemGroup>
  </Target>

Currently I have a piece of code like this which in fact doesn't work, but project compiles with it:

<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile" AfterTargets="Build">
  <TransformXml Source="App.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" />
  <ItemGroup>
    <AppConfigWithTargetPath Remove="App.config" />
    <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
      <TargetPath>$(TargetFileName).config</TargetPath>
    </AppConfigWithTargetPath>
  </ItemGroup>
</Target>

So the question is, is it supported in new csproj format?
If yes, how I could port such task(s)?
If no, would it be supported in future?

OS info:
Visual studio 15.4.4

Most helpful comment

@rainersigwald "May I ask why you did this?" I try everything to make it works :) Finally I remove this element cause it doesn't change nothign. It seems that my problem was an invalid attribute depending when the task should run. If I change to AfterTargets="PrepareForBuild" it works like a charm :). Thank you very much!

So finally to answer my own question, how to migrate msbuild task which previously looks like this:

<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile" Condition="Exists('App.$(Configuration).config')">
  <!--Generate transformed app config in the intermediate directory-->
  <TransformXml Source="App.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" />
  <!--Force build process to use the transformed configuration file from now on.-->
  <ItemGroup>
    <AppConfigWithTargetPath Remove="App.config" />
    <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
      <TargetPath>$(TargetFileName).config</TargetPath>
    </AppConfigWithTargetPath>
  </ItemGroup>
</Target>

You have to change Target to be suitable with new approach (in this situation AfterTargets="PrepareForBuild") so the new part of csproj should looks like this:

<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="ApplyConfigurationConfigFile" AfterTargets="PrepareForBuild" Condition="Exists('App.$(Configuration).config')">
  <ItemGroup>
    <AppConfigWithTargetPath Remove="App.config" />
    <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
      <TargetPath>$(TargetFileName).config</TargetPath>
    </AppConfigWithTargetPath>
  </ItemGroup>
  <TransformXml Source="App.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" />
</Target>

@rainersigwald one more time many thanks for help, and wish you a Merry Christmas :)

All 8 comments

@MNie How are you including this target in the project?

I suspect this is an issue where you're trying to override a target, but defining it _before_ the target you want to override. This has always been possible but is exacerbated by Sdk-style automatic import of .targets at the _very end_ of the file.

If that's the case, there are more details at https://github.com/Microsoft/msbuild/issues/1680, including other suggestions. I suggest going to a custom target name, maybe something like ApplyConfigurationConfigFile, with the appropriate BeforeTargets to hook it into the build process.

Hi @rainersigwald
I try to rename target and right now fragment of my csproj file which should replace config files looks like this:

<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.targets" />
<Message Text="Inside BeforeBuild" Importance="high" />
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="ApplyConfigurationConfigFile" BeforeTargets="Build">
  <ItemGroup>
    <AppConfigWithTargetPath Remove="App.config" />
    <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
      <TargetPath>$(TargetFileName).config</TargetPath>
    </AppConfigWithTargetPath>
  </ItemGroup>
  <Message Text="Inside BeforeBuild" Importance="high" />
  <TransformXml Source="App.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" />
  <Message Text="Inside BeforeBuild" Importance="high" />
</Target>

In property group I add following element:

<BuildDependsOn>BeforeBuild</BuildDependsOn>

The result is that I see an information in output window:
image

But task doesn't replace values in app.config file..

@MNie BeforeTargets="Build" is probably too late (Build is actually, confusingly, one of the very _last_ targets to execute in most projects). Can you instead do AfterTargets="PrepareForBuild"? That is what assigns AppConfigWithTargetPath, so it seems reasonable to me to tweak it immediately afterward.

In property group I add following element:

<BuildDependsOn>BeforeBuild</BuildDependsOn>

This should already be the case. May I ask why you did this?

@rainersigwald "May I ask why you did this?" I try everything to make it works :) Finally I remove this element cause it doesn't change nothign. It seems that my problem was an invalid attribute depending when the task should run. If I change to AfterTargets="PrepareForBuild" it works like a charm :). Thank you very much!

So finally to answer my own question, how to migrate msbuild task which previously looks like this:

<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile" Condition="Exists('App.$(Configuration).config')">
  <!--Generate transformed app config in the intermediate directory-->
  <TransformXml Source="App.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" />
  <!--Force build process to use the transformed configuration file from now on.-->
  <ItemGroup>
    <AppConfigWithTargetPath Remove="App.config" />
    <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
      <TargetPath>$(TargetFileName).config</TargetPath>
    </AppConfigWithTargetPath>
  </ItemGroup>
</Target>

You have to change Target to be suitable with new approach (in this situation AfterTargets="PrepareForBuild") so the new part of csproj should looks like this:

<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="ApplyConfigurationConfigFile" AfterTargets="PrepareForBuild" Condition="Exists('App.$(Configuration).config')">
  <ItemGroup>
    <AppConfigWithTargetPath Remove="App.config" />
    <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
      <TargetPath>$(TargetFileName).config</TargetPath>
    </AppConfigWithTargetPath>
  </ItemGroup>
  <TransformXml Source="App.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" />
</Target>

@rainersigwald one more time many thanks for help, and wish you a Merry Christmas :)

Glad that worked! One thing to note if you are doing the adoption gradually is that the new hooks should work fine with the old projects, too, so you can switch this independently of moving to Sdk-style imports.

For anyone who stumbles upon this like I have: looks like the transform files are not taken into account by the FastUpToDate check (at least when they have Build Action = None, like they should). This causes a problem, because when the only file that is changed is e.g. App.Debug.config, the project is not rebuilt during subsequent builds.

This can be fixed by adding this to the csproj:

<ItemGroup Condition="Exists('App.$(Configuration).config')">
  <CustomAdditionalCompileInputs Include="App.$(Configuration).config" />
</ItemGroup>

Both this and the target from https://github.com/Microsoft/msbuild/issues/2746#issuecomment-353531239 can even be put in Directory.Build.targets and they automatically apply to all projects.

(thanks goes to https://github.com/dotnet/project-system/issues/4100#issuecomment-428899648 for inspiration)

I was not able to get CustomAdditionalCompileInputs to work. UpToDateCheckBuilt works fantastically, however. I was also able to use the TransformXml task from Microsoft.NET.Sdk.Publish (just add it to your project's SDK list, then there is no need to use UsingTask).

<Project Sdk="Microsoft.NET.Sdk.Worker;Microsoft.NET.Sdk.Publish">
  ...

  <Target Name="ApplyConfigurationConfigFile" AfterTargets="PrepareForBuild" Condition="Exists('App.$(Configuration).config')">
    <ItemGroup>
      <AppConfigWithTargetPath Remove="App.config" />
      <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config" TargetPath="$(TargetFileName).config" />
      <UpToDateCheckBuilt Include="$(IntermediateOutputPath)$(TargetFileName).config" Original="App.config" />
      <UpToDateCheckBuilt Include="$(IntermediateOutputPath)$(TargetFileName).config" Original="App.$(Configuration).config" />
    </ItemGroup>
    <TransformXml Source="App.config" Transform="App.$(Configuration).config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" />
  </Target>

  <ItemGroup>
    <Content Remove="App.config" />
    <Content Remove="App.*.config" />
    <None Include="App.config" />
    <None Include="App.*.config" />
  </ItemGroup>

  ...
</Project>

A few years later, it's now even easier to perform App.config transforms thanks to the Microsoft.VisualStudio.SlowCheetah NuGet package. Here's how to modify your csproj to enable transformations: add the Microsoft.VisualStudio.SlowCheetah pacakge and update your App.config original and configuration-specific files metadata with TransformOnBuild and repectively IsTransformFile.

<ItemGroup>
  <PackageReference Include="Microsoft.VisualStudio.SlowCheetah" Version="3.2.26" PrivateAssets="all" />
  <None Update="App.config">
    <TransformOnBuild>true</TransformOnBuild>
  </None>
  <None Update="App.*.config">
    <DependentUpon>App.config</DependentUpon>
    <IsTransformFile>true</IsTransformFile>
  </None>
</ItemGroup>
Was this page helpful?
0 / 5 - 0 ratings