Source generators are C# compiler plugins that run during compilation and generate additional C# source files. They enable compile-time code generation that eliminates runtime reflection costs. Instead of a JSON serialiser discovering properties at runtime through reflection, a source generator can generate the serialisation code at compile time โ the properties are read directly, no reflection required. ASP.NET Core and .NET 8 use source generators extensively: for JSON serialisation, regex compilation, logging, and dependency injection. Understanding how to consume source generators (even without writing your own) is increasingly important for modern .NET development.
JsonSerializerContext โ Compile-Time JSON
// โโ Without source generation โ runtime reflection โโโโโโโโโโโโโโโโโโโโโโโโโ
string json = JsonSerializer.Serialize(post); // uses reflection to find properties
// โโ With source generation โ compile-time code โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Define a JsonSerializerContext that enumerates the types to support
[JsonSerializable(typeof(Post))]
[JsonSerializable(typeof(List<Post>))]
[JsonSerializable(typeof(PostDto))]
[JsonSerializable(typeof(CreatePostRequest))]
public partial class BlogJsonContext : JsonSerializerContext { }
// Program.cs โ register the context with ASP.NET Core
builder.Services
.AddControllers()
.AddJsonOptions(opts =>
opts.JsonSerializerOptions.TypeInfoResolver = BlogJsonContext.Default);
// Or directly in serialisation code:
string json2 = JsonSerializer.Serialize(post, BlogJsonContext.Default.Post);
Post? post2 = JsonSerializer.Deserialize(json2, BlogJsonContext.Default.Post);
// Benefits:
// - No reflection at runtime โ property access is direct compiled code
// - AOT (Ahead-of-Time) compilation compatible
// - Trimming-safe (unused code can be stripped by the linker)
// - Faster cold-start performance
JsonSerializerContext for any ASP.NET Core Web API targeting high throughput or AOT deployment. The [JsonSerializable] attribute needs a type for each concrete type that will be serialised or deserialised โ include all request and response DTOs, domain entities returned from controllers, and collection variants. Missing a type causes a runtime fallback to reflection, partially defeating the purpose. The compiler (with the source generator active) will warn about missing types in most scenarios.partial keyword is present on any class the generator needs to extend. The most common source generator issue is forgetting partial on the JsonSerializerContext class โ the generator silently fails to extend the class without it.Regex Source Generator (C# 11 / .NET 7+)
// โโ Traditional regex โ compiled at runtime โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
private static readonly Regex EmailRegex = new(
@"^[^@\s]+@[^@\s]+\.[^@\s]+$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
// โโ Source-generated regex โ compiled at build time โโโโโโโโโโโโโโโโโโโโโโโโ
// Generates a NFA/DFA state machine at compile time โ fastest possible matching
public partial class Validators
{
[GeneratedRegex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.IgnoreCase)]
private static partial Regex EmailRegex();
}
bool isValid = Validators.EmailRegex().IsMatch("alice@example.com"); // no compile cost
LoggerMessage Source Generator
// โโ Traditional logging โ boxes parameters, string interpolation overhead โโโ
_logger.LogInformation("User {UserId} logged in from {IpAddress}", userId, ipAddress);
// โโ Source-generated LoggerMessage โ zero allocation, maximum performance โโโ
public static partial class LogMessages
{
[LoggerMessage(Level = LogLevel.Information, Message = "User {UserId} logged in from {IpAddress}")]
public static partial void UserLoggedIn(ILogger logger, string userId, string ipAddress);
}
// Call โ no boxing, no string allocation, no conditional check on IsEnabled:
LogMessages.UserLoggedIn(_logger, user.Id, request.RemoteIpAddress?.ToString() ?? "unknown");
// The source generator emits: if (!logger.IsEnabled(LogLevel.Information)) return;
Common Mistakes
Mistake 1 โ Forgetting partial keyword on source-generated class
โ Wrong โ compile error: cannot extend a non-partial class:
public class BlogJsonContext : JsonSerializerContext { } // missing partial!
โ Correct:
public partial class BlogJsonContext : JsonSerializerContext { } // โ
Mistake 2 โ Not including all serialised types in JsonSerializerContext
โ Wrong โ serialising a type not registered in the context falls back to reflection silently.
โ
Correct โ add a [JsonSerializable(typeof(T))] for every type you serialise, including collection variants like List<PostDto>.