Source Generators and Compile-Time Code Generation

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
Note: Source generators run as part of the Roslyn compilation pipeline โ€” they have access to the semantic model of your code (types, members, attributes) and produce new C# source files that are compiled alongside your code. The generated code is visible in Visual Studio and Rider under “Generated Files” in the project tree. You can inspect the generated code to understand what was produced. If the generated code has a bug, you can read it and report it as a bug to the generator’s author.
Tip: Enable 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.
Warning: Source generators require specific project configurations to work. Ensure the NuGet package providing the generator is referenced correctly (as an Analyzer, not a regular reference), that the generated files are not being excluded by your .gitignore or build configuration, and that the 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>.

🧠 Test Yourself

What is the main advantage of using JsonSerializerContext (source-generated JSON) over the default reflection-based serialisation?