Build Time Code Generation in MSBuild

Build-time code generation is a really powerful way to automate repetitive parts of your code. It can save time, reduce frustration, and eliminate a source of copy/paste bugs.

This is something I’m familiar with due to my past work on MonoDevelop’s tooling for ASP.NET, T4 and Moonlight, and designing and/or implementing similar systems for Xamarin.iOS and Xamarin.Android. However, I haven’t seen any good documentation on it, so I decided to write an article to outline the basics.

This isn’t just something for custom project types, it’s also something that you can include in NuGets, since they can include MSBuild logic.

Background

The basic idea is to generate C# code from other files in the project, and include it in the build. This can be to generate helpers, for example CodeBehind for views (ASPX, XAML), or to process simple DSLs (T4), or any other purpose you can imagine.

MSBuild makes this pretty easy. You can simply hook a custom target before the Compile target, and have it emit a Compile item based on whatever input items you want. For the purposes of this guide I’m going to assume you’re comfortable with enough MSBuild to understand that - if you’re not, the MSDN docs are pretty good for the basics.

The challenge is to include the generated C# in code completion, and update it automatically.

An IDE plugin can do this fairly easily - see for example the Generator mechanism used by T4, and the *.designer.cs file generated by the old Windows Forms and ASP.NET designers. However, doing it this way has several downsides, for example you have to check their output into source control, and they won’t update if you edit files outside the IDE. Build-time generation, as used for XAML, is a better option in most cases.

This article describes how to implement the same model used by WPF/Silverlight/Xamarin.Forms XAML.

Generating the Code

First, you need a build target that updates the generated files, emits them into the intermediate output directory, and injects them to the Compile ItemGroup. For the purposes of this article I’ll call it UpdateGeneratedFiles and assume that it’s processing ResourceFile items and emitting a file called GeneratedCode.g.cs. In a real implementation, you should use unique names won’t conflict with other targets, items and files.

For example:

<Target Name="UpdateGeneratedFiles"
  DependsOnTargets="_UpdateGeneratedFiles"
  Condition=="'@(ResourceFile)' != ''"
>
  <ItemGroup>
    <Compile Include="$(IntermediateOutputPath)GeneratedFile.g.cs" />
    <!-- see https://mhut.ch/journal/2016/04/19/msbuild_code_generation_vs2015
    <FileWrites Include="$(IntermediateOutputPath)GeneratedFile.g.cs" />
    -->
  </ItemGroup>
</Target>
<Target Name="_UpdateGeneratedFiles"
  Inputs="$(MSBuildProjectFile);@(ResourceFile)"
  Outputs="$(IntermediateOutputPath)GeneratedFile.g.cs"
>
  <FileGenerationTask
      Inputs="@(ResourceFile)"
      Output="$(IntermediateOutputPath)GeneratedFile.g.cs"
  >
</Target>

A quick breakdown:

The UpdateGeneratedFiles target runs if you have any ResourceFile items. It injects the generated file into the build as a Compile item, and also injects a FileWrites item so the file is recorded for incremental clean. It depends on the ‘real’ generation target, _UpdateGeneratedFiles, so that the file is generated before the UpdateGeneratedFiles target runs.

The _UpdateGeneratedFiles target has Inputs and Outputs set, so that it is incremental. The target will be skipped if the output file exists is newer than all of the input files - the project file and the resource files.

The project file is included in the inputs list because its write time will change if the list of resource files changes.

The _UpdateGeneratedFiles target simply runs a tasks that generates the output file from the input files.

Note that the generated file has the suffix .g.cs. This is the convention for built-time generated files. The .designer.cs suffix is used for user-visible files generated at design-time by the designer.

Hooking into the Build

The UpdateGeneratedFiles target is added to the dependencies of the CoreCompile target by prepending it to the CoreCompileDependsOn property.

<PropertyGroup>
  <CoreCompileDependsOn>UpdateGeneratedFiles;$(CoreCompileDependsOn)</CoreCompileDependsOn>
</PropertyGroup>

This means that whenever the the project is compiled, the generated file is generated or updated if necessary, and the injected Compile item is injected before the compiler is called, so is passed to the compiler - though it never exists in the project file itself.

Live Update on Project Change

So how do the types from the generated file show up in code completion before the project has been compiled? This takes advantage of the way that Visual Studio initializes its in-process compiler that’s used for code completion.

When the project is loaded in Visual Studio, or when the project file is changed, Visual Studio runs the CoreCompile target. It intercepts the call to the compiler via a host hook in the the MSBuild Csc task and uses the file list and arguments to initialize the in-process compiler.

Because UpdateGeneratedFiles is a dependency of CoreCompile, this means that the generated file is updated before the code completion system is initialized, and the injected file is passed to the code completion system.

Note that the UpdateGeneratedFiles target has to be fast, or it will add latency to code completion availability when first loading the project or after cleaning it.

Live Update on File Change

So, the generated code is updated whenever the project changes. But what happens when the contents of the ResourceFile files that it depends on change?

This is handled via Generator metadata on each of the ResourceFile files:

<ItemGroup>
  <ResourceFile Include="Foo.png">
    <Generator>MSBuild:UpdateGeneratedFiles</Generator>
  </ResourceFile>
</ItemGroup>

This takes advantage of another Visual Studio feature. Whenever the file is saved, VS runs the UpdateGeneratedFiles target. The code completion system detects the change to the generated file and reparses it.

This metadata has to be applied to the items by the IDE (or the user). It may be possible for the build targets to apply it automatically using an ItemDefinitionGroup but I haven’t tested whether VS respects this for Generator metadata.

Xamarin Studio/MonoDevelop

But we have another problem. What about Xamarin Studio/MonoDevelop?

Although Xamarin Studio respects Generator metadata, it doesn’t have an in-process compiler. It doesn’t run CoreCompile, nor does it intercept the Csc file list, so its code completion system won’t see the generated file at all.

The solution - for now - is to add explicit support in a Xamarin Studio addin to run the UpdateGeneratedFiles target on project load and when the resource files change, parse the generated file and inject it into the type system directly.

Migration

Migrating automatically from a designer-generation system to a build-generation system has a few implications.

You either have to force migration of the project to the new system via an IDE, or handle the old system and make the migration optional - e.g. toggled by the presence of the old files. You have to update the project templates and samples, and you have to build a migration system that removes the designer files from the project and adds Generator metadata to existing files.