C# Source Generators: Less Boilerplate Code, More Productivity

One exciting feature of the upcoming .NET 5 are Source Generators.

Source Generators as the name suggests generate C# source code as part of the compilation process. Code generation is not a new concept in Visual Studio and .NET – for example T4 templates have been around for a while now and enable you to programmatically generate/transform content that can be compiled. There are also techniques such as IL Weaving that tools such as Fody use to manipulate the assembly that is produced from the compilation process.

Source Generators essentially enable you to add new code dynamically as part of the build process, for example adding new classes based on the hand written code in the project.

One thing to note is that Source Generators are designed to add additional generated code and not modify the code you have already written.

Source Generators can examine the existing code you have written and make decisions about what new code to generate, they can also access other files to determine what to generate.

When using Source Generators the sequence looks like this: Begin Compilation –> Any Source Generators Being Used? –> Yes –> Analyse Source Code –> Generate New Source Code –> Add Generated Source Code to Compilation –> Compile Hand-Written and Generated Source Into Output Assembly.

Creating a Simple C# Source Generator

Step 1: Creating the Source Generator

The first step is to actually define the Source Generator, this is done by creating a separate project and once it’s created, referencing it in the project you want to add generated source to.

First off you will need Visual Studio Preview and .NET 5 Preview installed.

Once installed, open VS Preview and create a new C# .NET Standard 2.0 Class Library Project project called “CheeseSourceGenerator”.

Once the project is created, you’ll need to modify the project file by double clicking on it. Source Generators are currently in preview so we can expect better tooling support in the final versions. Change the project file to the following:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <LangVersion>preview</LangVersion>
    </PropertyGroup>
    
    <PropertyGroup>
        <RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json ;$(RestoreAdditionalProjectSources)</RestoreAdditionalProjectSources>
    </PropertyGroup>
    
    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.6.0-3.20207.2" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0-beta2.final" PrivateAssets="all" />
    </ItemGroup>
</Project>

Save the project file and build the project to check there are no errors.

The next thing to do is to actually define a Source Generator. To do this add a new class to hold the generator called “Generator” and implement the ISourceGenerator interface and decorate the class with the [Generator] attribute – both of these are from the Microsoft.CodeAnalysis namespace:

using System;
using Microsoft.CodeAnalysis;


namespace CheeseSourceGenerator
{
    [Generator]
    public class Generator : ISourceGenerator
    {
        public void Execute(SourceGeneratorContext context)
        {
            throw new NotImplementedException();
        }

        public void Initialize(InitializationContext context)
        {
            throw new NotImplementedException();
        }
    }
}

The Execute method is where the actual source code generation takes place and the Initialize method allows for some more complex scenarios. In this simple example we’ll just add code to the Generate method and leave the method empty.

In this simple example we’ll just add a new class to the compilation – in this simple example there is no logic involved in the generation:

using System;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace CheeseSourceGenerator
{
    [Generator]
    public class Generator : ISourceGenerator
    {
        public void Execute(SourceGeneratorContext context)
        {
            const string source = @"
namespace GeneratedCheese
{
    public class CheeseChooser
    {
        public string BestCheeseForPasta => ""Parmigiano-Reggiano"";
        public string BestCheeseForBakedPotato => ""Mature Cheddar"";
    }
}
";
            const string desiredFileName = "CheeseChooser.cs";
            
            SourceText sourceText = SourceText.From(source, Encoding.UTF8); // If no encoding specified then SourceText is not debugable

            // Add the "generated" source to the compilation
            context.AddSource(desiredFileName, sourceText);
        }

        public void Initialize(InitializationContext context)
        {
            // Advanced usage
        }
    }
}

Notice in the preceding code that the SourceGeneratorContext passed to the Execute method is the object that allows us to add the source to the compilation.

Build the project. At this point no source code generation has taken place, we’ve just compiled the generator into an assembly.

Step 2: Register the C# Source Generator in a Project

Add a new .NET Core Console project to the solution called “CheeseConsole”.

