Project Overview
Overview
Section titled “Overview”The previous two chapters covered the concept and rationale for source generators. Now it’s time to connect theory to a real project. This chapter introduces the design goals, the problem being solved, and the project structure of the ObservablePortGenerator that we will implement throughout this tutorial. The big picture drawn here becomes the learning context for each subsequent chapter.
Learning Objectives
Section titled “Learning Objectives”Core Learning Objectives
Section titled “Core Learning Objectives”- Understand the purpose and expected benefits of ObservablePortGenerator
- Identify the cross-cutting concern problem in the adapter layer and the need for automation
- Understand the reasons for implementing with a source generator
- Specific advantages of compile-time generation over runtime AOP
- Understand the overall project structure
- The relationship between the source generator, core library, and test projects
What Is ObservablePortGenerator?
Section titled “What Is ObservablePortGenerator?”ObservablePortGenerator is a source generator that automatically adds Observability capabilities to adapter classes.
The Problem It Solves
Section titled “The Problem It Solves”The adapter layer (database access, external API calls, etc.) must handle the following cross-cutting concerns:
Cross-Cutting Concerns======================
1. Logging - Request/response recording - Parameter value tracking - Error information recording
2. Tracing - Distributed tracing context propagation - Activity creation and management - Request correlation tracking
3. Metrics - Response time measurement - Success/failure counters - Resource usage measurementProblems with Manual Implementation
Section titled “Problems with Manual Implementation”Manually implementing these features causes the following problems:
// Manual implementation - boilerplate code repeated in every methodpublic class UserRepository : IObservablePort{ private readonly ILogger<UserRepository> _logger; private readonly ActivitySource _activitySource; private readonly Counter<long> _requestCounter; private readonly Histogram<double> _durationHistogram;
public FinT<IO, User> GetUserAsync(int userId) { // Record start time var startTimestamp = Stopwatch.GetTimestamp();
// Request logging _logger.LogInformation("GetUserAsync request: userId={UserId}", userId);
// Create tracing Activity using var activity = _activitySource.StartActivity("GetUserAsync");
try { // Actual business logic var result = await _dbContext.Users.FindAsync(userId);
// Success logging var elapsed = CalculateElapsed(startTimestamp); _logger.LogInformation("GetUserAsync success: {Elapsed}ms", elapsed);
// Record metrics _requestCounter.Add(1);
return result; } catch (Exception ex) { // Failure logging var elapsed = CalculateElapsed(startTimestamp); _logger.LogError(ex, "GetUserAsync failure: {Elapsed}ms", elapsed);
// Record metrics _durationHistogram.Record(elapsed);
throw; } }
// Same pattern repeated for other methods... public FinT<IO, IEnumerable<User>> GetUsersAsync() { /* same boilerplate */ } public FinT<IO, Unit> UpdateUserAsync(User user) { /* same boilerplate */ } public FinT<IO, Unit> DeleteUserAsync(int userId) { /* same boilerplate */ }}Problems:
- 30-50 lines of boilerplate code added per method
- Possibility of accidentally omitting logging
- Difficult to maintain consistency of logging formats
- Difficult to identify core logic during code review
Existing techniques such as runtime AOP or Interceptors could be used to solve this problem. However, ObservablePortGenerator chose source generators. Let’s examine the specific reasons why.
Why a Source Generator?
Section titled “Why a Source Generator?”1. Automated Consistency
Section titled “1. Automated Consistency”Source generators apply the same pattern to every method. It is impossible for developers to accidentally omit anything.
// Code the developer writes - focus only on core logic[GenerateObservablePort]public class UserRepository(ILogger<UserRepository> logger) : IObservablePort{ public FinT<IO, User> GetUserAsync(int userId) => // Write only pure business logic from user in _dbContext.Users.FindAsync(userId) select user;
public FinT<IO, IEnumerable<User>> GetUsersAsync() => from users in _dbContext.Users.ToListAsync() select users;}
// Code automatically generated by the source generatorpublic class UserRepositoryObservable : UserRepository{ // Logging, tracing, and metrics automatically applied to all methods}2. Compile-Time Performance
Section titled “2. Compile-Time Performance”Unlike runtime AOP (Aspect-Oriented Programming) or Interceptors, code is generated at compile time, so:
| Approach | Runtime Overhead | AOT Support |
|---|---|---|
| Castle DynamicProxy | High | Limited |
| DispatchProxy | Medium | Limited |
| Source Generator | None | Full Support |
3. Ease of Debugging
Section titled “3. Ease of Debugging”The generated code is regular C# code, so you can step into it with the debugger to examine the logging logic.
4. High-Performance Logging
Section titled “4. High-Performance Logging”Automatically generates high-performance logging code using LoggerMessage.Define:
// High-performance logging code generated by the source generatorinternal static class UserRepositoryObservableLoggers{ private static readonly Action<ILogger, string, string, string, string, int, Exception?> _logAdapterRequestDebug_UserRepository_GetUserAsync = LoggerMessage.Define<string, string, string, string, int>( LogLevel.Debug, ObservabilityNaming.EventIds.Adapter.AdapterRequest, "{request.layer} {request.category} {request.handler}.{request.handler.method} requesting with {request.params.userid}");
public static void LogAdapterRequestDebug_UserRepository_GetUserAsync( this ILogger logger, string requestLayer, string requestCategory, string requestHandler, string requestHandlerMethod, int userId) { if (!logger.IsEnabled(LogLevel.Debug)) return;
_logAdapterRequestDebug_UserRepository_GetUserAsync(logger, requestLayer, requestCategory, requestHandler, requestHandlerMethod, userId, null); }}5. Type Safety
Section titled “5. Type Safety”When parameter types change, a compile error occurs immediately, alerting you right away.
Now that we have confirmed the reasons for choosing a source generator, let’s examine what design patterns ObservablePortGenerator is built upon.
Core Design Patterns
Section titled “Core Design Patterns”Template Method Pattern
Section titled “Template Method Pattern”IncrementalGeneratorBase applies the Template Method Pattern to define the common flow of source generators:
// Template Method Pattern - common flow definitionpublic abstract class IncrementalGeneratorBase<TValue>( Func<IncrementalGeneratorInitializationContext, IncrementalValuesProvider<TValue>> registerSourceProvider, // Step 1: Register source provider Action<SourceProductionContext, ImmutableArray<TValue>> generate, // Step 2: Code generation //Action<IncrementalGeneratorPostInitializationContext>? registerPostInitializationSourceOutput = null, bool AttachDebugger = false) : IIncrementalGenerator{ protected const string ClassEntityName = "class";
private readonly bool _attachDebugger = AttachDebugger; private readonly Func<IncrementalGeneratorInitializationContext, IncrementalValuesProvider<TValue>> _registerSourceProvider = registerSourceProvider; private readonly Action<SourceProductionContext, ImmutableArray<TValue>> _generate = generate;
// Template method - fixed algorithm flow public void Initialize(IncrementalGeneratorInitializationContext context) {#if DEBUG if (_attachDebugger && Debugger.IsAttached is false) { Debugger.Launch(); }#endif
// Step 1: Extract targets of interest from source code IncrementalValuesProvider<TValue> provider = _registerSourceProvider(context) .Where(static m => m is not null);
// Step 2: Generate code from extracted information context.RegisterSourceOutput(provider.Collect(), Execute); }
private void Execute(SourceProductionContext context, ImmutableArray<TValue> displayValues) { _generate(context, displayValues); }}Template Method Pattern Structure=================================
IncrementalGeneratorBase (abstract class)│├── Initialize() # Template method (fixed)│ ├── registerSourceProvider() # Abstract step 1│ ├── .Where(not null) # Null filtering│ └── Execute() → generate() # Abstract step 2│└── ObservablePortGenerator (concrete class) ├── RegisterSourceProvider() # Implementation: Filter [GenerateObservablePort] classes └── Generate() # Implementation: Generate Observable codeBenefits:
- Reuse common structure of source generators
- Only implement core logic when adding new generators
- Centrally manage common features such as debugging flags
Strategy Pattern with IObservablePort
Section titled “Strategy Pattern with IObservablePort”The Strategy Pattern is implemented through the IObservablePort interface. Each adapter encapsulates a communication strategy with a specific external system:
// IObservablePort interface - common contract for strategiespublic interface IObservablePort{ string RequestCategory { get; }}
// Concrete strategy definition - user repositorypublic interface IUserRepository : IObservablePort{ FinT<IO, User> GetUserAsync(int id); FinT<IO, IEnumerable<User>> GetUsersAsync();}
// Concrete strategy definition - order repositorypublic interface IOrderRepository : IObservablePort{ FinT<IO, Order> GetOrderAsync(int id); FinT<IO, Unit> CreateOrderAsync(Order order);}Strategy Pattern Structure==========================
IObservablePort (strategy interface)│├── IUserRepository # User-related strategy│ └── UserRepository # Concrete implementation│ └── UserRepositoryObservable <- Generated by source generator│├── IOrderRepository # Order-related strategy│ └── OrderRepository # Concrete implementation│ └── OrderRepositoryObservable <- Generated by source generator│└── IProductRepository # Product-related strategy └── ProductRepository # Concrete implementation └── ProductRepositoryObservable <- Generated by source generatorRole of the Source Generator:
// Written by the developer - strategy implementation[GenerateObservablePort]public class UserRepository(ILogger<UserRepository> logger) : IUserRepository{ public FinT<IO, User> GetUserAsync(int id) => // Pure data access logic from user in _dbContext.Users.FindAsync(id) select user;}
// Auto-generated by source generator - strategy decoratorpublic class UserRepositoryObservable : UserRepository{ // Inherits original strategy and adds observability features // Logging, tracing, and metrics are automatically applied}Benefits:
- Business logic isolation for each adapter (strategy)
- Consistent automatic application of observability code
- Easy to swap in Observable classes in the DI container
// Use Observable classes when registering with DIservices.AddScoped<IUserRepository, UserRepositoryObservable>();services.AddScoped<IOrderRepository, OrderRepositoryObservable>();By combining the Template Method Pattern and the Strategy Pattern, the common flow of source generators is reused while consistently generating Observability code for each adapter. Let’s verify the actual impact of this design with numbers.
Expected Benefits
Section titled “Expected Benefits”Before (Manual Implementation)
Section titled “Before (Manual Implementation)”Lines of Code==============UserRepository.cs : 200 lines (including logging code)OrderRepository.cs : 180 lines (including logging code)ProductRepository.cs : 220 lines (including logging code)-----------------------------------------Total : 600 lines
Problems========- Business logic mixed with cross-cutting concerns- Difficult to maintain consistency- Increased code review complexityAfter (Source Generator)
Section titled “After (Source Generator)”Lines of Code==============UserRepository.cs : 50 lines (pure business logic)OrderRepository.cs : 40 lines (pure business logic)ProductRepository.cs : 60 lines (pure business logic)-----------------------------------------Total : 150 lines (75% reduction)
+ Auto-generated Observable classes UserRepositoryObservable.g.cs : auto-generated OrderRepositoryObservable.g.cs : auto-generated ProductRepositoryObservable.g.cs : auto-generated
Benefits========- Focus only on business logic- 100% consistent Observability- Improved code review efficiencyLet’s examine the actual directory structure of the project that achieves these benefits.
Project Structure
Section titled “Project Structure”Functorium/├── Src/│ ├── Functorium.SourceGenerators/ # Source generator│ │ ├── Abstractions/│ │ │ ├── Constants.cs # Common constants (headers, etc.)│ │ │ └── Selectors.cs # Common selectors│ │ ││ │ └── Generators/│ │ ├── IncrementalGeneratorBase.cs # Template Method Pattern base class│ │ ││ │ ├── ObservablePortGenerator/ # Observability code generator│ │ │ ├── ObservablePortGenerator.cs # Main source generator│ │ │ ├── ObservableGeneratorConstants.cs # Generator-specific constants│ │ │ ├── ObservableClassInfo.cs # Class info record│ │ │ ├── MethodInfo.cs # Method info│ │ │ ├── ParameterInfo.cs # Parameter info│ │ │ ├── TypeExtractor.cs # Type extraction utility│ │ │ ├── CollectionTypeHelper.cs # Collection type detection│ │ │ ├── SymbolDisplayFormats.cs # Type string format│ │ │ ├── ConstructorParameterExtractor.cs # Constructor analysis│ │ │ └── ParameterNameResolver.cs # Name conflict resolution│ │ ││ │ ├── EntityIdGenerator/ # Entity ID auto-generator│ │ │ ├── EntityIdGenerator.cs # Ulid-based ID struct generation│ │ │ └── EntityIdInfo.cs # Entity info record│ │ ││ │ └── UnionTypeGenerator/ # Union Type generator│ │ ├── UnionTypeGenerator.cs # Match/Switch method generation│ │ └── UnionTypeInfo.cs # Union info record│ ││ ├── Functorium/ # Core domain library│ │ └── Domains/│ │ └── Observabilities/│ │ └── IObservablePort.cs # Observability marker interface│ ││ ├── Functorium.Adapters/ # Adapter library│ │ ├── SourceGenerators/│ │ │ └── GenerateObservablePortAttribute.cs # [GenerateObservablePort] attribute│ │ └── Observabilities/│ │ └── Naming/│ │ ├── ObservabilityNaming.cs # Observability naming rules│ │ ├── ObservabilityNaming.Events.cs # Event ID definitions│ │ └── ObservabilityNaming.Attributes.cs # Attribute key definitions│ ││ └── Functorium.Testing/ # Test utilities│ └── Actions/│ └── SourceGenerators/│ └── SourceGeneratorTestRunner.cs # Test runner│└── Tests/ └── Functorium.Tests.Unit/ └── AdaptersTests/ └── SourceGenerators/ ├── ObservablePortGeneratorTests.cs # 31 snapshot tests ├── ObservablePortObservabilityTests.cs # Tag structure verification ├── ObservablePortLoggingStructureTests.cs # Logging field structure verification ├── ObservablePortMetricsStructureTests.cs # Metrics tag structure verification ├── ObservablePortTracingStructureTests.cs # Tracing tag structure verification └── Snapshots/ # Snapshot files ├── ObservablePortGenerator/ ├── ObservablePortLoggingStructure/ ├── ObservablePortMetricsStructure/ └── ObservablePortTracingStructure/Core Components
Section titled “Core Components”1. ObservablePortGenerator
Section titled “1. ObservablePortGenerator”The main source generator class. It operates as a 2-stage pipeline:
Stage 1: Target Class Filtering================================Selects only classes that have the[GenerateObservablePort] attribute (predefinedin the Functorium library) and implement IObservablePort
Stage 2: Observable Class Generation=====================================Generates wrapper methods that include logging,tracing, and metrics code for each method2. IncrementalGeneratorBase
Section titled “2. IncrementalGeneratorBase”Provides the Template Pattern for incremental source generators. See the Core Design Patterns > Template Method Pattern section for implementation code.
3. Helper Classes
Section titled “3. Helper Classes”| Class | Role |
|---|---|
TypeExtractor | FinT<IO, User> -> User type extraction |
CollectionTypeHelper | List<T>, IEnumerable<T> etc. collection detection |
SymbolDisplayFormats | Deterministic type string generation |
ConstructorParameterExtractor | Constructor parameter analysis |
ParameterNameResolver | logger -> baseLogger name conflict resolution |
Learning Roadmap
Section titled “Learning Roadmap”Part 0: Introduction====================- Source Generator concept, Hello World, project overview
Part 1: Fundamentals====================- Development environment setup- Roslyn Architecture (Syntax API, Semantic API, Symbol)
Part 2: Core Concepts=====================- IIncrementalGenerator, Provider Pattern- ForAttributeWithMetadataName, symbol analysis- StringBuilder code generation, deterministic output
Part 3: Advanced================- Constructor, Generic, Collection handling- LoggerMessage.Define 6-parameter limit- Snapshot testing, 31 test scenarios
Part 4: Cookbook===============- Entity ID, EF Core Value Converter, Validation generators- Custom Generator templateSummary at a Glance
Section titled “Summary at a Glance”ObservablePortGenerator automatically generates and eliminates repetitive logging, tracing, and metrics code in the adapter layer. The reason for choosing a source generator is that it simultaneously meets four requirements: consistency, performance, type safety, and AOT support. With a design combining the Template Method Pattern and the Strategy Pattern, it reduces the amount of code developers write by approximately 75% while guaranteeing 100% consistent observability.
Q1: What are the benefits of using IncrementalGeneratorBase<TValue>?
Section titled “Q1: What are the benefits of using IncrementalGeneratorBase<TValue>?”A: It manages common logic such as debugger attachment, null filtering, and batch processing via Collect() in a single place. When adding a new source generator, you only need to implement the registerSourceProvider and generate functions, eliminating duplication of pipeline configuration code.
Q2: What role does the IObservablePort interface play in the Strategy Pattern?
Section titled “Q2: What role does the IObservablePort interface play in the Strategy Pattern?”A: IObservablePort serves as a marker to identify targets for which the source generator should generate code. Only classes implementing this interface become targets for the [GenerateObservablePort] attribute, and each adapter defines its observability category through the RequestCategory property.
Q3: What is the basis for the claim that code volume decreases by 75% after introducing the source generator?
Section titled “Q3: What is the basis for the claim that code volume decreases by 75% after introducing the source generator?”A: With manual implementation, 30-50 lines of boilerplate for logging, tracing, and metrics are added per method. After introducing the source generator, only pure business logic remains and all cross-cutting concern code is auto-generated, resulting in approximately 75% code reduction based on actual project measurements.
Q4: Why are helper classes like TypeExtractor and CollectionTypeHelper separated into distinct classes?
Section titled “Q4: Why are helper classes like TypeExtractor and CollectionTypeHelper separated into distinct classes?”A: Each helper handles an independent responsibility (type extraction, collection detection, name conflict resolution, etc.). Separating them according to the Single Responsibility Principle allows individual logic to be tested independently and reused across other source generators.
Now that we have grasped the overall picture of the project, it is time to set up the environment for actually developing source generators.