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:

PowerShell
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:

XML
<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:

C#
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:

PowerShell
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.