Skip to content

Why Source Generator

In the previous chapter, we examined the definition and operating principles of source generators. However, knowing “this technology exists” and deciding “we should choose this technology” are different matters. With existing code generation techniques like Reflection, T4 templates, and Expression Trees already available, why should we use source generators specifically? This chapter covers the concrete advantages of source generators and suitable use scenarios.

  1. Understand the advantages of source generators
    • Grasp the specific evidence for performance, type safety, debugging, and AOT support
  2. Identify differences from existing code generation techniques
    • Comparison with T4 templates, Reflection.Emit, and Expression Trees
  3. Determine suitable use scenarios
    • Distinguish between cases where source generators are and are not appropriate

Runtime cost directly impacts application performance. Source generators generate code at compile time, so there is no runtime overhead.

// Reflection-based (runtime cost incurred)
public void LogWithReflection(object obj)
{
var properties = obj.GetType().GetProperties(); // Reflection call every time
foreach (var prop in properties)
{
var value = prop.GetValue(obj); // Runtime cost
Console.WriteLine($"{prop.Name}: {value}");
}
}
// Source generator-based (no runtime cost)
public void LogWithSourceGenerator(User user)
{
// Code generated at compile time - direct property access
Console.WriteLine($"Id: {user.Id}");
Console.WriteLine($"Name: {user.Name}");
Console.WriteLine($"Email: {user.Email}");
}
Performance Comparison (Example)
================================
Reflection: ~1,000 ns/call
Source Generator: ~10 ns/call
-> Approximately 100x performance improvement

Beyond performance, safety is also important. Since code is generated at compile time, type errors can be discovered at build time.

// Reflection - error occurs at runtime
var value = prop.GetValue(obj); // Runtime exception if obj is null
// Source generator - error discovered at compile time
public void Process(User user)
{
var id = user.Id; // Compile error if User has no Id
}

Generated code is regular C# code, so you can step into it with a debugger.

Viewing Generated Code in Visual Studio
========================================
1. Solution Explorer -> Dependencies -> Analyzers
2. Expand source generator project
3. View and debug generated .g.cs files

Fully compatible with .NET 10 Native AOT.

<!-- Enable AOT in .NET 10 project -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
</PropertyGroup>

Reflection-based code has limitations with AOT, but code generated by source generators is statically compiled with no restrictions.

Generated code is immediately recognized by the IDE’s IntelliSense.

// When the source generator generates a UserPipeline class
var pipeline = new UserPipeline();
pipeline. // <- IntelliSense shows generated methods

Having examined the individual advantages of source generators, let’s now directly compare them with existing code generation technologies to determine specifically which technology is suitable for which situation.


ItemT4 TemplatesSource Generator
Execution TimeDesign time (manual)Compile time (automatic)
Input Change DetectionManual re-execution neededAuto-detection and regeneration
Source ControlGenerated files must be includedNot needed
IDE IntegrationLimitedFull integration
.NET 10 SupportLegacyOfficial support
ItemReflection.EmitSource Generator
OutputIL codeC# source code
DebuggingVery difficultEasy (regular C#)
AOT SupportLimitedFull support
Learning CurveHigh (IL knowledge needed)Low (C# knowledge)
ItemExpression TreesSource Generator
Execution TimeRuntimeCompile time
Expression ScopeLambda expressionsFull C# syntax
PerformanceCaching neededNo overhead
ComplexityMediumMedium

Having confirmed the position of source generators through comparison, let’s now organize the cases where source generators should and should not be chosen in actual projects.


O Eliminating repetitive boilerplate code
Example: DTO mapping, INotifyPropertyChanged implementation
O Attribute-based code generation
Example: [Serialize], [Validate], [Log]
O Automating interface implementation
Example: Repository pattern, CQRS handlers
O Performance-critical serialization/deserialization
Example: JSON, MessagePack, Protocol Buffers
O Projects requiring AOT deployment
Example: iOS/Android, WebAssembly, Serverless
X When types are determined dynamically at runtime
Example: Plugin systems, scripting engines
X Very simple code generation
Example: One or two line wrapper methods
X Dependence on external data sources
Example: Database schema-based generation (not accessible at compile time)

Based on these criteria, let’s look at representative examples of how source generators are actually being used in the .NET ecosystem.


.NET’s official JSON library supports compile-time serialization with source generators.

// .NET 10 - JSON Source Generator
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(Order))]
public partial class AppJsonContext : JsonSerializerContext;
// Usage
var json = JsonSerializer.Serialize(user, AppJsonContext.Default.User);

A source generator for high-performance logging.

public static partial class Log
{
[LoggerMessage(Level = LogLevel.Information,
Message = "User {UserId} logged in at {LoginTime}")]
public static partial void UserLoggedIn(
ILogger logger, int userId, DateTime loginTime);
}

The source generator we will implement in this tutorial, which automatically generates adapter pipeline code.

// Input - written by developer
[GenerateObservablePort]
public class UserRepository(ILogger<UserRepository> logger) : IObservablePort
{
public FinT<IO, User> GetUserAsync(int id) => ...;
}
// Output - auto-generated by source generator
public partial class UserRepositoryObservable
{
private readonly ILogger<UserRepository> _logger;
public FinT<IO, User> GetUserAsync(int id)
{
// Logging, metrics, tracing code automatically included
}
}

Source generators provide five key advantages: performance with no runtime overhead, type safety through compile-time error detection, direct debugging of generated C# code, full compatibility with .NET 10 Native AOT, and IDE integration supporting IntelliSense and refactoring. They have a clear advantage over existing technologies, particularly in scenarios requiring repetitive boilerplate code generation and AOT deployment.


Q1: What are the advantages of source generators over T4 templates?

Section titled “Q1: What are the advantages of source generators over T4 templates?”

A: T4 templates must be manually executed at design time, and generated files must be included in source control. Source generators automatically execute at compile time, auto-detect input changes, and do not require generated files in source control, significantly reducing maintenance costs.

Q2: In what situations are source generators not suitable?

Section titled “Q2: In what situations are source generators not suitable?”

A: They are unsuitable for plugin systems where types are dynamically determined at runtime, or for database schema-based generation that cannot be accessed at compile time. Also, if you only need one or two line simple wrapper methods, the setup cost of source generators outweighs the benefits.

Q3: Why are source generators important in AOT environments?

Section titled “Q3: Why are source generators important in AOT environments?”

A: Native AOT restricts runtime code generation and dynamic reflection. Source generators statically generate all code at compile time, allowing the AOT compiler to optimize the generated code just like regular code, making them the only alternative for AOT deployment environments.


Having confirmed the reasons for choosing source generators, let’s now examine the overall structure and design goals of the ObservablePortGenerator project we will actually implement in this tutorial.

03. Project Overview