Skip to content

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.

“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.

This document covers the following topics:

  1. Mediator Pipeline Configuration — Structure of Observable wrappers generated by Source Generator and auto-provided features
  2. DI Registration Patterns — How to map Pipeline to Port interfaces with RegisterScopedObservablePort
  3. Options Pattern Usage — Strongly-typed configuration binding with OptionsConfigurator and startup validation

A basic understanding of the following concepts is needed to understand this document:

Do not write observability code directly in Adapters. A single line of [GenerateObservablePort] + DI registration automatically applies logging, metrics, and tracing consistently.

// DI registration (Pipeline -> Port interface)
services.RegisterScopedObservablePort<IProductRepository, ProductRepositoryInMemoryObservable>();
// HttpClient registration (External API)
services.AddHttpClient<ExternalPricingApiServiceObservable>(client =>
client.BaseAddress = new Uri(options.BaseUrl));
Terminal window
# Verify Pipeline generated files
ls {Project}/obj/GeneratedFiles/Functorium.SourceGenerators/.../*.g.cs
  1. Build after applying [GenerateObservablePort] -> Verify Pipeline class generation in obj/GeneratedFiles/
  2. Create Registration class (Adapter{Layer}Registration)
  3. Register in DI by calling RegisterScopedObservablePort<IPort, ObservableAdapter>()
  4. Call Registration from Program.cs
ConceptDescription
Pipeline classObservability wrapper auto-generated by Source Generator ({ClassName}Observable)
RegisterScopedObservablePortExtension method for DI-registering Pipeline to Port interface
Registration classStatic class that groups DI registrations per Adapter project
Options patternStrongly-typed configuration binding with OptionsConfigurator<T>

Note: UsecaseCachingPipeline depends on IMemoryCache. When using UseCaching(), you must register services.AddMemoryCache() in the DI container.

First, let’s verify how Pipelines are generated, then proceed sequentially through DI registration and the Options pattern.


When an Adapter with the [GenerateObservablePort] attribute is built, the Source Generator automatically creates Pipeline classes.

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 │
└─────────────────────────────────────┘

After building, verify the generated files at the following path.

{Project}/obj/GeneratedFiles/
└── Functorium.SourceGenerators/
└── Functorium.SourceGenerators.Generators.ObservablePortGenerator.ObservablePortGenerator/
└── {Namespace}.{ClassName}Observable.g.cs

Example:

LayeredArch.Adapters.Persistence/obj/GeneratedFiles/.../
└── Repositories.ProductRepositoryInMemoryObservable.g.cs
LayeredArch.Adapters.Infrastructure/obj/GeneratedFiles/.../
└── ExternalApis.ExternalPricingApiServiceObservable.g.cs
OrderService/obj/GeneratedFiles/.../
└── Messaging.RabbitMqInventoryMessagingObservable.g.cs

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 virtual methods to add Observability logic
  • Observability dependencies such as ActivitySource, ILogger, IMeterFactory are injected in the constructor
  • Original constructor parameters are also forwarded

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.

FeatureDescriptionKey Tags/Fields
Distributed TracingAuto Span creation ({layer} {category} {handler}.{method})request.layer, request.category.name, request.handler.name, request.handler.method, response.status, response.elapsed
Structured LoggingAuto request/response/error logging (EventId 2001-2004)Request(Info/Debug), Success(Info/Debug), Warning(Expected), Error(Exceptional)
Metrics CollectionAuto Counter + Histogram recordingadapter.{category}.requests, adapter.{category}.responses, adapter.{category}.duration
Error ClassificationAuto Expected/Exceptional/Aggregate classificationerror.type, error.code

Log Level Rules:

EventEventIdLog LevelCondition
Request2001Information / DebugDebug includes parameter values
Response Success2002Information / DebugDebug includes return values
Response Warning2003Warningerror.IsExpected == true
Response Error2004Errorerror.IsExceptional == true

Error Classification Rules:

Error Caseerror.typeerror.codeLog Level
IHasErrorCode + IsExpected"expected"Error codeWarning
IHasErrorCode + IsExceptional"exceptional"Error codeError
ManyErrors"aggregate"First error codeWarning/Error
Expected (LanguageExt)"expected"Type nameWarning
Exceptional (LanguageExt)"exceptional"Type nameError

Detailed Specification: For tracing Tag structure, log Message Template, metrics Instrument definitions, and other details, see 08-observability.md.

This table summarizes build errors that can occur during Pipeline generation and their solutions.

ErrorSymptomCauseSolution
CS0506cannot override because it is not virtualMissing virtual keyword on methodAdd virtual to all interface methods
Pipeline class not generatedNo file in obj/GeneratedFiles/Missing [GenerateObservablePort] attributeAdd attribute to class
Constructor parameter conflictSource Generator errorConstructor parameter type conflicts with Observability typesUse unique types for constructor parameters
Missing namespaceusing errorMissing Functorium package referenceAdd Functorium.SourceGenerators NuGet package

If the Pipeline was successfully generated, it can now be registered in the DI container for runtime use.


Register the generated Pipeline classes in the DI container.

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 IConfiguration parameter to the Registration method. See 4.6 Options Pattern.

// Single interface registration
services.RegisterScopedObservablePort<
IProductRepository, // Port interface
ProductRepositoryInMemoryObservable>(); // Generated Pipeline
// InMemory environment
services.RegisterScopedObservablePort<IUnitOfWork, UnitOfWorkInMemoryObservable>();
// EF Core environment
services.RegisterScopedObservablePort<IUnitOfWork, UnitOfWorkEfCoreObservable>();

External API Adapters require registering both HttpClient and Pipeline.

// Step 1: HttpClient registration
services.AddHttpClient<ExternalPricingApiServiceObservable>(client =>
{
client.BaseAddress = new Uri(configuration["ExternalApi:BaseUrl"]
?? "https://api.example.com");
client.Timeout = TimeSpan.FromSeconds(30);
});
// Step 2: Pipeline registration
services.RegisterScopedObservablePort<
IExternalPricingService,
ExternalPricingApiServiceObservable>();

Note: Register HttpClient with the Observable class type. Since Observable inherits from the original Adapter, it receives the constructor’s HttpClient parameter as-is.

HttpClient Lifetime Management: AddHttpClient<T>() internally uses IHttpClientFactory to manage the lifetime of HttpClient. Creating HttpClient directly via new can cause socket exhaustion issues, so you must always create it through IHttpClientFactory. IHttpClientFactory automatically handles pooling and lifetime management of internal HttpMessageHandler (default 2-minute rotation), optimizing DNS change reflection and connection pooling.

// Pipeline registration (MessageBus requires separate registration)
services.RegisterScopedObservablePort<
IInventoryMessaging,
RabbitMqInventoryMessagingObservable>();

Reference: Tutorials/Cqrs06Services/Src/OrderService/Program.cs (line 57)

// InMemory Provider -- Query Adapter Pipeline registration
services.RegisterScopedObservablePort<
IProductQuery,
ProductQueryInMemoryObservable>();
// Sqlite Provider -- Dapper Query Adapter Pipeline registration
services.RegisterScopedObservablePort<
IProductQuery,
ProductQueryDapperObservable>();

Note: Query Adapters use the same RegisterScopedObservablePort API as Repositories. In the Provider branching pattern (4.6), InMemory registers the InMemory Query Adapter, while Sqlite registers the Dapper Query Adapter.

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>();

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

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 interfaces
services.RegisterScopedObservablePortFor<IService1, IService2, IService3, MyServiceObservable>();
// 4+ interfaces (params Type[] overload)
services.RegisterScopedObservablePortFor<MyServiceObservable>(
typeof(IService1), typeof(IService2), typeof(IService3), typeof(IService4));

Note: The For suffix methods support all three Lifetimes: Scoped, Transient, and Singleton (e.g., RegisterTransientObservablePortFor, RegisterSingletonObservablePortFor).

The following table summarizes when to use each Lifetime and their caveats.

LifetimeWhen to UseCaveats
Scoped (default)Repository, External API, MessagingSame instance shared within HTTP request
TransientStateless lightweight AdaptersNew instance created each time (watch memory)
SingletonThread-safe read-only AdaptersNo state changes, thread safety must be guaranteed

Recommendation: Use Scoped unless there is a specific reason not to.

Registration API Summary:

Registration APILifetimePurpose
RegisterScopedObservablePort<TService, TImpl>()ScopedOne per HTTP request (default recommendation)
RegisterTransientObservablePort<TService, TImpl>()TransientNew instance per request
RegisterSingletonObservablePort<TService, TImpl>()SingletonSingle instance for entire application
Register{Lifetime}ObservablePortFor<T1, T2, TImpl>()Scoped/Transient/Singleton2 interfaces -> 1 implementation
Register{Lifetime}ObservablePortFor<T1, T2, T3, TImpl>()Scoped/Transient/Singleton3 interfaces -> 1 implementation
Register{Lifetime}ObservablePortFor<TImpl>(params Type[])Scoped/Transient/Singleton4+ interfaces -> 1 implementation

Reference: Src/Functorium/Abstractions/Registrations/ObservablePortRegistration.cs

Call per-layer Registrations from Program.cs.

// File: {Host}/Program.cs
var builder = WebApplication.CreateBuilder(args);
// Per-layer service registration
builder.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 via IServiceCollection extension methods
  • UseAdapter{Layer}(): Middleware configuration via IApplicationBuilder extension methods
  • Registration order is determined by dependency direction (Presentation -> Persistence -> Infrastructure)
  • Adapters using the Options pattern receive an IConfiguration parameter (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.

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.

// 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

  • Declared as sealed class
  • SectionName constant defined (appsettings.json section name)
  • Implements IStartupOptionsLogger (LogConfiguration method)
  • Nested Validator class (inherits AbstractValidator<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:

ItemDescription
Options bindingSectionName in appsettings.json -> Options property mapping (BindConfiguration)
IValidator<TOptions> registrationRegisters TValidator as Scoped in DI
FluentValidation connectionConnects IValidateOptions<TOptions> via AddValidateFluentValidation()
ValidateOnStart()Validates at program startup (terminates immediately on failure)
IStartupOptionsLogger auto-registrationChecks 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

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: Value2

Rules:

  • Align labels with PadRight(20)
  • Mask sensitive information (passwords, API keys)
  • Use structured logging template {Label}: {Value}

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:

MethodPurpose
NotEmpty()Required value
InclusiveBetween()Range validation
Must()Custom rules (SmartEnum, etc.)
When()Conditional validation
Matches()Regex validation

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

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;
}

SectionName <-> JSON Key Mapping:

Options ClassSectionNameappsettings.json Key
PersistenceOptions"Persistence""Persistence": { ... }
OpenTelemetryOptions"OpenTelemetry""OpenTelemetry": { ... }

Rule: The SectionName constant 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:

ProviderCommand SideQuery SidePurpose
"InMemory"ConcurrentDictionaryInMemory Query AdapterDevelopment/testing (default)
"Sqlite"EF Core (SQLite)Dapper (SQLite)Local persistence

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.

// Good
public virtual FinT<IO, Product> GetById(Guid id) { ... }
// Bad - CS0506
public FinT<IO, Product> GetById(Guid id) { ... }

Cause: The [GenerateObservablePort] attribute is missing, or dotnet build was not run so the Source Generator was not triggered.

Resolution:

  1. Add the [GenerateObservablePort] attribute to the class.
  2. Run dotnet build to trigger the Source Generator.
  3. Verify that XxxObservable.g.cs was generated in obj/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 class
services.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.

// Good
public virtual FinT<IO, Product> GetById(Guid id) { ... }
// Bad - Pipeline cannot override
public 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 operation
public 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 operation
public 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 conflict
public ProductRepositoryInMemory(
ILogger<ProductRepositoryInMemory> logger1,
ILogger<ProductRepositoryInMemory> logger2) // Type conflict!
// Good - each parameter is a unique type
public 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 have override wrappers 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 ValuePurpose
"Repository"Aggregate CRUD persistence
"UnitOfWork"Transaction commit
"QueryAdapter"Read-only queries (direct DTO return)
"ExternalApi"External HTTP API calls
"Messaging"Message queue communication

DocumentDescription
04-ddd-tactical-overview.mdDomain modeling overview
05a-value-objects.mdValue Object implementation guide
06b-entity-aggregate-core.mdEntity/Aggregate core patterns
11-usecases-and-cqrs.mdUse case implementation (CQRS Command/Query)
08a-error-system.mdError system: Basics and naming
08b-error-system-domain-app.mdError system: Domain/Application errors
08c-error-system-adapter-testing.mdError system: Adapter errors and testing
12-ports.mdPort definition guide
13-adapters.mdAdapter implementation guide
14b-adapter-testing.mdAdapter unit testing guide
15a-unit-testing.mdUnit testing guide
08-observability.mdObservability specification (tracing, logging, metrics details)
01-project-structure.mdService project structure guide

External References: