Introducing source generators
This online-only section is about source generators.
Source generators were introduced with C# 9 and .NET 5. They allow a programmer to get a compilation object that represents all the code being compiled, dynamically generate additional code files, and compile those too.
Source generators are like code analyzers that can add more code to the compilation process.
- Example source generator from Microsoft
- What will our source generator do?
- Creating a console app that outputs culture-specific variables
- Creating a source generator class library
- Using the source generator
- Doing more with source generators
An example source generator from Microsoft is the System.Text.Json.SourceGeneration
source generator. The classic method for serializing JSON uses reflection at runtime to dynamically analyze an object model, but this is slow and cannot use native ahead-of-time (AOT) compilation. The better method uses source generators to create source code that is then compiled to give improved performance.
You can read more about the
System.Text.Json.SourceGeneration
source generator at the following link: https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/
We will create a source generator that programmatically creates a code file that adds a ConfigureConsole
method to the Program
class, as shown in the following code:
// Source-generated class.
#nullable enable
using System.Globalization; // To use CultureInfo.
partial class Program
{
private static CultureInfo? _computerCulture = null;
public static void ConfigureConsole(
string culture = "en-US",
bool useComputerCulture = false,
bool showCulture = true)
{
// Store the original computer culture so we can reset it later.
if (_computerCulture is null)
{
_computerCulture = CultureInfo.CurrentCulture;
}
// Enable special characters like Euro currency symbol.
OutputEncoding = System.Text.Encoding.UTF8;
if (useComputerCulture)
{
CultureInfo.CurrentCulture = _computerCulture;
}
else
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture);
}
if (showCulture)
{
WriteLine($"Current culture: {CultureInfo.CurrentCulture.DisplayName}.");
}
}
}
This method can then be called in the Program.cs
file of the project that uses this source generator to control the culture, as shown in the following code:
ConfigureConsole(); // Sets current culture to US English by default.
ConfigureConsole(useComputerCulture: true); // Sets current culture to the local computer's culture.
ConfigureConsole(culture: "fr-FR"); // Sets current culture to French in France.
Let's see how to do this.
First, we will create a console app that outputs culture-specific variables so we can see the problem:
- Use your preferred code editor to add a new Console App /
console
project namedConsoleApp.SourceGeneration
to theChapter07
solution. - In
ConsoleApp.SourceGeneration.csproj
, statically and globally import theConsole
class, as shown in the following markup:
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
- In
Program.cs
, delete the existing statements and then declare some variables whose formatting is affected by culture and output their values, as shown in the following code:
decimal price = 19.99M;
DateTimeOffset today = DateTimeOffset.Now;
WriteLine($"Today, {today:D}, the price is {price:C}.");
- Run the
ConsoleApp.SourceGeneration
project and note the result, as shown in the following output:
Today, 07 July 2023, the price is £19.99.
My computer is configured to use English (British) culture which is why the currency symbol is British pounds £. When you run this console app on your computer, the date format and currency will match your local culture. This can be a problem with a book where you want the output in the book to match the output on the readers screen. But we also want to see how it would look in our own culture or a specified culture. And, by default, some command prompts and terminals do not show special symbols like the Euro currency symbol. We want to see those too.
Next, we will create a class library that implements the source generator:
- Use your preferred code editor to add a new Class Library /
classlib
project namedPackt.SourceGeneration
that targets .NET Standard 2.0 to theChapter07
solution.
Currently, source generators must target .NET Standard 2.0. The default C# version used for class libraries that target .NET Standard 2.0 is C# 7.3, as shown at the following link: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/configure-language-version#defaults.
- In the project file, set the C# language version to
10
or later (so that we can useglobal using
statements), enable enforce extended analyzer rules, statically and globally import theConsole
class, and add the NuGet packagesMicrosoft.CodeAnalysis.Analyzers
andMicrosoft.CodeAnalysis.CSharp
, as shown in the following markup:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<!--Must use C# 10 or later to support 'global using' statements.-->
<LangVersion>10</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
<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.6.0" />
</ItemGroup>
</Project>
- Build the
Packt.SourceGeneration
project to restore packages. - Rename
Class1.cs
toProgramSourceGenerator.cs
. - In
ProgramSourceGenerator.cs
, define a class that implementsISourceGenerator
and is decorated with the[Generator]
attribute, as shown in the following code:
// To use [Generator], ISourceGenerator, and so on.
using Microsoft.CodeAnalysis;
namespace Packt.Shared;
[Generator]
public class ProgramSourceGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext execContext)
{
IMethodSymbol? mainMethod = execContext.Compilation
.GetEntryPoint(execContext.CancellationToken);
string sourceCode = $@"// Source-generated class.
#nullable enable
using System.Globalization; // To use CultureInfo.
partial class {mainMethod?.ContainingType.Name}
{{
private static CultureInfo? _computerCulture = null;
public static void ConfigureConsole(
string culture = ""en-US"",
bool useComputerCulture = false,
bool showCulture = true)
{{
// Store the original computer culture so we can reset it later.
if (_computerCulture is null)
{{
_computerCulture = CultureInfo.CurrentCulture;
}}
// Enable special characters like Euro currency symbol.
OutputEncoding = System.Text.Encoding.UTF8;
if (useComputerCulture)
{{
CultureInfo.CurrentCulture = _computerCulture;
}}
else
{{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture);
}}
if (showCulture)
{{
WriteLine($""Current culture: {{CultureInfo.CurrentCulture.DisplayName}}."");
}}
}}
}}
";
string fileName = $"{mainMethod?.ContainingType.Name}.Methods.g.cs";
execContext.AddSource(fileName, sourceCode);
}
public void Initialize(GeneratorInitializationContext initContext)
{
// This source generator does not need any initialization.
}
}
Good Practice: Include
.g.
or.generated.
in the filename of source generated files. The source code should explicitly enable nullability checks in the file since we do not know if the containing project will enable nullability at the project level or not.
Finally, we will use the source generator in our console app:
- In
ConsoleApp.SourceGeneration.csproj
, in the<PropertyGroup>
, add an entry to enable the generation of the code file, as shown in the following markup:
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
Visual Studio 2022 automatically runs code analyzers so this is not necessary. Visual Studio Code does not automatically run code analyzers. We must add this extra entry in the project file to enable the automatic generation of the source generator file. It is fine to enable it for Visual Studio 2022 too.
You can control the path for automatically generated code files by adding a
<CompilerGeneratedFilesOutputPath>
element.
- In
ConsoleApp.SourceGeneration.csproj
, add a reference to the class library project, as shown in the following markup:
<ItemGroup>
<ProjectReference Include=
"..\Packt.SourceGeneration\Packt.SourceGeneration.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
- Build the
ConsoleApp.SourceGeneration
project. - Note the auto-generated class file:
- In Visual Studio 2022, close and reopen the solution to trigger analyzers to be run, and then in Solution Explorer, expand the Dependencies | Analyzers | Packt.SourceGeneration | Packt.Shared.ProgramSourceGenerator nodes, and then open the
Program.Methods.g.cs
file, as shown in Figure 7B.1:
Figure 7B.1: The source generated Program.Methods.g.cs file
Good Practice: Microsoft's naming convention for proejcts that implement source generators seems to be to suffix with either
.SourceGeneration
or.Generator
. That is why I told you to usePackt.SourceGeneration
for the class library project name.
- In Visual Studio Code, in EXPLORER | CHAPTER07, in the
obj/Debug/net8.0
folder, note thegenerated
folder and its subfolderPackt.SourceGeneration/Packt.Shared.ProgramSourceGenerator
folder, and then open theProgram.Methods.g.cs
file, as shown in Figure 7B.2:
Figure 7B.2: The source generated Program.Methods.g.cs file
- In
Program.cs
, at the top of the file, call the method, as shown in the following code:
ConfigureConsole();
- Run the
ConsoleApp.SourceGeneration
project, and note the result, as shown in the following output:
Current culture: English (United States).
Today, Friday, July 7, 2023, the price is $19.99.
- Change the call to use a specific culture like French in France, as shown in the following code:
ConfigureConsole(culture: "fr-FR");
- Run the
ConsoleApp.SourceGeneration
project, and note the result, as shown in the following output:
Current culture: French (France).
Today, vendredi 7 juillet 2023, the price is 19,99 €.
- Change the call to use a specific culture, as shown in the following code:
ConfigureConsole(useComputerCulture: true);
- Run the
ConsoleApp.SourceGeneration
project, and note the result, as shown in the following output:
Current culture: English (United Kingdom).
Today, 07 July 2023, the price is £19.99.
Source generators are a massive topic. To learn more, use the following links:
- Source Generators design specification: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.md
- Source Generators samples: https://github.com/dotnet/roslyn-sdk/tree/main/samples/CSharp/SourceGenerators
- Source Generators cookbook: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md