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.