History

In early 2020, the first preview from Source Generators was introduced.

Source Generators: a new C# compiler feature that lets C# developers inspect user code and generate new C# source files that can be added to a compilation.

New?

The option to dynamically generate C# code in your project was already possible using the T4 text templating feature.

Example T4

See the example below which generates three public properties in a class using T4 text templating:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>
<# var properties = new string [] {"I1", "I2", "I3"}; #>
// This is generated
public class ExampleClass
{
<# // This code runs in the text template:
  foreach (string property in properties) { #>
    public int <#= property #> { get; set; }
<# } #>
}

The generated C# class looks like this:

1
2
3
4
5
6
7
// This is generated
public class ExampleClass
{
    public int I1 { get; set; }
    public int I2 { get; set; }
    public int I3 { get; set; }
}

Difference

🕒 Timing

The main difference between T4 text templates and the new Source Generators is that a T4 template needs to be executed manually before the source code is compiled. Whereas a source generator runs while the code is being compiled. So the generated output becomes part of the project.

📝 Access to compilation object

With source generators your have access to a compilation object that represents all the code that is being compiled. This object can be analyzed and inspected which allows you to write code that uses the syntax and semantic models from the code which is being compiled, just like it’s done with Roslyn Source Analyzers.

✏️ Note that for both options, external inputs (like a text file or a resource file) must be present before/during compile time, because that’s when the generators run.

Overview Source generators

Source generators run as a phase of compilation visualized below: Overview

Example Source Generator

When building the same ExampleClass using Source Generators, follow these steps:

1️⃣ Create a library

✏️ Note that you need to define your Source Generators in a .NET Standard 2.0 library. However this Source Generator library can be used without any issues in a older full framework like .NET 4.5 or even a older .NET Standard 1.0 project.

Also make sure to add the following NuGet references:

1
2
3
4
5
6
7
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="3.10.0" PrivateAssets="all" />
  </ItemGroup>

2️⃣ Add a new Source Generator

Add a new C# class which defines the Source Generator.

  • The class must implement the ISourceGenerator interface
  • The class must be annotated with the [Generator] attribute used to specify the attached class is a source generator that generates the C# sources.

See example below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Generator]
public class ExampleClassGenerator : ISourceGenerator
{
    public void Initialize(InitializationContext context)
    {
        // Add optionally initialization code 
    }

    public void Execute(SourceGeneratorContext context)
    {
        // The actual implementation is defined here
    }
}

When the same ExampleClass code is rebuild using Source Generators, the source generator class looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[Generator]
public class ExampleClassGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // No initialization required
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var src = new StringBuilder();
        src.AppendLine("namespace ExampleClassCodeGenerator");
        src.AppendLine("{");
        src.AppendLine("    public class ExampleClass");
        src.AppendLine("    {");

        foreach (var property in new [] { "I1", "I2", "I3" })
        {
            src.AppendLine($"        public int {property} {{ get; set; }}");
        }

        src.AppendLine("    }");
        src.AppendLine("}");

        context.AddSource("ExampleClass_Generated", SourceText.From(src.ToString(), Encoding.UTF8));
    }
}

In the example above, the C# code is just created using a StringBuilder. The last statement context.AddSource(...) actually adds the C# source code as SourceText and generates a C# file with the name ExampleClass_Generated.cs.

3️⃣ Use the Source Generator project

Create a simple ConsoleApp project, and reference the ExampleClassCodeGenerator project.

When referencing the project, make sure to add the OutputItemType="Analyzer" and ReferenceOutputAssembly="false" attributes like this:

1
2
3
4
5
6
<ItemGroup>
    <ProjectReference
      Include="..\..\src\ExampleClassCodeGenerator\ExampleClassCodeGenerator.csproj"
      OutputItemType="Analyzer"
      ReferenceOutputAssembly="false" />
</ItemGroup>

When this is correctly defined, you can see that Source Generator project is used in your console app: Analyzer ExampleClass Generated

And when you double-click the ExampleClass_Generated.cs, you can see the actual C# code which is generated. Analyzer ExampleClass Generated C# Code

Now you can use the ExampleClass just like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using ExampleClassCodeGenerator;

namespace ExampleClassConsumer
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var example = new ExampleClass
            {
                I1 = 1,
                I2 = 42,
                I3 = 0
            };
        }
    }
}

✏️ Note that sometimes when adding new functionality to the Source Generator and trying to use that newly generated code, you run into errors in Visual Studio. ExampleClass_VS_Error

The only solution is to restart Visual Studio to make sure that the cache is flushed. So if anyone knows a permanent solution/fix for this, send me a DM via Twitter: @sheyenrath.

4️⃣ Debug

To support debugging, you need to make sure that the Debugger is launched (Debugger.Launch()) when the SourceGenerator is initialized. Surround this statement with a #if DEBUG - #endif to make sure that the debugger is only launched when running in Debug mode.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[Generator]
public class ExampleClassGenerator : ISourceGenerator
{
    public void Initialize(InitializationContext context)
    {
#if DEBUG
        if (!Debugger.IsAttached)
        {
            Debugger.Launch();
        }
#endif
    }

    public void Execute(SourceGeneratorContext context)
    {
        // The actual implementation is defined here
    }
}

For more detail on advanced debugging support, see this blog debug-source-generators-with-vs2019.

5️⃣ Using a Source Generator package in a library

