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.
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
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.
Condition=="'@(ResourceFile)' != ''"
<Compile Include="$(IntermediateOutputPath)GeneratedFile.g.cs" />
<!-- see https://mhut.ch/journal/2016/04/19/msbuild_code_generation_vs2015
<FileWrites Include="$(IntermediateOutputPath)GeneratedFile.g.cs" />
A quick breakdown:
UpdateGeneratedFiles target runs if you have any
ResourceFile items. It
injects the generated file into the build as a
Compile item, and also injects
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.
_UpdateGeneratedFiles target has
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.
_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
UpdateGeneratedFiles target is added to the dependencies of the
CoreCompile target by prepending it to the
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.
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
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
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
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
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.
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.