Part of .NET works with reflection: at run time, code reads the properties of a type, builds a description from them and
sometimes even generates new code before it does the actual work. JsonSerializer without further settings does this on the first call for every type,
configuration binding on every Bind, Regex when a pattern is created. That costs startup time and memory, and it rules out Native AOT (lesson
3.12.5). A source generator does the same work at build time: the compiler calls it, it reads your code and writes additional C# code that
is compiled along with it. At run time the description is already there. This lesson shows the four generators that
help in every web app of the Kladde, how to look at their code, and how to measure what they bring.
New in this lesson:
| Package | Version | what for |
|---|---|---|
Microsoft.Extensions.Configuration.Json, Microsoft.Extensions.Configuration.Binder, Microsoft.Extensions.Logging.Console |
10.0.15 | only for the exercise project; in the Kladde, ASP.NET Core brings them along |
Microsoft.Extensions.Logging.Abstractions |
10.0.15 | in the benchmark project, for the log message generator |
The exercise project
In k3-exercises/3-12, create a console app:
dotnet new console -n Generators -o generators
cd generators
dotnet add package Microsoft.Extensions.Configuration.Json --version 10.0.15
dotnet add package Microsoft.Extensions.Configuration.Binder --version 10.0.15
dotnet add package Microsoft.Extensions.Logging.Console --version 10.0.15
Add three properties to Generators.csproj: warnings as errors as everywhere, the generator for configuration, and the instruction to write the
generated code out as files:
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
Program.cs uses all four generators. The first part measures how long the first JSON answer of a fresh process takes, once with
reflection and once with the generator:
using System.Diagnostics;
using System.Text.Json;
using System.Text.RegularExpressions;
using Generators;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
// "reflection" or "generator": the first JSON answer of a fresh process, measured once.
string mode = args.FirstOrDefault() ?? "generator";
var week = new WeekAnswer(new DateOnly(2025, 3, 3),
[new Slot(new TimeOnly(9, 30), 45, "Mara Feld"), new Slot(new TimeOnly(11, 0), 30, "Tom Wieland")]);
var watch = Stopwatch.StartNew();
string json = mode == "reflection"
? JsonSerializer.Serialize(week, JsonSerializerOptions.Web)
: JsonSerializer.Serialize(week, KladdeJson.Default.WeekAnswer);
Console.WriteLine($"{mode}: first answer after {watch.Elapsed.TotalMilliseconds:0.0} ms");
Console.WriteLine(json);
// Configuration bound by the generator (EnableConfigurationBindingGenerator in the project file).
IConfiguration configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { ["Booking:SlotMinutes"] = "30", ["Booking:DaysAhead"] = "28" })
.Build();
BookingOptions booking = configuration.GetSection("Booking").Get<BookingOptions>()!;
Console.WriteLine($"Appointments of {booking.SlotMinutes} minutes, {booking.DaysAhead} days ahead");
using ILoggerFactory factory = LoggerFactory.Create(b => b.AddSimpleConsole(o => o.SingleLine = true));
ILogger logger = factory.CreateLogger("Kladde.Booking");
Log.AppointmentBooked(logger, 4711, "Mara Feld");
Console.WriteLine(Patterns.PostalCode().IsMatch("10115") + " " + Patterns.PostalCode().IsMatch("1O115"));
namespace Generators
{
public sealed record Slot(TimeOnly Start, int Minutes, string Contact);
public sealed record WeekAnswer(DateOnly Monday, Slot[] Slots);
public sealed class BookingOptions
{
public int SlotMinutes { get; set; }
public int DaysAhead { get; set; }
}
[System.Text.Json.Serialization.JsonSourceGenerationOptions(System.Text.Json.JsonSerializerDefaults.Web)]
[System.Text.Json.Serialization.JsonSerializable(typeof(WeekAnswer))]
public sealed partial class KladdeJson : System.Text.Json.Serialization.JsonSerializerContext;
public static partial class Log
{
[LoggerMessage(EventId = 1001, Level = LogLevel.Information, Message = "Appointment {AppointmentId} booked for {Contact}")]
public static partial void AppointmentBooked(ILogger logger, int appointmentId, string contact);
}
public static partial class Patterns
{
// A German postal code: exactly five digits.
[GeneratedRegex("^[0-9]{5}$")]
public static partial Regex PostalCode();
}
}
Build in Release and start the DLL directly, so that every run is a fresh process without dotnet run in front of it:
dotnet build -c Release
dotnet bin/Release/net10.0/Generators.dll generator
generator: first answer after 38.8 ms
{"monday":"2025-03-03","slots":[{"start":"09:30:00","minutes":45,"contact":"Mara Feld"},{"start":"11:00:00","minutes":30,"contact":"Tom Wieland"}]}
Appointments of 30 minutes, 28 days ahead
info: Kladde.Booking[1001] Appointment 4711 booked for Mara Feld
True False
The console logger writes the log line on a thread of its own; on your machine it may also appear after the last line. The four generators one after the other.