Custom MSBuild Tasks (Or, How To Update Your Content On Build)

20 October 2014

If you've ever wanted to dynamically replace some kind of content in your Visual Studio projects on compilation / build, a powerful (and not too difficult) way of doing it is creating your own MSBuild Task. In the past, this meant writing a custom Task with all its associated code, but with .NET 4.0 or later, you can actually make your own Tasks inline, right in your .csproj file. Straight into some sample code!

<UsingTask TaskName="MyTask" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
  <InputFilename ParameterType="System.String" Required="true" />
  <OutputFilename ParameterType="System.String" Required="true" />
  <MatchExpression ParameterType="System.String" Required="true" />
  <ReplacementString ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
  <Reference Include="System.Core" />
  <Using Namespace="System" />
  <Using Namespace="System.IO" />
  <Using Namespace="System.Text.RegularExpressions" />
  <Code Type="Fragment" Language="cs">
    <![CDATA[
        File.WriteAllText(
            OutputFilename,
            Regex.Replace(File.ReadAllText(InputFilename), MatchExpression, ReplacementString)
            );
      ]]>
  </Code>
</Task>
</UsingTask>

This task defines a very simple process - it's pretty self documenting, you can see that we can set up parameters, include various references and run some code. Above, we load in the contents of a file, replace whatever content we need to with a Regex, and write it back out.

Edit your .csproj (if you want to edit it within Visual Studio, you'll have to unload your project first - I find it easier to just edit it in Notepad++ or some other text editor instead of unloading and reloading constantly) and place your <UsingTask> somewhere within the <Project> tags. Now that you've defined your task, all you need to do is call it, with your own <Target> element. If you look at the bottom of the file, you can see that they've already got a Target=AfterBuild commented out and ready for you to use, so you can go ahead and co-opt that. For example:

<Target Name="AfterBuild">
<ReplaceFileText
  InputFilename="$(OutputPath)/../about.html"
  OutputFilename="$(OutputPath)/../about.html"
  MatchExpression="###version###"
  ReplacementString="Version 1.0.5" />   
</Target>

If you save and reload your project, you'll notice that any time you execute a build, the about.html file in the root of your project directory will be parsed and have the text "###version###" replaced with "Version 1.0.5".

With a little bit more work, you can extend this concept to automatically read out an Assembly version and continually keep that updated on whatever file you desire. (If you're looking to do this, I recommend using AssemblyName.GetAssemblyName() instead of Assembly.Load() as you'll run into problems with the .dll being locked on repeat builds. Also, the build OutputPath may or may not be the same in an Azure Continuous Integration build environment, such as when using automatic deployments in Visual Studio Online.)

Inline Build Tasks are a very powerful way of having dynamically generated content that's not dependent on executing deployed code. Now if only you could pass MSBuild parameters to your projects within the Visual Studio hosted MSBuild instance...

Tags: MSBuild, Visual Studio, .NET 4.0

Add a Comment

No Comments