📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

As a consumer, how do you use the source generators that ship with .NET 10? Explain what a source generator can and cannot do, and walk through System.Text.Json source generation, GeneratedRegex, LoggerMessage, LibraryImport, the configuration binding and Request Delegate generators, and how to inspect and debug the generated code.

Using built-in .NET source generators for performance, trimming and Native AOT, and inspecting generated code.

Mid-Level Theory Advanced logging native-aot regex source-generators system-text-json

🎯 Key points

  • Source generators run inside the compiler and only add code, usually completing partial declarations marked with an attribute
  • System.Text.Json source generation uses a JsonSerializerContext and removes run-time reflection
  • GeneratedRegex compiles a pattern to C# at build time; partial properties are supported from .NET 9
  • LoggerMessage generates allocation-free logging with IsEnabled checks and template validation
  • LibraryImport generates P/Invoke marshalling at build time and needs AllowUnsafeBlocks
  • The configuration binding and Request Delegate generators use interceptors and are enabled automatically with Native AOT
  • Generated code improves start-up, reduces allocations and makes apps trim and AOT safe
  • Inspect output in the IDE analyzers node or with EmitCompilerGeneratedFiles

⏱ 30-second answer

A source generator is a compiler plug-in that reads your code (usually a marker attribute on a partial declaration) and adds new C# files to the compilation; it can never change or delete your code. .NET ships several: System.Text.Json ([JsonSerializable] on a JsonSerializerContext) for reflection-free serialisation, [GeneratedRegex] for regex compiled to C# at build time, [LoggerMessage] for allocation-free structured logging, [LibraryImport] for P/Invoke marshalling, and in ASP.NET Core the configuration binding and Request Delegate generators that replace reflection in Bind/Get<T> and minimal APIs. They improve startup, cut allocations and make code trim and Native AOT safe. Inspect output under Dependencies, Analyzers in the IDE, or set EmitCompilerGeneratedFiles to write the files to obj.

🗣 Full answer (senior)

How I would answer this in an interview

A source generator is a compiler plug-in that runs during compilation, inspects your code (syntax and semantic model) and adds new C# source files to the compilation. As a consumer, you usually trigger one with an attribute or a partial declaration, and get fast, reflection-free, AOT-friendly code. I'd explain what generators can and can't do, then walk through the ones that ship with .NET 10 and how to inspect and debug their output.

1. What a source generator can and cannot do

Can:

  • Read your code: syntax trees, symbols, attributes, AdditionalFiles (e.g. JSON schemas), MSBuild properties.
  • Add new source files (*.g.cs) — typically implementing partial members you declared.
  • Report diagnostics (warnings/errors).
  • Work incrementally (re-run only when relevant inputs change) — modern generators use IIncrementalGenerator.

Cannot:

  • Modify or delete your existing code (only add). That's why they rely on partial types/methods/properties.
  • See output of other generators in the same compilation (generators run in parallel against the same input).
  • Run at application run time — it's all compile-time; there's no reflection cost later.
  • Rewrite call sites — except via interceptors (C# 12 preview, stable for generators in .NET 9/10), which redirect specific calls to generated methods.

2. The generators shipping with .NET 10

System.Text.Json source generation

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(OrderDto))]
[JsonSerializable(typeof(List<OrderDto>))]
internal partial class AppJsonContext : JsonSerializerContext { }

var json = JsonSerializer.Serialize(order, AppJsonContext.Default.OrderDto);
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));

Generates type metadata (and optionally fast-path serialisation code) at compile time: no runtime reflection, faster start-up, required for NativeAOT/trimming.

[GeneratedRegex]

public static partial class Patterns
{
    [GeneratedRegex(@"^\d{4}$", RegexOptions.CultureInvariant)]
    public static partial Regex AustralianPostcode();
}

Generates the regex matching code as C# — no runtime regex parsing/compilation, readable source, AOT-friendly, often faster than RegexOptions.Compiled. Analyser SYSLIB1045 suggests converting.

[LoggerMessage]

public static partial class Log
{
    [LoggerMessage(EventId = 1001, Level = LogLevel.Information, Message = "Order {OrderId} shipped to {State}")]
    public static partial void OrderShipped(ILogger logger, Guid orderId, string state);
}

Generates high-performance logging: checks IsEnabled first, no params object[], no boxing, cached template parsing.

