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

📚 Interview question

What are the options for build-time code generation in .NET, and how do you choose between source generators, MSBuild tasks, T4 templates and IL weaving?

Comparing source generators, MSBuild-generated files (gRPC, OpenAPI), T4, IL weaving and interceptors for build-time code generation, with trade-offs.

Senior Architecture Advanced code-generation il-weaving msbuild source-generators t4

🎯 Key points

  • Source generators run in the compiler and add C# based on the user's code
  • MSBuild targets or tool packages generate code from non-C# contracts such as .proto or OpenAPI
  • T4 is legacy text templating without semantic knowledge
  • IL weaving rewrites compiled output and is fragile and invisible
  • Interceptors are a niche tool used inside generators
  • Make MSBuild generation incremental with Inputs, Outputs and the intermediate folder
  • Prefer visible, incremental, standard tooling and existing generators

⏱ 30-second answer

Build-time options: source generators (in the compiler, see the user's code, add C#), MSBuild targets or tools that write .cs files before compilation from external inputs (gRPC .proto via Grpc.Tools, OpenAPI clients via Kiota or NSwag), T4 templates (text templates run at design time or build time), and IL weaving (for example Fody), which rewrites the compiled assembly after build. Choose source generators when output depends on C# code, MSBuild generation when the input is a non-C# contract, T4 only for legacy cases, and avoid IL weaving in new code because the generated behaviour is invisible and fragile. Interceptors are a specialised tool for generator authors.

🗣 Full answer (senior)

Comparison:

Option Input When it runs Visible in IDE Typical uses
Source generator User's C# code, additional files During compilation, also live in IDE Yes JSON contexts, logging, DI registration, mappers
MSBuild target or tool .proto, OpenAPI, SQL, resx Before CoreCompile After build, in obj/ gRPC stubs, API clients, resource classes
T4 template Template plus any data Design time or build (with extra setup) Generated file committed Legacy code, simple text expansion
IL weaving Compiled IL After compile No (only via decompiler) INotifyPropertyChanged injection, legacy AOP
Interceptors Generator-produced call redirection During compilation Yes Framework internals, for example ASP.NET Core request delegate generation

Decision guide:

  1. Is the input C# code you control? Use a source generator (existing ones first).
  2. Is the input a contract file in another format? Use an MSBuild target or the vendor's tool package, with proper Inputs/Outputs for incremental builds.
  3. Do you need to change existing method bodies? Prefer redesigning (decorators, partial methods, interceptors in a generator) over IL weaving.
  4. Is it a one-off scaffold? Generate once with a CLI and commit the code.

Committed versus generated-on-build: generated-on-build stays in sync automatically; committed code is reviewable and works without the tool. API clients are often regenerated in CI and diffed, so contract changes appear in pull requests.

Production example: a team used Fody to inject INotifyPropertyChanged into view models. After an SDK upgrade the weaver broke the build, and debugging showed code that did not exist in source. Replacing it with the CommunityToolkit.Mvvm source generator ([ObservableProperty]) gave the same result with code visible in the IDE and no post-compile step.

🔍 What the interviewer is checking

The interviewer checks architectural judgement about build tooling.

Green flags: compares several mechanisms with inputs and timing, prefers source generators for C#-driven output, MSBuild for contract files, incremental targets, cautions against IL weaving, considers maintenance.

Red flags: knows only one option; recommends IL weaving by default; ignores build performance.

📚 Learn in depth

1. MSBuild generation done right

  • Run before CoreCompile (BeforeTargets="CoreCompile") and add outputs to @(Compile).
  • Declare Inputs and Outputs so the target is skipped when nothing changed.
  • Write to $(IntermediateOutputPath) and add files to @(FileWrites) so clean removes them.

2. Viewing what generators produced

<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> writes source-generator output under obj/<config>/<tfm>/generated. Useful in code review and when debugging.

3. Why IL weaving fell out of favour

  • The behaviour is invisible in source and in the debugger without decompiling.
  • It is coupled to compiler output details and breaks on SDK changes.
  • It complicates trimming, AOT and deterministic builds.

Source generators plus partial members (including partial properties since C# 13) cover most former weaving use cases.

4. T4 today

T4 still works in Visual Studio and with the dotnet-t4 tool, but it does not see the semantic model and is not incremental. It is acceptable for existing code; new work usually uses generators or scripts.

5. Try it yourself

Add the MSBuild target from the code example to a project, build twice and check in the build log (-v:n) that the second build skips the target because inputs did not change.

💻 Code

<!-- Generate a constants class from version.txt (for example "1.4.2") before compilation, incrementally -->
<Target Name="GenerateBuildInfo"
        BeforeTargets="CoreCompile"
        Inputs="$(MSBuildProjectDirectory)/version.txt"
        Outputs="$(IntermediateOutputPath)BuildInfo.g.cs">
  <PropertyGroup>
    <_BuildInfoVersion>$([System.IO.File]::ReadAllText('$(MSBuildProjectDirectory)/version.txt').Trim())</_BuildInfoVersion>
  </PropertyGroup>
  <!-- ';' separates lines; %3B is a literal semicolon -->
  <WriteLinesToFile File="$(IntermediateOutputPath)BuildInfo.g.cs"
                    Overwrite="true"
                    Lines="// &lt;auto-generated/&gt;;internal static class BuildInfo { public const string Version = &quot;$(_BuildInfoVersion)&quot;%3B }" />
  <ItemGroup>
    <Compile Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
    <FileWrites Include="$(IntermediateOutputPath)BuildInfo.g.cs" />
  </ItemGroup>
</Target>

<!-- gRPC: contract-first generation through the tool package -->
<ItemGroup>
  <PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="all" />
  <Protobuf Include="Protos/orders.proto" GrpcServices="Server" />
</ItemGroup>

<!-- See what source generators produced -->
<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
// Source generator alternative to IL weaving (CommunityToolkit.Mvvm 8.4+ partial property support)
using CommunityToolkit.Mvvm.ComponentModel;

public partial class OrderViewModel : ObservableObject
{
    [ObservableProperty]
    public partial string? Status { get; set; }   // the generator implements the property with change notification
}

// Using the MSBuild-generated constant
static class About
{
    public static string Describe() => $"Version {BuildInfo.Version}";
}

⚠ Common mistakes

  • Mistake: generating files in an MSBuild target without Inputs and Outputs. Why it is wrong: the target runs on every build and forces recompilation. Instead: declare inputs and outputs and write to the intermediate folder.
  • Mistake: choosing IL weaving for new cross-cutting behaviour. Why it is wrong: it hides behaviour, breaks on SDK changes and hurts trimming and AOT. Instead: use source generators, partial members or explicit decorators.
  • Mistake: writing a custom generator when a maintained one exists. Why it is wrong: you take on compiler-version and performance maintenance. Instead: use the .NET generators (JSON, logging, regex, config binding) or established packages first.

🏛 Senior insight: trade-offs

Code generation is a long-term maintenance commitment, so seniors pick the most visible, least magical option.

Priority Reason
Visible output Reviewable, debuggable, explainable to new team members
Incremental Does not slow builds or the IDE
Standard tooling Survives SDK upgrades
Clear ownership Someone maintains the generator or template

The senior question to ask: would hand-written code plus a test be simpler for this case?

🔗 Follow-ups the interviewer may ask

  1. When: Candidate chooses a source generator Why: Checks generator authoring
    How to answer

    A source generator is a class marked [Generator] that implements IIncrementalGenerator (the older ISourceGenerator is deprecated). It runs inside the compiler, reads the user's code through Roslyn and adds new C# files to the compilation; it cannot modify existing code. The generator project targets netstandard2.0, references Microsoft.CodeAnalysis.CSharp (a version no newer than your consumers' compilers) with PrivateAssets="all", and sets EnforceExtendedAnalyzerRules. Consumers reference it as an analyser (OutputItemType="Analyzer" ReferenceOutputAssembly="false"), or you pack it into analyzers/dotnet/cs in a NuGet package. Use RegisterPostInitializationOutput for marker attributes and EmitCompilerGeneratedFiles to see the output.

  2. When: Candidate mentions interceptors Why: Probes what interceptors are and when they fit
    How to answer

    Interceptors are a compiler feature, first shipped experimentally in .NET 8 / C# 12 and stable from the .NET 9.0.2xx SDK onwards, that lets a source generator replace one specific method call in your code with a call to generated code, at compile time. The generator emits a method marked [InterceptsLocation(...)] that identifies the exact call site; the compiler then emits a call to the interceptor instead of the original method. It exists so generators can make existing APIs reflection-free and AOT-safe without you changing your code: ASP.NET Core's Request Delegate Generator intercepts MapGet/MapPost, and the configuration binding generator intercepts Bind, Get<T> and Configure<T>. You opt in per namespace with the InterceptorsNamespaces MSBuild property (the SDK does this for those generators). Developers should know they exist when debugging, but not hand-write them: the location data is generated and tied to the exact file contents.

  3. When: Candidate writes an MSBuild target Why: Checks MSBuild fundamentals
    How to answer

    MSBuild is the build engine under dotnet build, Visual Studio and Rider, and a .csproj is an MSBuild file. Properties are named values such as $(Configuration). Items are lists with metadata, such as @(Compile) holding every .cs file. Targets are named steps, ordered with DependsOnTargets, BeforeTargets and AfterTargets. Each target runs tasks, the real units of work, such as Csc to compile or Copy. The build has two phases. Evaluation reads imports, properties and items. Execution runs the targets. Targets that declare Inputs and Outputs are skipped when nothing has changed, which is what makes builds incremental.

🛠 Real-world problems: how you handle them

Replacing an IL weaver that broke on an SDK upgrade
Situation
A WPF line-of-business app used Fody to inject property-change notification into [about 150] view models.
Signals
After an SDK upgrade, builds failed with weaver errors and debugging showed behaviour not present in source.
Investigation
Confirmed the weaver package lagged the new compiler output; no quick fix available.
Resolution
Migrated view models to CommunityToolkit.Mvvm `[ObservableProperty]` source generation over [two sprints], project by project.
Prevention
Added a guideline preferring source generators over post-compile tools.

How to tell it (STAR)

Our [WPF app] used IL weaving, and an SDK upgrade broke the build. I led a migration to the MVVM Toolkit source generator. The behaviour stayed the same, but the generated code became visible and debuggable and our builds no longer depended on a post-compile step.

More in Compilation

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me