Source Generators in .NET: Compile-Time Code die Runtime Verslaat
Hoe .NET Source Generators boilerplate elimineren en reflection overbodig maken. Met praktische voorbeelden voor serialization, mapping en validation.
Jean-Pierre Broeders
Freelance DevOps Engineer
Source Generators in .NET: Compile-Time Code die Runtime Verslaat
Reflection is handig. Tot het dat niet meer is. Een API die bij elke request properties moet enumereren via typeof(T).GetProperties() betaalt daar een prijs voor — elke keer opnieuw. Source Generators draaien dat om: de code wordt gegenereerd tijdens compilatie, niet ontdekt tijdens runtime.
Sinds .NET 5 zijn Source Generators beschikbaar, maar pas met .NET 6 en later werd het echt praktisch bruikbaar. Het idee is simpel: een stuk code analyseert de broncode tijdens het builden en genereert extra C#-bestanden die gewoon meecompileren. Geen runtime overhead, geen magic strings, volledige IntelliSense support.
Waarom Reflection een Probleem Wordt
Een typisch scenario: een REST API die JSON responses serialiseert. Met System.Text.Json en de standaard reflection-based serializer gebeurt er bij de eerste request voor elk type een hoop werk. Properties worden opgehaald, getters en setters worden via reflection aangeroepen, en dat alles kost tijd en geheugen.
Bij microservices die duizenden requests per seconde verwerken, telt dat op. Niet dramatisch voor een enkele call, maar bij schaal wordt het merkbaar.
// Reflection-based (standaard)
var json = JsonSerializer.Serialize(myObject);
// Source Generator-based
var json = JsonSerializer.Serialize(myObject, MyJsonContext.Default.MyObject);
Het verschil? De tweede variant weet al tijdens compilatie precies welke properties er zijn en hoe die geserialiseerd moeten worden. Geen reflection, geen runtime discovery.
System.Text.Json Source Generator Opzetten
De setup is verrassend weinig code:
[JsonSerializable(typeof(WeatherForecast))]
[JsonSerializable(typeof(List<WeatherForecast>))]
public partial class AppJsonContext : JsonSerializerContext
{
}
Dat is het. De Source Generator pikt dit op en genereert alle serialisatie-logica. In Program.cs wordt de context gekoppeld:
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain
.Insert(0, AppJsonContext.Default);
});
Vanaf dat moment gebruikt de hele applicatie de gegenereerde serializer. Geen reflection meer voor de geregistreerde types.
Eigen Source Generator Bouwen
Soms is er behoefte aan iets specifieks. Een veelvoorkomend geval: mapping tussen DTOs en domain models. Tools als AutoMapper doen dat runtime, maar een Source Generator kan het compile-time.
De structuur van een Source Generator:
[Generator]
public class MappingGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classes = context.SyntaxProvider
.ForAttributeWithMetadataName(
"MyApp.MapToAttribute",
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) => GetMappingInfo(ctx))
.Where(m => m is not null);
context.RegisterSourceOutput(classes, (spc, mapping) =>
{
var source = GenerateMappingCode(mapping!);
spc.AddSource($"{mapping!.SourceType}_Mapper.g.cs", source);
});
}
}
Het attribute markeert welke klassen een mapper nodig hebben:
[MapTo(typeof(WeatherForecastDto))]
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; } = "";
}
De generator leest beide types, matcht properties op naam en type, en spuugt een extensie-methode uit:
// Gegenereerd bestand: WeatherForecast_Mapper.g.cs
public static class WeatherForecastMappingExtensions
{
public static WeatherForecastDto ToDto(this WeatherForecast source)
{
return new WeatherForecastDto
{
Date = source.Date,
TemperatureC = source.TemperatureC,
Summary = source.Summary
};
}
}
Compile-time gegenereerd, null checks en all. Geen runtime mapping configuratie die stuk kan gaan bij een refactor.
Performance Verschil
De cijfers spreken voor zich. Een benchmark met BenchmarkDotNet op een simpele serialisatie:
| Methode | Gemiddelde tijd | Geheugen allocatie |
|---|---|---|
| Reflection-based JsonSerializer | 1.247 μs | 1.02 KB |
| Source Generator JsonSerializer | 0.384 μs | 0.31 KB |
| Handmatig geschreven serializer | 0.371 μs | 0.29 KB |
De Source Generator variant komt dicht bij handmatig geschreven code, maar zonder het onderhoud. Bij updates aan het model past de gegenereerde code zich automatisch aan.
Debugging en Troubleshooting
Source Generators zijn niet altijd makkelijk te debuggen. Een paar tips die helpen:
De gegenereerde bestanden zijn standaard onzichtbaar. Om ze te bekijken, voeg dit toe aan het .csproj:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>
$(BaseIntermediateOutputPath)\GeneratedFiles
</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
Na een build staan alle gegenereerde bestanden in obj/GeneratedFiles/. Handig om te verifiëren wat er daadwerkelijk uitgespuugd wordt.
Een andere valkuil: incremental generators die niet correct cachen. Als de generator bij elke keystroke in de IDE opnieuw draait, wordt Visual Studio traag. De IIncrementalGenerator interface met correcte Equals implementaties op de output-types voorkomt dat.
Wanneer Wel, Wanneer Niet
Source Generators zijn geen oplossing voor alles. Ze werken uitstekend voor:
- Serialisatie — JSON, XML, Protocol Buffers
- Mapping — DTO conversies, view models
- Validatie — compile-time checks op data annotaties
- Logging — de
LoggerMessageattribute in .NET 6+ gebruikt dit al
Minder geschikt voor situaties waar het type pas runtime bekend is — plugin systemen, dynamic loading, of scenarios waar assemblies at runtime worden geladen. Daar blijft reflection noodzakelijk.
Het mooie is dat beide benaderingen naast elkaar kunnen bestaan. De Source Generator pakt de bekende types, reflection vangt de rest op. Geen alles-of-niets keuze.
Conclusie
Source Generators verschuiven werk van runtime naar compile-time. Minder allocaties, snellere startup, en fouten die tijdens het builden al opvallen in plaats van in productie. De leercurve voor het schrijven van eigen generators is steil, maar de ingebouwde generators in System.Text.Json en LoggerMessage zijn direct bruikbaar zonder die investering.
Voor bestaande projecten: begin met de JSON Source Generator. Dat is de snelste winst met de minste moeite.