[LibraryImport] (replaces [DllImport] for P/Invoke)

[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static partial int strlen(string s);

Generates marshalling code at compile time instead of runtime IL stubs — AOT-compatible and debuggable.

Configuration binding generator: enable with <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator> (on by default for AOT/trimmed web apps). It uses interceptors to replace calls like config.GetSection("Smtp").Get<SmtpOptions>() and services.Configure<T>(section) with generated, reflection-free binding code.

Request Delegate Generator (RDG) for minimal APIs: <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator> (default with PublishAot). It intercepts app.MapGet(...) calls and generates the parameter binding and response-writing code that would otherwise be built at run time with reflection and expression trees.

Others: options validation generator ([OptionsValidator]), [ComInterfaceGenerator] (COM interop), metrics generators ([Counter], [Histogram] in Microsoft.Extensions.Telemetry.Abstractions), and the Blazor/Razor compilers.

3. Inspecting and debugging generated code

  • IDE: Solution Explorer → Dependencies → Analyzers → Generator name → generated files; "Go to Definition" on a partial method jumps to the generated implementation. Rider has similar support.
  • Emit to disk:
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
  • Debug through it: generated files are part of the compilation, so you can step into them in the debugger.
  • Diagnostics: generators report errors (e.g. SYSLIB1006 for invalid logging templates, SYSLIB1031 for JSON context issues) — read them like compiler errors.
  • Binary logs: dotnet build -bl and the MSBuild Structured Log Viewer show generator timings.

4. A production example

Moving an API to NativeAOT for faster cold start on Azure Container Apps produced dozens of trimming warnings. We added a JsonSerializerContext for all API DTOs, enabled RDG and the configuration binding generator, converted [DllImport] to [LibraryImport] in a small interop layer, and replaced hot-path Regex instances with [GeneratedRegex]. Warnings went to zero, cold start dropped from ~1.4 s to ~120 ms, and memory usage fell — and the JSON and logging changes made the JIT build faster too.

🔍 What the interviewer is checking

The interviewer wants to see that you use modern .NET's build-time features, not just reflection-era APIs.

Green flags

  • Explains generators as additive, compile-time code producers using partial declarations.
  • Names several built-in generators and what each replaces.
  • Links them to start-up time, allocations, trimming and Native AOT.
  • Knows how to inspect generated code.

Red flags

  • Thinks generators run at run time or can modify existing code.
  • Has never seen JsonSerializerContext or [LoggerMessage].
  • Cannot say how to see what a generator produced.

📚 Learn in depth

1. How generators run

The compiler loads analyser/generator assemblies (from NuGet packages or the SDK's built-in ones), creates the initial compilation, runs generators (incremental pipelines), adds their outputs, then compiles everything together. Because outputs feed into the same compilation, your code can call generated methods.

Incremental generators define a pipeline (context.SyntaxProvider.ForAttributeWithMetadataName(...), .Select(...), .Combine(...)) whose steps are cached by value equality, so typing in the IDE only re-runs affected parts — crucial for IDE performance.

2. Partial members as the extension point

You write the declaration:

[LoggerMessage(...)] public static partial void OrderShipped(ILogger logger, Guid id, string state);

The generator writes the implementation in another part:

public static partial void OrderShipped(ILogger logger, Guid id, string state)
{
    if (logger.IsEnabled(LogLevel.Information))
        __OrderShippedCallback(logger, id, state, null);
}

C# 13 partial properties and C# 14 partial constructors/events extend what generators can implement.

3. Interceptors

Interceptors let a generator say "replace the call at file X, line Y, column Z with my method" via [InterceptsLocation] (now using an opaque, versioned location encoding). RDG and the configuration binder use them to swap reflection-based APIs for generated code without you changing call sites. Projects opt in via InterceptorsNamespaces (formerly InterceptorsPreviewNamespaces).

4. Consumer trade-offs

Benefit Cost
No runtime reflection; AOT/trim friendly Build-time cost (usually small with incremental generators)
Faster start-up and execution Generated code adds to assembly size
Errors at compile time Some features limited vs reflection (e.g. polymorphic JSON needs explicit config)
Debuggable generated source Requires partial declarations and attributes

5. System.Text.Json modes

  • Metadata mode (default): generates JsonTypeInfo metadata; serialisation still uses the general serializer engine.
  • Serialisation optimisation (fast-path) mode: generates direct Utf8JsonWriter code for serialisation (not deserialisation), fastest for writes.
  • Configure via [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata | Serialization)].

Try it yourself

  1. Add [GeneratedRegex] and [LoggerMessage] to a project and inspect the generated files.
  2. Set EmitCompilerGeneratedFiles and look at the output folder.
  3. Publish a minimal API with PublishAot and compare warnings with and without a JsonSerializerContext.
  4. Benchmark Regex with RegexOptions.Compiled vs [GeneratedRegex] for start-up and throughput.

💻 Code

<!-- Generators.Demo.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>                 <!-- required by LibraryImport -->
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <!-- write generated files to obj -->
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
  </ItemGroup>
</Project>
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;

public sealed record Order(int Id, string Customer, decimal Total);

// System.Text.Json: serialisation metadata generated at build time
[JsonSerializable(typeof(Order))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
internal partial class AppJsonContext : JsonSerializerContext
{
}

public static partial class Patterns
{
    // Australian postcode; partial property form needs .NET 9 / C# 13
    [GeneratedRegex(@"^\d{4}$")]
    public static partial Regex Postcode { get; }
}

public static partial class Log
{
    // Template placeholders are checked against parameters at build time
    [LoggerMessage(EventId = 1001, Level = LogLevel.Information, Message = "Order {OrderId} placed for {Total}")]
    public static partial void OrderPlaced(ILogger logger, int orderId, decimal total);
}

public static partial class Native
{
    // Marshalling generated at build time (Linux/macOS libc)
    [LibraryImport("libc", EntryPoint = "getpid")]
    public static partial int GetPid();
}

public static class Program
{
    public static void Main()
    {
        var order = new Order(42, "Mia", 129.95m);

        string json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
        Order? back = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
        Console.WriteLine($"{json} -> {back}");

        Console.WriteLine(Patterns.Postcode.IsMatch("2000"));   // True
        Console.WriteLine(Patterns.Postcode.IsMatch("20000"));  // False

        using ILoggerFactory factory = LoggerFactory.Create(b => b.AddConsole());
        ILogger logger = factory.CreateLogger("Orders");
        Log.OrderPlaced(logger, order.Id, order.Total);

        if (!OperatingSystem.IsWindows())
            Console.WriteLine($"PID {Native.GetPid()}");
    }
}

⚠ Common mistakes

1. Forgetting partial

What happens: attributes added to non-partial methods/classes. Why it is wrong: generators can't add implementations; compile errors or no generation. Do instead: declare classes and members partial as required.

2. Not registering the JSON context

What happens: a JsonSerializerContext exists but ASP.NET Core still uses reflection. Why it is wrong: no AOT benefit; trim warnings remain. Do instead: add the context to TypeInfoResolverChain or pass JsonTypeInfo explicitly.

3. Missing types in the JSON context

What happens: some DTOs or collection types aren't listed. Why it is wrong: runtime NotSupportedException in AOT apps. Do instead: register every root type (including List<T> and polymorphic types).

4. Editing generated files

What happens: modifying .g.cs outputs. Why it is wrong: overwritten on the next build. Do instead: change your declarations or generator options.

5. Ignoring generator diagnostics

What happens: SYSLIB warnings suppressed. Why it is wrong: hides misconfiguration (invalid templates, unsupported types). Do instead: fix them; treat them as errors in CI.

6. Expecting generators to modify existing code

What happens: trying to inject behaviour into existing methods. Why it is wrong: generators only add code. Do instead: use partial members, interceptors (via official generators), or decorators.

7. Keeping RegexOptions.Compiled for static patterns

What happens: compiled regexes at start-up. Why it is wrong: slower start-up and not AOT friendly. Do instead: [GeneratedRegex].

8. Using interpolated strings in logging instead of [LoggerMessage]

What happens: LogInformation($"...") in hot paths. Why it is wrong: allocations and lost structure. Do instead: source-generated logging methods.

🏛 Senior insight: trade-offs

Built-in generators are the practical face of a big .NET shift: move work from run time to build time. A senior engineer uses them deliberately.

Goal Generator lever
Faster cold start in containers or serverless JSON context, GeneratedRegex, RDG, config binder
Lower allocation in hot logging paths LoggerMessage
Native AOT or trimmed publish All of the above; they remove reflection warnings
Earlier failure Build-time diagnostics for templates, patterns and marshalling

The costs are real but small: slightly longer builds, more files to review, and features that the reflection path supports but the fast path does not (some custom converters, dynamic types). Adopt them where start-up, throughput or AOT matter, and turn on the SYSLIB analyser suggestions so the team migrates gradually rather than in one risky change.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says generators avoid reflection. Why: Generators exist to remove run-time reflection.
    How to answer

    Reflection reads the metadata the compiler wrote into an assembly at run time: typeof(T) or obj.GetType() gives a Type, and from it you get PropertyInfo, MethodInfo, FieldInfo and so on, filtered by BindingFlags. It is flexible but slow on hot paths: lookups search metadata, Invoke takes an object[], boxes value types, checks arguments and wraps exceptions in TargetInvocationException. Make it fast by looking up once and caching, turning a MethodInfo into a typed delegate, using MethodInvoker (.NET 8) when you only have a MethodInfo, or [UnsafeAccessor] (.NET 8) to reach a private member with no reflection at all. Reflection also hides dependencies from the trimmer, so in trimmed or Native AOT apps you must annotate with [DynamicallyAccessedMembers] or move the work to a source generator.

  2. When: Candidate mentions partial members with attributes. Why: Generators are driven by marker attributes.
    How to answer

    An attribute is declarative metadata: the compiler serialises the constructor arguments and named properties into the assembly's metadata, and nothing runs until someone reads it. An instance is created only when code calls GetCustomAttribute(s), and a new instance is created on every call. [AttributeUsage] controls valid targets, AllowMultiple and Inherited. Arguments must be compile-time constants, typeof, enums or one-dimensional arrays of those. C# 11 added generic attributes ([Validator<OrderValidator>]). Some attributes are read by the compiler itself ([Obsolete], [Conditional], nullable attributes such as [NotNullWhen]), and caller info attributes ([CallerMemberName], [CallerFilePath], [CallerLineNumber], C# 10's [CallerArgumentExpression]) make the compiler fill optional parameters at the call site.

  3. When: Candidate mentions Roslyn or build diagnostics. Why: Analysers and generators share the Roslyn APIs.
    How to answer

    Roslyn is the C# compiler exposed as an API: code becomes syntax trees, a semantic model that knows what every symbol means, and language-neutral operations. A Roslyn analyser is a plug-in that subscribes to those and reports diagnostics in the IDE and in every build, so team rules become compiler warnings or errors instead of review comments. Start with what ships with the SDK: the .NET analysers (CAxxxx) and code style rules (IDExxxx), tuned with AnalysisLevel/AnalysisMode, .editorconfig severities and EnforceCodeStyleInBuild. BannedApiAnalyzers bans specific APIs from a BannedSymbols.txt file with no custom code. When you need a domain rule, write a DiagnosticAnalyzer targeting netstandard2.0, register operation or symbol actions, enable concurrent execution, test it, and ship it as a NuGet package or project reference.

🛠 Real-world problems: how you handle them

Slow cold starts blocked scale-out of a containerised API
Situation
An order API on [Azure Container Apps] scaled from [2] to [20] replicas during sales. Each new replica took about [2 seconds] to serve its first request.
Signals
Spikes of 503s and high latency during scale-out events; startup traces showed time in JSON metadata building, regex construction and minimal API binding.
Investigation
Measured startup with dotnet-trace and the EventPipe startup events, then enabled trimming analysis, which listed [37] IL2026/IL3050 warnings pointing at reflection-based JSON and configuration binding.
Resolution
Added a JsonSerializerContext for all DTOs, converted regexes to GeneratedRegex, switched logging to LoggerMessage, and published with Native AOT, which turned on the Request Delegate and configuration binding generators.
Prevention
Made trimming warnings errors in CI and added a startup-time check to the deployment pipeline.

How to tell it (STAR)

**Situation:** during [sales events] our order API scaled out, but each new replica needed [2 seconds] to start, so users saw errors. **Task:** I was asked to cut cold start without a rewrite. **Action:** I profiled startup, found reflection-heavy JSON, regex and binding work, and moved each to the built-in source generators, then published with Native AOT and fixed the remaining trimming warnings. **Result:** cold start fell to about [300 ms], memory per replica dropped by [40%], and scale-out errors disappeared.

↩ Usually asked after

More in Advanced Language Features

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me