Ein Teil von .NET arbeitet mit Reflexion: Zur Laufzeit liest Code die Eigenschaften eines Typs, baut daraus eine Beschreibung und erzeugt manchmal sogar neuen Code, bevor er die eigentliche Arbeit tut. JsonSerializer ohne weitere Angaben macht das beim ersten Aufruf für jeden Typ, die Konfigurationsbindung bei jedem Bind, Regex beim Erzeugen eines Musters. Das kostet Startzeit, Speicher und schließt Native AOT aus (Lektion 3.12.5). Ein Quellgenerator macht dieselbe Arbeit beim Build: Der Compiler ruft ihn auf, er liest Ihren Code und schreibt zusätzlichen C#-Code, der mitübersetzt wird. Zur Laufzeit ist die Beschreibung dann schon da. Diese Lektion zeigt die vier Generatoren, die in jeder Web-App der Kladde nützen, wie man ihren Code ansieht, und misst, was sie bringen.

Neu in dieser Lektion:

Paket Version wofür
Microsoft.Extensions.Configuration.Json, Microsoft.Extensions.Configuration.Binder, Microsoft.Extensions.Logging.Console 10.0.15 nur für das Übungsprojekt; in der Kladde bringt ASP.NET Core sie mit
Microsoft.Extensions.Logging.Abstractions 10.0.15 im Benchmark-Projekt, für den Generator der Logmeldungen

Das Übungsprojekt

Legen Sie in k3-uebungen/3-12 eine Konsolen-App an:

PowerShell
dotnet new console -n Generatoren -o generatoren
cd generatoren
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

In Generatoren.csproj kommen drei Eigenschaften dazu: Warnungen als Fehler wie überall, der Generator für die Konfiguration, und die Anweisung, den erzeugten Code als Dateien abzulegen:

XML
<PropertyGroup>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Program.cs benutzt alle vier Generatoren. Der erste Teil misst, wie lange die erste JSON-Antwort eines frischen Prozesses dauert, einmal mit Reflexion und einmal mit dem Generator:

C#
using System.Diagnostics;
using System.Text.Json;
using System.Text.RegularExpressions;
using Generatoren;
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}: erste Antwort nach {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($"Termine zu {booking.SlotMinutes} Minuten, {booking.DaysAhead} Tage im Voraus");

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 Generatoren
{
    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 = "Termin {AppointmentId} gebucht für {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();
    }
}

Bauen Sie in Release und starten Sie die DLL direkt, damit jeder Lauf ein frischer Prozess ohne dotnet run davor ist:

PowerShell
dotnet build -c Release
dotnet bin/Release/net10.0/Generatoren.dll generator
generator: erste Antwort nach 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"}]}
Termine zu 30 Minuten, 28 Tage im Voraus
info: Kladde.Booking[1001] Termin 4711 gebucht für Mara Feld
True False

Die Logzeile schreibt der Konsolen-Logger auf einem eigenen Thread; bei Ihnen kann sie auch nach der letzten Zeile stehen. Die vier Generatoren der Reihe nach.