Adapter Integration -- Pipeline and DI
This document is a guide covering Pipeline generation, DI registration, and Options patterns for Adapters. For Port definitions, see 12-ports.md; for Adapter implementation, see 13-adapters.md; for unit testing, see 14b-adapter-testing.md.
Introduction
Section titled “Introduction”“How do we consistently compose cross-cutting concerns such as Use Cases, validation, and transactions?” “Do we need to repeatedly write logging, metrics, and tracing code for each Adapter?” “How do we register Pipeline Observable classes in DI and inject configuration values with the Options pattern?”
Pipeline is Functorium’s core mechanism for handling cross-cutting concerns. Through Observable wrappers generated by the Source Generator, logging, metrics, and tracing are applied automatically and consistently. This document covers the entire process of Adapter integration, from Pipeline generation verification to DI registration and Options pattern usage.
What You Will Learn
Section titled “What You Will Learn”This document covers the following topics:
- Mediator Pipeline Configuration — Structure of Observable wrappers generated by Source Generator and auto-provided features
- DI Registration Patterns — How to map Pipeline to Port interfaces with
RegisterScopedObservablePort - Options Pattern Usage — Strongly-typed configuration binding with
OptionsConfiguratorand startup validation
Prerequisites
Section titled “Prerequisites”A basic understanding of the following concepts is needed to understand this document:
- Port Architecture and Definitions — Port interface design principles
- Adapter Implementation —
[GenerateObservablePort]attribute andvirtualkeyword - Error System: Basics and Naming —
FinT<IO, T>return patterns
Do not write observability code directly in Adapters. A single line of
[GenerateObservablePort]+ DI registration automatically applies logging, metrics, and tracing consistently.
Summary
Section titled “Summary”Key Commands
Section titled “Key Commands”// DI registration (Pipeline -> Port interface)services.RegisterScopedObservablePort<IProductRepository, ProductRepositoryInMemoryObservable>();
// HttpClient registration (External API)services.AddHttpClient<ExternalPricingApiServiceObservable>(client => client.BaseAddress = new Uri(options.BaseUrl));# Verify Pipeline generated filesls {Project}/obj/GeneratedFiles/Functorium.SourceGenerators/.../*.g.csKey Procedures
Section titled “Key Procedures”- Build after applying
[GenerateObservablePort]-> Verify Pipeline class generation inobj/GeneratedFiles/ - Create Registration class (
Adapter{Layer}Registration) - Register in DI by calling
RegisterScopedObservablePort<IPort, ObservableAdapter>() - Call Registration from
Program.cs
Key Concepts
Section titled “Key Concepts”| Concept | Description |
|---|---|
| Pipeline class | Observability wrapper auto-generated by Source Generator ({ClassName}Observable) |
RegisterScopedObservablePort | Extension method for DI-registering Pipeline to Port interface |
| Registration class | Static class that groups DI registrations per Adapter project |
| Options pattern | Strongly-typed configuration binding with OptionsConfigurator<T> |
Note:
UsecaseCachingPipelinedepends onIMemoryCache. When usingUseCaching(), you must registerservices.AddMemoryCache()in the DI container.
First, let’s verify how Pipelines are generated, then proceed sequentially through DI registration and the Options pattern.
Activity 3: Verify Pipeline Generation
Section titled “Activity 3: Verify Pipeline Generation”When an Adapter with the [GenerateObservablePort] attribute is built, the Source Generator automatically creates Pipeline classes.
GenerateObservablePort Source Generator
Section titled “GenerateObservablePort Source Generator”Applying the [GenerateObservablePort] attribute to a class causes the Source Generator to automatically generate a Pipeline wrapper class.
Location: Functorium.Adapters.SourceGenerators.GenerateObservablePortAttribute
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]public sealed class GenerateObservablePortAttribute : Attribute { }Transformation Diagram:
Original Class Generated Pipeline Class┌─────────────────────────────┐ ┌─────────────────────────────────────┐│ [GenerateObservablePort] │ │ ProductRepositoryInMemoryObservable ││ ProductRepositoryInMemory │ ──► │ : ProductRepositoryInMemory ││ : IProductRepository │ │ │├─────────────────────────────┤ ├─────────────────────────────────────┤│ GetById(Guid id) │ │ override GetById(Guid id) ││ GetAll() │ │ + Activity Span creation ││ Create(Product) │ │ + Logging (Debug/Info/Error) │└─────────────────────────────┘ │ + Metrics (Counter/Histogram) │ │ + Error classification │ └─────────────────────────────────────┘Verify Generated Files
Section titled “Verify Generated Files”After building, verify the generated files at the following path.
{Project}/obj/GeneratedFiles/ └── Functorium.SourceGenerators/ └── Functorium.SourceGenerators.Generators.ObservablePortGenerator.ObservablePortGenerator/ └── {Namespace}.{ClassName}Observable.g.csExample:
LayeredArch.Adapters.Persistence/obj/GeneratedFiles/.../ └── Repositories.ProductRepositoryInMemoryObservable.g.cs
LayeredArch.Adapters.Infrastructure/obj/GeneratedFiles/.../ └── ExternalApis.ExternalPricingApiServiceObservable.g.cs
OrderService/obj/GeneratedFiles/.../ └── Messaging.RabbitMqInventoryMessagingObservable.g.csGenerated Code Structure
Section titled “Generated Code Structure”The generated Pipeline class has the following structure.
// Auto-generated code (example structure)public class ProductRepositoryInMemoryObservable : ProductRepositoryInMemory{ private readonly ActivitySource _activitySource; private readonly ILogger<ProductRepositoryInMemoryObservable> _logger; private readonly Histogram<double> _durationHistogram; // ... other Observability fields
public ProductRepositoryInMemoryObservable( ActivitySource activitySource, ILogger<ProductRepositoryInMemoryObservable> logger, IMeterFactory meterFactory, IOptions<OpenTelemetryOptions> openTelemetryOptions /* + original constructor parameters */) : base(/* original constructor parameters */) { // Observability initialization }
public override FinT<IO, Product> Create(Product product) { // Start Activity -> call original method -> record logging/metrics return /* wrapped call */; }}Core Structure:
- Inherits from the original Adapter class (
ProductRepositoryInMemoryObservable : ProductRepositoryInMemory) - Overrides
virtualmethods to add Observability logic - Observability dependencies such as
ActivitySource,ILogger,IMeterFactoryare injected in the constructor - Original constructor parameters are also forwarded
Auto-Provided Features (Summary)
Section titled “Auto-Provided Features (Summary)”Pipelines automatically provide the following observability features. All fields use snake_case + dot notation for consistency with OpenTelemetry semantic conventions.
The following table summarizes the 4 observability features automatically provided by the Pipeline.
| Feature | Description | Key Tags/Fields |
|---|---|---|
| Distributed Tracing | Auto Span creation ({layer} {category} {handler}.{method}) | request.layer, request.category.name, request.handler.name, request.handler.method, response.status, response.elapsed |
| Structured Logging | Auto request/response/error logging (EventId 2001-2004) | Request(Info/Debug), Success(Info/Debug), Warning(Expected), Error(Exceptional) |
| Metrics Collection | Auto Counter + Histogram recording | adapter.{category}.requests, adapter.{category}.responses, adapter.{category}.duration |
| Error Classification | Auto Expected/Exceptional/Aggregate classification | error.type, error.code |
Log Level Rules:
| Event | EventId | Log Level | Condition |
|---|---|---|---|
| Request | 2001 | Information / Debug | Debug includes parameter values |
| Response Success | 2002 | Information / Debug | Debug includes return values |
| Response Warning | 2003 | Warning | error.IsExpected == true |
| Response Error | 2004 | Error | error.IsExceptional == true |
Error Classification Rules:
| Error Case | error.type | error.code | Log Level |
|---|---|---|---|
IHasErrorCode + IsExpected | "expected" | Error code | Warning |
IHasErrorCode + IsExceptional | "exceptional" | Error code | Error |
ManyErrors | "aggregate" | First error code | Warning/Error |
Expected (LanguageExt) | "expected" | Type name | Warning |
Exceptional (LanguageExt) | "exceptional" | Type name | Error |
Detailed Specification: For tracing Tag structure, log Message Template, metrics Instrument definitions, and other details, see 08-observability.md.
Build Error Resolution
Section titled “Build Error Resolution”This table summarizes build errors that can occur during Pipeline generation and their solutions.
| Error | Symptom | Cause | Solution |
|---|---|---|---|
| CS0506 | cannot override because it is not virtual | Missing virtual keyword on method | Add virtual to all interface methods |
| Pipeline class not generated | No file in obj/GeneratedFiles/ | Missing [GenerateObservablePort] attribute | Add attribute to class |
| Constructor parameter conflict | Source Generator error | Constructor parameter type conflicts with Observability types | Use unique types for constructor parameters |
| Missing namespace | using error | Missing Functorium package reference | Add Functorium.SourceGenerators NuGet package |
If the Pipeline was successfully generated, it can now be registered in the DI container for runtime use.
Activity 4: DI Registration
Section titled “Activity 4: DI Registration”Register the generated Pipeline classes in the DI container.
Creating Registration Classes
Section titled “Creating Registration Classes”Location Rule: {Project}.Adapters.{Layer}/Abstractions/Registrations/
Naming Rule: Adapter{Layer}Registration
Pattern for mapping Pipeline Observable classes to Port interfaces with RegisterScopedObservablePort.
// File: {Adapters.Persistence}/Abstractions/Registrations/AdapterPersistenceRegistration.cs
using Functorium.Adapters.Abstractions.Registrations;
public static class AdapterPersistenceRegistration{ public static IServiceCollection RegisterAdapterPersistence( this IServiceCollection services) { // Pipeline registration services.RegisterScopedObservablePort< IProductRepository, ProductRepositoryInMemoryObservable>();
return services; }
public static IApplicationBuilder UseAdapterPersistence( this IApplicationBuilder app) { return app; }}Reference:
Tests.Hosts/01-SingleHost/LayeredArch.Adapters.Persistence/Abstractions/Registrations/AdapterPersistenceRegistration.cs
Note: If an Adapter requires the Options pattern, add an
IConfigurationparameter to the Registration method. See 4.6 Options Pattern.
Registration Patterns by Type
Section titled “Registration Patterns by Type”Repository Registration
Section titled “Repository Registration”// Single interface registrationservices.RegisterScopedObservablePort< IProductRepository, // Port interface ProductRepositoryInMemoryObservable>(); // Generated PipelineUnitOfWork Registration
Section titled “UnitOfWork Registration”// InMemory environmentservices.RegisterScopedObservablePort<IUnitOfWork, UnitOfWorkInMemoryObservable>();
// EF Core environmentservices.RegisterScopedObservablePort<IUnitOfWork, UnitOfWorkEfCoreObservable>();External API Registration
Section titled “External API Registration”External API Adapters require registering both HttpClient and Pipeline.
// Step 1: HttpClient registrationservices.AddHttpClient<ExternalPricingApiServiceObservable>(client =>{ client.BaseAddress = new Uri(configuration["ExternalApi:BaseUrl"] ?? "https://api.example.com"); client.Timeout = TimeSpan.FromSeconds(30);});
// Step 2: Pipeline registrationservices.RegisterScopedObservablePort< IExternalPricingService, ExternalPricingApiServiceObservable>();Note: Register
HttpClientwith the Observable class type. Since Observable inherits from the original Adapter, it receives the constructor’sHttpClientparameter as-is.
HttpClient Lifetime Management:
AddHttpClient<T>()internally usesIHttpClientFactoryto manage the lifetime ofHttpClient. CreatingHttpClientdirectly vianewcan cause socket exhaustion issues, so you must always create it throughIHttpClientFactory.IHttpClientFactoryautomatically handles pooling and lifetime management of internalHttpMessageHandler(default 2-minute rotation), optimizing DNS change reflection and connection pooling.
Messaging Registration
Section titled “Messaging Registration”// Pipeline registration (MessageBus requires separate registration)services.RegisterScopedObservablePort< IInventoryMessaging, RabbitMqInventoryMessagingObservable>();Reference:
Tutorials/Cqrs06Services/Src/OrderService/Program.cs(line 57)
Query Adapter Registration
Section titled “Query Adapter Registration”// InMemory Provider -- Query Adapter Pipeline registrationservices.RegisterScopedObservablePort< IProductQuery, ProductQueryInMemoryObservable>();
// Sqlite Provider -- Dapper Query Adapter Pipeline registrationservices.RegisterScopedObservablePort< IProductQuery, ProductQueryDapperObservable>();Note: Query Adapters use the same
RegisterScopedObservablePortAPI as Repositories. In the Provider branching pattern (4.6), InMemory registers the InMemory Query Adapter, while Sqlite registers the Dapper Query Adapter.
Ctx Enricher Registration
Section titled “Ctx Enricher Registration”Ctx Enrichers are not ICustomUsecasePipeline, so they must be registered separately in DI. When UseObservability() is used, CtxEnricher is automatically activated.
Usecase Ctx Enricher — Enricher auto-generated by Source Generator:
services.AddScoped< IUsecaseCtxEnricher<CreateOrderCommand.Request, FinResponse<CreateOrderCommand.Response>>, CreateOrderCommandRequestCtxEnricher>();Domain Event Ctx Enricher — Enricher auto-generated by DomainEventCtxEnricherGenerator detecting IDomainEventHandler<T>:
services.AddScoped< IDomainEventCtxEnricher<Order.CreatedEvent>, OrderCreatedEventCtxEnricher>();services.AddScoped< IDomainEventCtxEnricher<Customer.CreatedEvent>, CustomerCreatedEventCtxEnricher>();DomainEvent Publisher Registration
Section titled “DomainEvent Publisher Registration”RegisterDomainEventPublisher() registers 3 services in DI: IDomainEventPublisher, IDomainEventCollector, and ObservableDomainEventNotificationPublisher. To enable handler-level observability, NotificationPublisherType must also be configured:
services.AddMediator(options =>{ options.ServiceLifetime = ServiceLifetime.Scoped; options.NotificationPublisherType = typeof(ObservableDomainEventNotificationPublisher);});services.RegisterDomainEventPublisher();Reference: Domain Events - Handler Registration, Logging Manual - IDomainEventCtxEnricher
Multiple Interface Registration
Section titled “Multiple Interface Registration”When a single implementation class implements multiple interfaces:
// 2 interfaces (Scoped example -- Transient/Singleton also support the same For pattern)services.RegisterScopedObservablePortFor<IReadRepository, IWriteRepository, ProductRepositoryObservable>();
// 3 interfacesservices.RegisterScopedObservablePortFor<IService1, IService2, IService3, MyServiceObservable>();
// 4+ interfaces (params Type[] overload)services.RegisterScopedObservablePortFor<MyServiceObservable>( typeof(IService1), typeof(IService2), typeof(IService3), typeof(IService4));Note: The
Forsuffix methods support all three Lifetimes: Scoped, Transient, and Singleton (e.g.,RegisterTransientObservablePortFor,RegisterSingletonObservablePortFor).
DI Lifetime Selection Guide
Section titled “DI Lifetime Selection Guide”The following table summarizes when to use each Lifetime and their caveats.
| Lifetime | When to Use | Caveats |
|---|---|---|
| Scoped (default) | Repository, External API, Messaging | Same instance shared within HTTP request |
| Transient | Stateless lightweight Adapters | New instance created each time (watch memory) |
| Singleton | Thread-safe read-only Adapters | No state changes, thread safety must be guaranteed |
Recommendation: Use Scoped unless there is a specific reason not to.
Registration API Summary:
| Registration API | Lifetime | Purpose |
|---|---|---|
RegisterScopedObservablePort<TService, TImpl>() | Scoped | One per HTTP request (default recommendation) |
RegisterTransientObservablePort<TService, TImpl>() | Transient | New instance per request |
RegisterSingletonObservablePort<TService, TImpl>() | Singleton | Single instance for entire application |
Register{Lifetime}ObservablePortFor<T1, T2, TImpl>() | Scoped/Transient/Singleton | 2 interfaces -> 1 implementation |
Register{Lifetime}ObservablePortFor<T1, T2, T3, TImpl>() | Scoped/Transient/Singleton | 3 interfaces -> 1 implementation |
Register{Lifetime}ObservablePortFor<TImpl>(params Type[]) | Scoped/Transient/Singleton | 4+ interfaces -> 1 implementation |
Reference:
Src/Functorium/Abstractions/Registrations/ObservablePortRegistration.cs
Host Bootstrap Integration
Section titled “Host Bootstrap Integration”Call per-layer Registrations from Program.cs.
// File: {Host}/Program.cs
var builder = WebApplication.CreateBuilder(args);
// Per-layer service registrationbuilder.Services .RegisterAdapterPresentation() .RegisterAdapterPersistence(builder.Configuration) // Pass IConfiguration when using Options pattern .RegisterAdapterInfrastructure(builder.Configuration);
var app = builder.Build();
app.UseAdapterInfrastructure() .UseAdapterPersistence() .UseAdapterPresentation();
app.Run();Reference:
Tests.Hosts/01-SingleHost/LayeredArch/Program.cs
Key Points:
RegisterAdapter{Layer}(): Service registration viaIServiceCollectionextension methodsUseAdapter{Layer}(): Middleware configuration viaIApplicationBuilderextension methods- Registration order is determined by dependency direction (Presentation -> Persistence -> Infrastructure)
- Adapters using the Options pattern receive an
IConfigurationparameter (see 4.6)
Note: Registration order is irrelevant to DI container dependency resolution; Domain -> Adapter -> Infrastructure order is recommended for readability.
Note: For the rationale behind registration order and environment-specific configuration branching, see 01-project-structure.md — Host Project.
Options Pattern (OptionsConfigurator)
Section titled “Options Pattern (OptionsConfigurator)”Now that you understand the basic DI registration patterns, let’s learn how to inject configuration options into Adapters. The OptionsConfigurator pattern is used. It reads settings from appsettings.json, validates with FluentValidation at startup, and automatically outputs to StartupLogger.
Options Class Structure
Section titled “Options Class Structure”// File: {Adapters.Persistence}/Abstractions/Options/PersistenceOptions.cs
using FluentValidation;using Functorium.Adapters.Observabilities.Loggers;using Microsoft.Extensions.Logging;
public sealed class PersistenceOptions : IStartupOptionsLogger{ public const string SectionName = "Persistence"; // appsettings.json section name
public string Provider { get; set; } = "InMemory"; public string ConnectionString { get; set; } = "Data Source=layeredarch.db";
public static readonly string[] SupportedProviders = ["InMemory", "Sqlite"];
// IStartupOptionsLogger -- auto-logging at startup public void LogConfiguration(ILogger logger) { const int labelWidth = 20; logger.LogInformation("Persistence Configuration"); logger.LogInformation(" {Label}: {Value}", "Provider".PadRight(labelWidth), Provider); if (Provider == "Sqlite") logger.LogInformation(" {Label}: {Value}", "ConnectionString".PadRight(labelWidth), ConnectionString); }
// FluentValidation -- auto-validation at startup public sealed class Validator : AbstractValidator<PersistenceOptions> { public Validator() { RuleFor(x => x.Provider) .NotEmpty() .Must(p => SupportedProviders.Contains(p)) .WithMessage($"{nameof(Provider)} must be one of: {string.Join(", ", SupportedProviders)}");
RuleFor(x => x.ConnectionString) .NotEmpty() .When(x => x.Provider == "Sqlite") .WithMessage($"{nameof(ConnectionString)} is required when Provider is 'Sqlite'."); } }}Reference:
Tests.Hosts/01-SingleHost/LayeredArch.Adapters.Persistence/Abstractions/Options/PersistenceOptions.cs
Options Class Checklist
Section titled “Options Class Checklist”- Declared as
sealed class -
SectionNameconstant defined (appsettings.json section name) - Implements
IStartupOptionsLogger(LogConfigurationmethod) - Nested
Validatorclass (inheritsAbstractValidator<TOptions>) - Location:
{Adapter}/Abstractions/Options/
Options Registration in Registration Class
Section titled “Options Registration in Registration Class”// Options registration (completed in 1 line)services.RegisterConfigureOptions<PersistenceOptions, PersistenceOptions.Validator>( PersistenceOptions.SectionName);Items automatically handled by RegisterConfigureOptions:
| Item | Description |
|---|---|
| Options binding | SectionName in appsettings.json -> Options property mapping (BindConfiguration) |
IValidator<TOptions> registration | Registers TValidator as Scoped in DI |
| FluentValidation connection | Connects IValidateOptions<TOptions> via AddValidateFluentValidation() |
ValidateOnStart() | Validates at program startup (terminates immediately on failure) |
IStartupOptionsLogger auto-registration | Checks typeof(IStartupOptionsLogger).IsAssignableFrom(typeof(TOptions)), auto-outputs to StartupLogger when implemented |
API Signature:
public static OptionsBuilder<TOptions> RegisterConfigureOptions<TOptions, TValidator>( this IServiceCollection services, string configurationSectionName) where TOptions : class where TValidator : class, IValidator<TOptions>- Return type:
OptionsBuilder<TOptions>(additional chaining possible) - Constraints:
TOptions : class,TValidator : class, IValidator<TOptions>
Reference:
Src/Functorium.Adapters/Options/OptionsConfigurator.cs
IStartupOptionsLogger Interface
Section titled “IStartupOptionsLogger Interface”When an Options class implements IStartupOptionsLogger, RegisterConfigureOptions automatically registers it as IStartupOptionsLogger in DI. StartupLogger receives IEnumerable<IStartupOptionsLogger> and calls each Options’ LogConfiguration() at application startup.
public interface IStartupOptionsLogger{ void LogConfiguration(ILogger logger);}Log Output Format:
Main Topic Configuration <= empty line Subtopic 1 Label1: Value1 Label2: Value2Rules:
- Align labels with
PadRight(20) - Mask sensitive information (passwords, API keys)
- Use structured logging template
{Label}: {Value}
Validator Class Pattern
Section titled “Validator Class Pattern”Define a nested Validator class inside the Options class.
public sealed class Validator : AbstractValidator<PersistenceOptions>{ public Validator() { RuleFor(x => x.Provider) .NotEmpty() .Must(p => SupportedProviders.Contains(p)) .WithMessage($"{nameof(Provider)} must be one of: {string.Join(", ", SupportedProviders)}");
RuleFor(x => x.ConnectionString) .NotEmpty() .When(x => x.Provider == "Sqlite") .WithMessage($"{nameof(ConnectionString)} is required when Provider is 'Sqlite'."); }}Key Validation Methods:
| Method | Purpose |
|---|---|
NotEmpty() | Required value |
InclusiveBetween() | Range validation |
Must() | Custom rules (SmartEnum, etc.) |
When() | Conditional validation |
Matches() | Regex validation |
Provider Branching Registration Pattern
Section titled “Provider Branching Registration Pattern”This pattern registers different Adapter implementations in DI based on Options values.
public static IServiceCollection RegisterAdapterPersistence( this IServiceCollection services, IConfiguration configuration){ // 1. Options registration services.RegisterConfigureOptions<PersistenceOptions, PersistenceOptions.Validator>( PersistenceOptions.SectionName);
// 2. Read Provider at startup var options = configuration .GetSection(PersistenceOptions.SectionName) .Get<PersistenceOptions>() ?? new PersistenceOptions();
// 3. Branch registration based on Provider switch (options.Provider) { case "Sqlite": services.AddDbContext<LayeredArchDbContext>(opt => opt.UseSqlite(options.ConnectionString)); RegisterSqliteRepositories(services); // Command side: EF Core RegisterDapperQueries(services, options.ConnectionString); // Query side: Dapper break;
case "InMemory": default: RegisterInMemoryRepositories(services); // Both Command + Query as InMemory break; }
return services;}Reference:
Tests.Hosts/01-SingleHost/LayeredArch.Adapters.Persistence/Abstractions/Registrations/AdapterPersistenceRegistration.cs
Initialization in UseAdapter{Layer}
Section titled “Initialization in UseAdapter{Layer}”public static IApplicationBuilder UseAdapterPersistence(this IApplicationBuilder app){ var options = app.ApplicationServices .GetRequiredService<IOptions<PersistenceOptions>>().Value;
if (options.Provider == "Sqlite") { using var scope = app.ApplicationServices.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<LayeredArchDbContext>(); dbContext.Database.EnsureCreated(); }
return app;}appsettings.json Configuration
Section titled “appsettings.json Configuration”SectionName <-> JSON Key Mapping:
| Options Class | SectionName | appsettings.json Key |
|---|---|---|
PersistenceOptions | "Persistence" | "Persistence": { ... } |
OpenTelemetryOptions | "OpenTelemetry" | "OpenTelemetry": { ... } |
Rule: The
SectionNameconstant value of the Options class must exactly match the top-level key in appsettings.json.
Define sections in appsettings.json that match the SectionName constant value of the Options class. For environment-specific overrides, see the ASP.NET Core Configuration documentation. For integration test appsettings configuration, see 16-testing-library.md.
Provider Choices:
| Provider | Command Side | Query Side | Purpose |
|---|---|---|---|
"InMemory" | ConcurrentDictionary | InMemory Query Adapter | Development/testing (default) |
"Sqlite" | EF Core (SQLite) | Dapper (SQLite) | Local persistence |
Troubleshooting
Section titled “Troubleshooting”CS0506 Error Due to Missing virtual Keyword
Section titled “CS0506 Error Due to Missing virtual Keyword”Cause: Since Pipeline classes inherit from the original class and override methods, a compile error occurs without virtual.
Resolution: Add the virtual keyword to all interface methods.
// Goodpublic virtual FinT<IO, Product> GetById(Guid id) { ... }
// Bad - CS0506public FinT<IO, Product> GetById(Guid id) { ... }Pipeline Class Not Generated
Section titled “Pipeline Class Not Generated”Cause: The [GenerateObservablePort] attribute is missing, or dotnet build was not run so the Source Generator was not triggered.
Resolution:
- Add the
[GenerateObservablePort]attribute to the class. - Run
dotnet buildto trigger the Source Generator. - Verify that
XxxObservable.g.cswas generated inobj/GeneratedFiles/.
Pipeline Not Registered in DI (InvalidOperationException)
Section titled “Pipeline Not Registered in DI (InvalidOperationException)”Cause: A No service for type 'IXxx' exception occurs because the Pipeline Observable class was not registered in DI.
Resolution:
// Register in the Registration classservices.RegisterScopedObservablePort<IXxx, XxxObservable>();For the complete problem-symptom-resolution list, see Appendix Quick Reference Checklist.
Q1. Why must methods be declared as virtual?
Section titled “Q1. Why must methods be declared as virtual?”Because Pipeline classes inherit from the original class and override methods. Without virtual, the Pipeline cannot wrap the method.
// Goodpublic virtual FinT<IO, Product> GetById(Guid id) { ... }
// Bad - Pipeline cannot overridepublic FinT<IO, Product> GetById(Guid id) { ... }Q2. Can I use Task<T> instead of FinT<IO, T>?
Section titled “Q2. Can I use Task<T> instead of FinT<IO, T>?”Pipelines expect the FinT<IO, T> return type. This type is used for functional error handling and composition.
// Synchronous operationpublic virtual FinT<IO, Product> GetById(Guid id){ return IO.lift(() => { if (_products.TryGetValue(id, out var product)) return Fin.Succ(product); return Fin.Fail<Product>(Error.New("Not found")); });}
// Asynchronous operationpublic virtual FinT<IO, Product> GetByIdAsync(Guid id){ return IO.liftAsync(async () => { var product = await _dbContext.Products.FindAsync(id); return product is not null ? Fin.Succ(product) : Fin.Fail<Product>(Error.New("Not found")); });}Q4. What happens when there are too many constructor parameters?
Section titled “Q4. What happens when there are too many constructor parameters?”Pipelines automatically include the original class’s constructor parameters. If there are multiple parameters of the same type, the Source Generator will raise an error.
// Bad - same type parameter conflictpublic ProductRepositoryInMemory( ILogger<ProductRepositoryInMemory> logger1, ILogger<ProductRepositoryInMemory> logger2) // Type conflict!
// Good - each parameter is a unique typepublic ProductRepositoryInMemory( ILogger<ProductRepositoryInMemory> logger, IOptions<RepositoryOptions> options)Q5. Can specific methods be excluded from the Pipeline?
Section titled “Q5. Can specific methods be excluded from the Pipeline?”Yes, using the [ObservablePortIgnore] attribute, specific methods can be excluded from Pipeline wrapper generation.
[GenerateObservablePort]public class MyAdapter : IMyPort{ public virtual FinT<IO, Product> GetById(ProductId id) { ... } // Wrapped by Pipeline
[ObservablePortIgnore] public virtual FinT<IO, Unit> InternalCleanup() { ... } // Excluded from Pipeline}- Methods with
[ObservablePortIgnore]will not haveoverridewrappers generated by the Source Generator. - Logging, metrics, and tracing will not be recorded when these methods are called.
- Use for internal utility methods or methods where Observability is unnecessary.
Q7. How do you determine the RequestCategory value?
Section titled “Q7. How do you determine the RequestCategory value?”RequestCategory is a classification tag used in the Observability Pipeline’s metrics/tracing. There are no reserved keywords defined by the framework; consistent naming within the team is what matters.
| Recommended Value | Purpose |
|---|---|
"Repository" | Aggregate CRUD persistence |
"UnitOfWork" | Transaction commit |
"QueryAdapter" | Read-only queries (direct DTO return) |
"ExternalApi" | External HTTP API calls |
"Messaging" | Message queue communication |
References
Section titled “References”| Document | Description |
|---|---|
| 04-ddd-tactical-overview.md | Domain modeling overview |
| 05a-value-objects.md | Value Object implementation guide |
| 06b-entity-aggregate-core.md | Entity/Aggregate core patterns |
| 11-usecases-and-cqrs.md | Use case implementation (CQRS Command/Query) |
| 08a-error-system.md | Error system: Basics and naming |
| 08b-error-system-domain-app.md | Error system: Domain/Application errors |
| 08c-error-system-adapter-testing.md | Error system: Adapter errors and testing |
| 12-ports.md | Port definition guide |
| 13-adapters.md | Adapter implementation guide |
| 14b-adapter-testing.md | Adapter unit testing guide |
| 15a-unit-testing.md | Unit testing guide |
| 08-observability.md | Observability specification (tracing, logging, metrics details) |
| 01-project-structure.md | Service project structure guide |
External References:
- OpenTelemetry .NET - Distributed tracing
- LanguageExt - Functional programming library