Source generators are a powerful feature introduced to C#, allowing developers to generate additional code during the compilation process automatically. They can help reduce boilerplate, improve performance, and simplify your codebase.
This blog post will introduce source generators, discuss how they work, and walk through an example of a source generator for generating build information.
What do we want to do here?
Simply having something like this:
var buildAt = BuildInformation.BuildAt; // In UTC when the Build was generated
var configuration = BuildInformation.Configuration; // Release or Debug
var platform = BuildInformation.Platform; // Something like AnyCPU or arm64
What are Source Generators?
Source generators are a compile-time feature in C# that enables you to generate additional C# source code to be included in the compilation process. They are essentially custom Roslyn analyzers that can produce new code files, which are then compiled together with the rest of your project. Source generators can help you automate code generation for repetitive tasks, enforce coding standards, and even optimize your code at compile time.
How to Use Source Generators?
First, we have to create a new class library project that targets netstandard2.0
. It has to target netstandard2.0
and not something else. The reason is here:
because the compiler must run on both the old .NET Framework and the new .NET all generators must target .NET Standard 2.0
Afterward add the following packages:
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" />
</ItemGroup>
With these two packages, we can generate the generator. PrivateAssets
means that the package is more or less a developer dependency and will not be shipped with the linked assembly. And that makes sense. The generator will create a new C# file that will be part of the linked assembly and not the generator itself, so why ship the generator?
Now we can implement the generator like this:
[Generator]
public sealed class IncrementalBuildInformationGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
We need the GeneratorAttribute
and implement the IIncrementalGenerator
interface. Now we can implement the generator that creates the source code that will enable us to get the build information:
using Microsoft.CodeAnalysis;
[Generator]
public sealed class IncrementalBuildInformationGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var compilerOptions = context.CompilationProvider.Select((s, _) => s.Options);
context.RegisterSourceOutput(compilerOptions, static (productionContext, options) =>
{
var buildInformation = new BuildInformationInfo
{
BuildAt = DateTime.UtcNow.ToString("O"),
Platform = options.Platform.ToString(),
WarningLevel = options.WarningLevel,
Configuration = options.OptimizationLevel.ToString(),
};
productionContext.AddSource("LinkDotNet.BuildInformation.g", GenerateBuildInformationClass(buildInformation));
});
}
private static string GenerateBuildInformationClass(BuildInformationInfo buildInformation)
{
return $@"
using System;
using System.Globalization;
public static class BuildInformation
{{
/// <summary>
/// Returns the build date (UTC).
/// </summary>
/// <remarks>Value is: {buildInformation.BuildAt}</remarks>
public static readonly DateTime BuildAt = DateTime.ParseExact(""{buildInformation.BuildAt}"", ""O"", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
/// <summary>
/// Returns the platform.
/// </summary>
/// <remarks>Value is: {buildInformation.Platform}</remarks>
public const string Platform = ""{buildInformation.Platform}"";
/// <summary>
/// Returns the warning level.
/// </summary>
/// <remarks>Value is: {buildInformation.WarningLevel}</remarks>
public const int WarningLevel = {buildInformation.WarningLevel};
/// <summary>
/// Returns the configuration.
/// </summary>
/// <remarks>Value is: {buildInformation.Configuration}</remarks>
public const string Configuration = ""{buildInformation.Configuration}"";
}}
";
}
private sealed class BuildInformationInfo
{
public string BuildAt { get; set; } = string.Empty;
public string Platform { get; set; } = string.Empty;
public int WarningLevel { get; set; }
public string Configuration { get; set; } = string.Empty;
}
}
The provided code is an example of an incremental source generator that generates a static BuildInformation
class containing build-related information, such as build date, platform, warning level, and configuration. Let's break it down step by step:
- The
IncrementalBuildInformationGenerator
class is marked with the[Generator]
attribute and implements theIIncrementalGenerator
interface. This informs the compiler that this class should be treated as an incremental source generator. - The
Initialize
method is responsible for setting up the generator's dependencies and registering the output. In this example, the generator depends on the compiler options. Thecontext.CompilationProvider
is used to obtain the compilation, and theSelect
method is used to extract theOptions
property from it. - The
context.RegisterSourceOutput
method is called to register the generator's output based on its dependencies (thecompilerOptions
). It accepts a delegate with two parameters: theproductionContext
, which allows you to add generated source code to the compilation, and theoptions
, which contains the compiler options. - The
productionContext.AddSource
method is called, passing a unique name for the generated file ("LinkDotNet.BuildInformation.g") and the generated source code. This adds the generatedBuildInformation
class to the compilation.
Usage
Now let's create a new console project to use the generator. To include the generator, you can do this:
<ItemGroup>
<ProjectReference Include="..\LinkDotNet.BuildInformation\LinkDotNet.BuildInformation.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
As we can see, the generator is considered an analyzer and we don't reference the generator in our output - as we described a bit above! Depending on your IDE, you can directly use our class or hit the "Build" button:
Console.WriteLine($"Build at: {BuildInformation.BuildAt}");
Console.WriteLine($"Platform: {BuildInformation.Platform}");
Console.WriteLine($"Warning level: {BuildInformation.WarningLevel}");
Console.WriteLine($"Configuration: {BuildInformation.Configuration}");
Which can lead to the following output:
Build at: 2023-03-23T12:34:56.7890123Z
Platform: AnyCPU
Warning level: 4
Configuration: Debug
Pretty easy. Of course, this was an easy entrance to the generators, but it can get complicated very quickly, especially if you have to handle the whole compilation object.
Conclusion
Source generators in C# provide an innovative way to automate code generation and improve your codebase. They can be used to simplify repetitive tasks, enforce coding standards, and optimize your code at compile time. The IncrementalBuildInformationGenerator example demonstrates how to create a source generator that generates build information, making it easy to track and access important details about your project's build configuration.
Resources
- If you want to use the
BuildInformation
in your project, you can use the NuGet: https://www.nuget.org/packages/LinkDotNet.BuildInformation - Source code to this blog post: here
- All my sample code is hosted in this repository: here