Saturday, 20 March 2021

Building .NET code generators

.NET 5 is finally live and there are a number of amazing new things that came out with it. You can read the announcement post here: https://devblogs.microsoft.com/dotnet/announcing-net-5-0/

But one of the things that I've been waiting for a very long time and was most excited by was the GA release of code generators! I've been waiting for something like this for years, and was super excited about the early announcements and releases, but didn't really get a chance to play with them much. Plus, the early releases were a bit tricky to use, and the tooling was very much lacking, so I didn't apply them in any projects until now

The general principle is simple - the code generators are somewhat similar to analyzers in behavior, they will run as part of the code analysis / build, but the main difference is that you get the ability to add new source files to your compilation on the fly. There are some caveats though, some of them being:

  • The process is additive only - you can't modify or delete existing code using code generators (at least for the moment), you can only add new code
  • All code generators will run in parallel with each other, and can't inspect each other's generated output. This means you can't have code generators analyze code that was created by a different generator

This may change in the future, as source generators evolve and improve.

So let's see what it takes to generate some code!

There are a number of things where code generators could help out a project. One of the most obvious ones is to remove what I call "ceremony" code - in some cases you end up following a lot of ceremony to perform some simple tasks, and the code you write may have multiple blocks that are be "almost" identical. Wouldn't it be great if you could automate that?

Scenario 1: INotifyPropertyChanged

A great example of that is the INotifyPropertyChanged interface, and it's usage in properties!

Here's an example of a property that uses this interface to raise an event when it's property changes:

public class MyModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _myValue;
    public string MyValue
    {
        get => _myValue;
        set
        {
            _myValue = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyValue)));
        }
    }
}

In this example, instead of just using an auto property, we're forced to expand that to a full field backed property, and raise the event in the property setter. One of the issues here is that this code is error prone - when you find yourself copy-pasting this to generate multiple properties, you can forget to replace the property name that's passed into the event, since it's passed as a string (in this example I used the nameof(MyValue) approach, but in many cases this would actually be used like this: new PropertyChangedEventArgs("MyValue"). And this just makes matters worse

If you find yourself doing that often, you'll often eventually write a helper method to simplify your property somewhat. The helper method will save the value to the backing field, and raise the event, passing in the property name in one call. It'll also get the property name automagically, meaning you can be confident you will be raising the events with the correct property name!

private void SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
    storage = value;
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

private string _myNewValue;
public string MyNewValue
{
    get => _myNewValue;
    set => SetProperty(ref _myNewValue, value);
}

The main piece of magic here is the [CallerMemberName] attribute on the method parameter. If you don't pass a value to that parameter, the framework will automatically populate it with the method/property name of the caller code. In this case - the property name.

While a considerably improvement, you have to add the SetProperty call to your model class, or use a base class for your properties. And it's still not quite as great as a simple auto property.

Scenario 2: WPF MVVM commands

I've been working on a WPF MVVM app (using Prism framework), and while wiring commands in the view model, I found another example of ceremony code that I'd love to automate. To create a command, you end up with something like this:

private ICommand _myActionCommand;
public ICommand MyActionCommand => _myActionCommand ??= new DelegateCommand(MyAction);

public void MyAction()
{
	// command code
}

This is actually already quite condensed, thanks to some new C# syntax, but it can get quite repetitive. Once you start adding parameters to your commands, or even CanExecute properties (properties that can control whether the command can be called at this moment), especially when this property uses value change notifications, this can start getting more messy:

private ICommand _myActionCommand;
public ICommand MyActionCommand => _myActionCommand ??= new DelegateCommand<string>(MyAction, () => IsReady)
	.ObservesProperty(() => IsReady);

public void MyAction(string value)
{
	// command code
}

I can also be error prone if you point to the wrong property, or forget to observe property changes. What makes it worse is the fact that these errors won't even cause exceptions - you just end up with strange behaviour, making it much harder to troubleshoot and find out what actually went wrong.

Source generators to the rescue!

So how can source generators help us? Simple - we get them to generate the boring and dull repetitive code for us. Not only we won't have to worry about it, our code becomes more concise, and we remove the human element from it, reducing the risk that we'll cause any issues by mistyping a property name, or forgetting to do something like monitor property changes.

Building a generator is simple. Here are the core (overly simplified) steps:

  • Create a separate project for your source generators. This project has to be completely independent and not reference any of your source code. Remember, it's compiled output has to run at build time for the rest of your code.
  • Reference the following NuGet packages to give you access to source code analysis.
<ItemGroup>
	<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
	<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
</ItemGroup>
  • Create a generator class that inherits from ISourceGenerator and decorate it with the [Generator] attribute
  • Register for syntax notifications in the Initialize call, and implement your new logic in the Execute call!

This is of course overly simplified, but the scope of this article isn't really about teaching to write a code analyzer - it's to show you can you can do with them!

So how could we use source generators to simplify the code in our scenarios? Don't forget that the source generators are additive by nature, so if you have a property in your class, you can't rewrite it's setter. Instead, you could just declare a private field, and have source generators write the property around it!

For example, given this code

[AutoNotify]
private bool _isReady = false;

A source generator could generate a property like this:

private bool IsReady
{
	get
	{
		return this._isReady;
	}
	set
	{
		this._isReady = value;
		this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(IsReady)));
	}
}

The same would go for the commands in Scenario 2. You could just add your command method, and decorate it with an attribute, and this would generate the command property and backing field. So the command in that scenario would become:

[CommandMethod(CanExecuteProperty = "IsReady")]
public void MyAction(string value)
{
	// command code
}

Summary

There are plenty of other cases where source generators could do great things. Since source generators are bundled as analyzers, you could combine them with your own analyzers that could point out duplicated code that could be improved using your own source generators. You could even add code fixes for the analyzers, that would rewrite your code to the shorter condensed version using the source generators! The sky is the limit!

If you are interested in trying it out, I would recommend you read the introduction post for the source generators themselves Introducing C# Source Generators

Also, you can check out this post showing an example of how to implement a source generator that will convert csv files in your project into strongly typed representations of that data: New C# Source Generator Samples

Finally, you can check out the source generator sample projects in the Rosly SDK repository on GitHub: SourceGenerators