When you use a Source Generator as a package reference in your library project, make sure that you define this NuGet package as a Private Asset (<PrivateAssets>) and define the correct <IncludeAssets>. This is needed to indicate that this dependency is purely used as a development harness and that you don’t want to expose that to projects that will consume your package.

See example from a library which uses the FluentBuilder as a development dependency:

1
2
3
4
5
<PackageReference Include="FluentBuilder" Version="0.0.6">
    <!-- 👇 -->
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

▶️ A real project: AnyOf

To see how easy (or how difficult) is was to build a real project which is a Source Generator, I decided to start a small project.

Requirements

Make a new type in C# which can be used to define two or more types as input for a method, much like this TypeScript functionality:

1
2
3
function Func(value: number | string) {
    // value can be a number or a string, but nothing else
}

Solution

The solution for two different types would be something AnyOf<TFirst, TSecond>, which can be used like:

1
2
3
4
public void Func(AnyOf<int, string> value)
{
    // value can be a int or a string. But nothing else.
}

Building such a type is straightforward, however this would only support two generic types.

Solution (with Source Generation)

So that’s where Source Generators can be used to generate as much as types as you want. In my case I limited it to 10, which resulted into these generated AnyOf-types:

  • AnyOf<TFirst, TSecond>
  • AnyOf<TFirst, TSecond, TThird>
  • AnyOf<TFirst, TSecond, TThird, TFourth>
  • … and so on …

Which makes it possible to use:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void Func3(AnyOf<int, string, bool> value)
{
    // value can be a int, string or bool. But nothing else.
}

public void Func4(AnyOf<int, string, bool, DateTime> value)
{
    // value can be a int, string, bool or DateTime. But nothing else.
}

// and so on

For the implementation see AnyOfCodeGenerator.cs. And if you have any questions or issues, please feel free to create an issue in that AnyOf project.

▶️ A real project: FluentBuilder

Based on an interesting blogpost from Tom Phan : &ldquo;auto-generate-builders-using-source-generator-in-net-5&rdquo; I created a NuGet package which can be used to automatically generate a FluentBuilder for a class or DTO.

This Source Generator is not just creating new FluentBuilders with ‘inline’ code using a StringBuilder (as is done in AnyOf), but it’s actually analyzing the sources using a ISyntaxReceiver to analyse which classes are annotated with the [AutoGenerateBuilder] attribute, which defines that a class is suitable for auto-generating a FluentBuilder.

Usage

Annotate

Annotate a DTO with [FluentBuilder.AutoGenerateBuilder] to indicate that a FluentBuilder should be generated for this class:

1
2
3
4
5
6
7
[FluentBuilder.AutoGenerateBuilder]
public class UserDto
{
    public string FirstName { get; set; }

    public string LastName { get; set; }
}

Using the FluentBuilder

Using the Fluent builder is straightforward, see the excerpt below:

1
2
3
4
var user = new FluentBuilder.UserDtoBuilder()
    .WithFirstName("Test")
    .WithLastName("User")
    .Build();

▶️ A real project: ProxyInterfaceGenerator

While building the FluentBuilder I wanted to be able to mock and unit-test the code which generates the files, but there was a dependency on the GeneratorExecutionContext class which is sealed class does not have an interface and therefore cannot be mocked.

So I decided to investigate if it was possible to build a Source Generator which could generate a Proxy class + Interface from another class.

This was possible and the initial project can be found here.

Usage

Given: an external existing class which does not implement an interface

1
2
3
4
5
6
7
8
9
public sealed class Person
{
    public string Name { get; set; }

    public string HelloWorld(string name)
    {
        return $"Hello {name} !";
    }
}

Create a partial interface

And annotate this with ProxyInterfaceGenerator.Proxy[...] and with the Type which needs to be wrapped / proxied:

1
2
3
4
[ProxyInterfaceGenerator.Proxy(typeof(ProxyInterfaceConsumer.Person))]
public partial interface IPerson
{
}

When the code is compiled, this source generator creates the following

1️⃣ An additional partial interface
Which defines the same properties and methods as in the external class.

1
2
3
4
5
6
public partial interface IPerson
{
    string Name { get; set; }

    string HelloWorld(string name);
}

2️⃣ A Proxy class
Which takes the external class in the constructor and wraps all properties and methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class PersonProxy : IPerson
{
    public Person _Instance { get; }

    public PersonProxy(Person instance)
    {
        _Instance = instance;
    }

    public string Name { get => _Instance.Name; set => _Instance.Name = value; }

    public string HelloWorld(string name)
    {
        string name_ = name;
        var result_19479959 = _Instance.HelloWorld(name_);
        return result_19479959;
    }
}

Use it

1
2
3
IPerson p = new PersonProxy(new Person());
p.Name = "test";
p.HelloWorld("stef");

Conclusion

See list below for some personal takeaways:

➕ This new ‘framework’ to build Source Generators is more powerful when compared to T4 and the order in which the Source Generator runs in the build process is more integrated. Another plus is that you can easily generate multiple source files, which is not possible using T4.

➕ Another thing to keep in mind is that analyzing existing source code files is done using Roslyn, which is released some time ago and had proven itself, so if you are familiar with Roslyn, then writing a Source Generator should be comparable.

➖ I did not yet have the time to investigate if there are any Unit testing frameworks or utilities which make unit testing easy. So for some of my Source Generator projects, I added some minimal unit tests to verify if the generated C# source files were conform the specification.

➖ Quickly hitting F5 to debug a Source Generator is not supported, so you need to use the #if DEBUG - #endif construction to launch the debugger.

📚 References