Once created add a project reference to the CheeseSourceGenerator project. This will allow the console app to generate source code as part of its compilation.

The project file will now look like:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\Cheese\CheeseSourceGenerator.csproj" />
  </ItemGroup>

</Project>

To actually opt-in to the code generation, the CheeseConsole project file needs to modified to add <LangVersion>preview</LangVersion> and change the reference to the generator project to be an analyser reference:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>

  <ItemGroup>
      <ProjectReference Include="..\Cheese\CheeseSourceGenerator.csproj" 
                        OutputItemType="Analyzer"
                        ReferenceOutputAssembly="false"/>
  </ItemGroup>

</Project>

If you build everything now you should see no errors.

Step 3: Use the Generated Code

In the console app Program.cs add a using directive to the namespace that was used in the source code string, namely using GeneratedCheese;

In the Main method we can now create an instance of a CheeseChooser and make use of it. Add the following code and notice that you get Intellisense support when referencing the  BestCheeseForPasta and BestCheeseForBakedPotato properties.

The Program.cs file should look like:

using System;
using GeneratedCheese;

namespace CheeseConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            var cheeseChooser = new CheeseChooser();

            Console.WriteLine($"The best cheese for pasta is: {cheeseChooser.BestCheeseForPasta}");
            Console.WriteLine($"The best cheese for potato is: {cheeseChooser.BestCheeseForBakedPotato}");

            Console.ReadLine();
        }
    }
}

If you run the console app you should see the following:

The best cheese for pasta is: Parmigiano-Reggiano
The best cheese for potato is: Mature Cheddar

This example is very simplistic but there are a  number of other use cases that I’ll cover in future posts such as:

  • Augmenting existing code
  • Auto-implementing boilerplate code (such as INotifyPropertyChanged)
  • Generation from (non C#) external file
  • Generation from database contents
  • Serialization without reflection
  • etc.

SHARE:

Comments (8) -

  • Norm

    5/4/2020 3:25:30 PM | Reply

    Hi, Jason. Thanks for your write up. Would generating an enum from records in a database table be a possible use case for this?

    • Jason Roberts

      5/6/2020 2:58:01 AM | Reply

      Hi Norm, that is certainly technically possible, just do a DB query as part of generation and emit the enum, might slow down compilation a little bit though. It will be interesting to see what real world uses people create Smile

  • Aleksey Troepolskiy

    5/4/2020 7:48:59 PM | Reply

    Awesome! I have been waiting for some language feature from C# as in the Nemerle language - there you can use the language construct to bypass for example all the fields of the class and do something without using reflection, i.e. the loop unfolds at the compilation stage. It will be usefull for serialization, clone, equals  etc.

  • Dan Colasanti

    5/5/2020 7:25:51 PM | Reply

    Please note that in the line that reads "The Generate method is where the actual source code generation takes place and the Initialize method...", I believe the word Generate should be Execute.

    • Jason Roberts

      5/6/2020 2:53:18 AM | Reply

      Thanks Dan! - corrected Smile

  • Victor

    5/12/2020 9:58:40 AM | Reply

    Whats the real use case for this source generator? How will it help my productivity or how will it help my app work better?

    • Jason Roberts

      5/15/2020 3:01:57 AM | Reply

      Hi Victor, I think it will be interesting to see how source generators end up being used, I see them as either a productivity tool, a quality increasing/defect reducing tool, , a performance increasing tool (e.g. eliminating reflection) and a tool to enable new ways of doing things.

  • Daniel Alves

    6/8/2020 5:57:37 PM | Reply

    Hey, Jason!

    I'm playing with the Source Generation since the last weekend and I liked its concept.
    However,  I just realized that the generated code is added during the compilation phase, but when you are working with some IDEs the source code is used as the source for auto-complete and intellisense, not the produced assembly.

    Unless the generated work is marked with some compiler flag and the produced assembly also analyzed by the IDE, I can't see a way of IDEs to complete that for you.

    What do you think?

Pingbacks and trackbacks (1)+

Add comment

Loading