Skip to main content

Troubleshooting & FAQ

Common Issues​

Send<TRequest, TResponse>() throws InvalidOperationException: No handler registered​

Cause: No IRequestHandler<TRequest, TResponse> (or IRequestHandler<TRequest>) was registered when the request was sent. Since v10.2.0 handlers are auto-registered by the source generator, so the usual culprits are:

  1. AddMediator() was never called — it applies the generator's registrations to the service collection.
  2. The handler lives in a class library that loads lazily — auto-registration only covers the assembly that calls AddMediator() plus any assembly loaded before that call (see the Auto Registration guide).
  3. The handler was skipped by the generator with a warning (FEAG002 generic handler) — register it explicitly.

Fix: Verify the request implements IRequest<TResponse> with a matching handler, and register the handler explicitly when auto-registration cannot cover it:

// ✅ Correct — all three type parameters must match the handler
builder.Services
.AddFeature<CreateProductCommand, int, CreateProductHandler>();

// ❌ Wrong — this overload registers a void-request handler; the handler
// implements IRequestHandler<CreateProductCommand, int>
builder.Services
.AddFeature<CreateProductCommand, CreateProductHandler>();

// ❌ Wrong — request doesn't implement IRequest<int>
public record CreateProductCommand(string Name, decimal Price); // Missing : IRequest<int>

Also verify that CreateProductCommand implements IRequest<int> — the TResponse type parameter on IRequest<TResponse> must match the second type argument to AddFeature. Two handlers for the same request pair in one assembly are a compile-time error (FEAG005).

My handler's HandleAsync override doesn't compile — "no suitable method found to override"​

Cause: The abstract method on RequestHandler<TRequest, TResponse> returns Task<TResponse> where TResponse is the plain response type. If you carried the v10-era Result<T> wrapper into the base type, the signature no longer matches.

Fix: Since v10.2.0 TResponse is your plain return type — return the value directly and throw typed exceptions on failure:

// ❌ Wrong (v10-era) — Result<int> was the response type; exceptions were returned as failures
public class CreateProductHandler : RequestHandler<CreateProductCommand, Result<int>>
{
public override async Task<Result<int>> HandleAsync(...)
{
return new ConflictException(nameof(Product), command.Name); // no implicit conversion anymore
}
}

// ✅ Correct (v11) — TResponse is the plain value type; failures are thrown
public class CreateProductHandler : RequestHandler<CreateProductCommand, int>
{
public override Task<int> HandleAsync(CreateProductCommand command, CancellationToken ct) { ... }
}

Your override must return Task<TResponse> — never Task<Result<TResponse>> — and the request declaration must match: IRequest<int>, not IRequest<Result<int>>.

Publish is swallowing my notification handler exceptions​

Cause: This is intentional behavior as of v10.0.2. One failing handler no longer prevents other handlers from executing.

Fix: Exceptions are logged at Error level with the handler type, notification type, and notification payload. Check your logs for [Publish] Handler {HandlerType} failed for notification {NotificationType}.

If you need the publisher to know about failures, consider implementing a compensating notification or using a separate error-collection mechanism within your handlers.

My pipeline behaviors aren't executing in the order I expect​

Cause: Behaviors execute in reverse registration order — the first registered behavior is outermost, the last registered is innermost (closest to the handler).

builder.Services
.AddFeature<MyCommand, int, MyHandler>()
.AddDecoration<LoggingBehavior<MyCommand, int>>() // Registered 1st → outermost
.AddValidator<MyCommandValidator>(); // Registered 2nd → innermost

// Execution order: Logging → Validation → Handler

Fix: Register behaviors from outermost to innermost. If you want validation to run before logging (so you don't log invalid requests), swap the order:

builder.Services
.AddFeature<MyCommand, int, MyHandler>()
.AddValidator<MyCommandValidator>() // Registered 1st → outermost
.AddDecoration<LoggingBehavior<MyCommand, int>>(); // Registered 2nd → innermost

// Execution order: Validation → Logging → Handler

AddValidator<T>() is registering a behavior — why?​

Cause: AddValidator<TValidator>() is a convenience method that does two things:

  1. Registers the validator: services.AddTransient<IValidator<TRequest>, TValidator>()
  2. Registers the validation behavior: services.AddTransient<IPipelineBehavior<TRequest, TResponse>, ValidationBehavior<TRequest, TResponse>>()

Fix: This is by design. If you only want to register the validator without the behavior, use AddTransient directly:

// Register validator only (no automatic validation pipeline)
builder.Services.AddTransient<IValidator<CreateProductCommand>, CreateProductCommandValidator>();

// Then register the feature without calling AddValidator
builder.Services
.AddFeature<CreateProductCommand, int, CreateProductHandler>()
.AddDecoration<LoggingBehavior<CreateProductCommand, int>>();

My notification handlers are registered but Publish doesn't invoke them​

Cause: The mediator resolves handlers via GetServices<INotificationHandler<TNotification>>(), so the handler genuinely isn't registered when Publish runs. With v11 auto-registration this usually means the handler's assembly was loaded after AddMediator() was called (module initializers run at assembly load), or the handler was skipped by the generator (FEAG002 generic, FEAG003 inaccessible).

Fix: Register the handler explicitly so it is registered regardless of assembly-loading order:

// ✅ Explicit builder registration — always applied when this code runs
builder.Services
.AddNotification<OrderPlaced>()
.AddHandler<SendConfirmationEmail>()
.AddHandler<UpdateInventory>();

Direct DI registration works too — the mediator does not distinguish where a registration came from:

builder.Services.AddTransient<INotificationHandler<OrderPlaced>, SendConfirmationEmail>();

Also verify that AddMediator() was called before publishing — it is the call that applies the auto-registered handlers.

No request logging appears — the RequestHandler base class doesn't log anymore​

Cause: Since v10.2.0 the RequestHandler base classes are pure passthroughs — they no longer accept an ILogger, no longer log request start/finish, and no longer catch exceptions.

Fix: Register the LoggingBehavior on the feature when you want request logging. It logs the request and the response (or just the request, for the no-response variant):

builder.Services
.AddFeature<MyCommand, int, MyHandler>()
.AddDecoration<LoggingBehavior<MyCommand, int>>();

// Logs:
// [Request Logging] Start. Request: {@Request}
// [Request Logging] Finished. Response: {@Response}

Exceptions are no longer swallowed by a base class either: unhandled exceptions propagate to the API layer's GlobalExceptionHandler, which maps them to HTTP responses.

I'm getting InvalidOperationException when registering the same request type twice​

Cause: Each IRequest type can only have one handler. Two handlers implementing the same request handler pair in one assembly are now a compile-time error (FEAG005, reported by the auto-registration generator). At runtime, the mediator resolves IRequestHandler<TRequest, TResponse> as a single service.

Fix: A command or query should have exactly one handler. If you need multiple behaviors for the same request, chain them on the same builder:

// ✅ One handler, multiple behaviors
builder.Services
.AddFeature<CreateProductCommand, int, CreateProductHandler>()
.AddDecoration<LoggingBehavior<CreateProductCommand, int>>()
.AddValidator<CreateProductCommandValidator>();

If you genuinely need different behavior chains for the same request type, use a decorator pattern in your handler or split the logic into multiple requests.

Mediator is registered as Scoped but my notification handler is Singleton​

Cause: The Mediator class is registered as Scoped, which is the correct lifetime for resolving scoped services like IServiceProvider and IRequestHandler instances. Notification handlers are registered as Transient by AddHandler<T>().

Fix: If you need a notification handler with a longer lifetime, register it directly instead of using the fluent builder:

// Singleton notification handler
builder.Services.AddSingleton<INotificationHandler<OrderPlaced>, SendConfirmationEmail>();

But generally, keep handlers as Transient (the default) to avoid captive dependency issues.

My context-aware behavior doesn't see the data set by an earlier behavior​

Cause: Your behavior is overriding the non-context HandleAsync method, which creates a new empty PipelineContext<MyContext>() rather than forwarding the one received from the pipeline.

Fix: Always call the context-aware overload from the non-context overload by forwarding the context:

// ✅ Correct — forwards context from the pipeline
public Task<TResponse> HandleAsync(
TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
=> HandleAsync(request, _ => next(), new PipelineContext<MyContext>(), ct);

The new PipelineContext<MyContext>() here is only used as the initial context when none exists. The actual context from upstream behaviors is passed to the context-aware overload.

PipelineContext<T> disappears between my behavior and handler​

Cause: Previous versions of this package used a dictionary-based PipelineContext. If you have a mix of old and new behavior code, the old non-generic API doesn't forward the typed context.

Fix: Ensure all behaviors in the chain override the generic HandleAsync<TContext>(...) method. The default interface method automatically adapts, but if you're still using the old dictionary-based API (from v10.0.2 or earlier), upgrade to v10.1.0 and use the typed API.

How do I access PipelineContext<T> from a handler that uses RequestHandler base class?​

Override the protected virtual method instead of the abstract one:

protected override async Task<Data> HandleAsync<TContext>(
MyCommand request, PipelineContext<TContext> context, CancellationToken ct)
where TContext : class, new()
{
if (context is PipelineContext<MyContext> ctx)
Console.WriteLine(ctx.Data.CorrelationId);
return await base.HandleAsync(request, context, ct);
}

The default implementation delegates to the non-context HandleAsync(request, ct), so calling base.HandleAsync(request, context, ct) preserves the chain.

RequestHandlerDelegate and RequestHandlerDelegate<TResponse> — which one should my behavior use?​

Cause: These are two separate delegate types for the two IPipelineBehavior variants.

Fix:

Behavior InterfaceDelegate Parameter TypeReturn Type
IPipelineBehavior<TRequest>RequestHandlerDelegateTask
IPipelineBehavior<TRequest, TResponse>RequestHandlerDelegate<TResponse>Task<TResponse>

The next delegate in HandleAsync always matches the behavior's interface. Use the non-generic variant for requests without a response, and the generic variant for requests with a response.


FAQ​

What's the difference between Send and Publish?​

  • Send<TRequest, TResponse>() — Request/response pattern. One request → one handler → one response. Use for commands and queries.
  • Send<TRequest>() — Request without a response. One request → one handler → completes when the handler finishes.
  • Publish<TNotification>() — Publish/subscribe pattern. One notification → zero or more handlers. Use for domain events and side effects.

When should I use IRequest<TResponse> vs INotification?​

Use IRequest<TResponse> when...Use INotification when...
You expect exactly one handlerYou want zero or more handlers
The caller needs a responseThe caller doesn't care about the outcome
The operation is a command or queryThe operation is an event or side effect
Example: "Create a product and return its ID"Example: "A product was created — send email, update cache, write audit log"

Can a request handler publish notifications?​

Yes. Inject IMediator into your handler and call Publish:

public class CreateProductHandler : RequestHandler<CreateProductCommand, int>
{
private readonly IMediator _mediator;
private readonly IProductRepository _repository;

public CreateProductHandler(IMediator mediator, IProductRepository repository)
{
_mediator = mediator;
_repository = repository;
}

public override async Task<int> HandleAsync(
CreateProductCommand command, CancellationToken cancellationToken)
{
var id = await _repository.AddAsync(new Product { Name = command.Name });
await _mediator.Publish(new ProductCreated(id, command.Name), cancellationToken);
return id;
}
}

Do I need to call AddMediator() before AddFeature()?​

No — AddMediator() and the AddFeature/AddNotification registrations can be called in any order, because they all target the same IServiceCollection. But note that since v10.2.0 AddMediator() is the call that applies the generator's auto-registrations: module initializers only queue handler registrations, and AddMediator() applies the queue. If AddMediator() is never called, Send and Publish fail at runtime (no IMediator, and no auto-registered handlers).

Can I use this without the fluent builder?​

Yes, but it's more verbose — and since v10.2.0 plain handlers don't need the builder at all (the source generator registers them when AddMediator() is called). The fluent builder remains the convenient way to register behaviors and validators, and it is syntactic sugar over standard DI registrations:

// Fluent builder (recommended)
builder.Services
.AddFeature<CreateProductCommand, int, CreateProductHandler>()
.AddValidator<CreateProductCommandValidator>();

// Equivalent manual registration
builder.Services.AddScoped<IRequestHandler<CreateProductCommand, int>, CreateProductHandler>();
builder.Services.AddTransient<IValidator<CreateProductCommand>, CreateProductCommandValidator>();
builder.Services.AddTransient<IPipelineBehavior<CreateProductCommand, int>, ValidationBehavior<CreateProductCommand, int>>();

How do I unit test a handler that inherits from RequestHandler?​

Test the public HandleAsync method directly — it's the one the mediator calls. Since v10.2.0 the method returns the value (or throws), so assert on the value or on the thrown exception — there is no Result to unwrap and the base class performs no logging or catching of its own:

[Fact]
public async Task HandleAsync_ValidCommand_ReturnsProductId()
{
// Arrange
var repository = new Mock<IProductRepository>();
repository.Setup(r => r.AddAsync(It.IsAny<Product>())).ReturnsAsync(42);

var handler = new CreateProductHandler(repository.Object);
var command = new CreateProductCommand("Test", 9.99m);

// Act — call the public HandleAsync, which is what the mediator calls
var productId = await handler.HandleAsync(command, CancellationToken.None);

// Assert
productId.Should().Be(42);
}

[Fact]
public async Task HandleAsync_DuplicateName_ThrowsConflictException()
{
// Arrange
var repository = new Mock<IProductRepository>();
repository.Setup(r => r.DoesProductExistAsync(It.IsAny<string>())).ReturnsAsync(true);

var handler = new CreateProductHandler(repository.Object);
var command = new CreateProductCommand("Existing", 9.99m);

// Act & Assert
await Assert.ThrowsAsync<ConflictException>(
() => handler.HandleAsync(command, CancellationToken.None));
}

Can I add multiple validators for the same request?​

Yes. The ValidationBehavior resolves IEnumerable<IValidator<TRequest>> and runs all validators. Register additional validators with AddTransient:

builder.Services
.AddFeature<CreateProductCommand, int, CreateProductHandler>()
.AddValidator<CreateProductCommandValidator>();

// Additional validators registered manually
builder.Services.AddTransient<IValidator<CreateProductCommand>, PricingValidator>();
builder.Services.AddTransient<IValidator<CreateProductCommand>, InventoryValidator>();

All three validators will run when the request is dispatched.

What's the difference between PipelineContext (old) and PipelineContext<T> (new)?​

As of v10.1.0, PipelineContext is now PipelineContext<T>. The old dictionary-based API (Set<T>(), Get<T>(), TryGetValue<T>()) has been removed in favor of strongly-typed context. Define a class for your context data and use ctx.Data.PropertyName instead. See the migration guide for detailed before/after examples.

Can I use multiple different context types in the same application?​

Yes. Each Send/Publish call specifies its context type via the generic parameter. Different features can use different context types:

var auditCtx = new PipelineContext<AuditContext>();
await mediator.Send<CommandA, Data, AuditContext>(cmd, auditCtx);

var perfCtx = new PipelineContext<PerformanceContext>();
await mediator.Send<CommandB, Data, PerformanceContext>(cmd, perfCtx);

How do notification behaviors compose?​

Each notification handler gets its own independent behavior pipeline. Behaviors are composed in registration order (reversed, same as request behaviors):

builder.Services
.AddNotification<OrderPlaced>()
.AddHandler<SendConfirmationEmail>()
.AddDecoration<NotificationRetryBehavior<OrderPlaced>>() // outermost: retries the inner pipeline
.AddDecoration<NotificationLoggingBehavior<OrderPlaced>>(); // innermost: logs around the handler

// For each handler: Retry → Logging → Handler

If the handler throws, NotificationLoggingBehavior sees the exception (it wraps the handler), then NotificationRetryBehavior catches it and retries the entire inner pipeline (including logging).


Known Limitations​

  • Compile-time registration, not runtime scanning: Handlers are discovered at compile time by the source generator, not at runtime. Auto-registration covers the assembly that calls AddMediator() plus any assembly loaded before that call; handlers in lazily-loaded class libraries may register too late (keep handlers in the entry assembly, touch the library at startup, or register explicitly).
  • One handler per request type: The mediator resolves IRequestHandler<TRequest, TResponse> as a single service. Two handlers for the same request pair in one assembly are a compile-time error (FEAG005) — use notifications for one-to-many dispatch.
  • No built-in request timeout or cancellation token modification: Pipeline behaviors receive the original CancellationToken. Behaviors cannot create linked tokens with timeouts without custom code.
  • No built-in transaction behavior: There is no TransactionBehavior in the box. Implement IPipelineBehavior<TRequest, TResponse> to wrap handlers in a transaction.
  • Notification handler ordering: Handlers are invoked in the order they appear in GetServices<T>() (registration order). There is no priority or ordering mechanism.

Performance Tips​

  • Register behaviors as Transient: The default AddDecoration<T>() and AddValidator<T>() register behaviors as Transient. This is correct — behaviors should not hold request-scoped state.
  • Avoid expensive work in constructors: Behaviors and handlers are resolved per-request (for Scoped handlers) or per-resolution (for Transient behaviors). Keep constructors cheap.
  • NotificationRetryBehavior increases latency: Each retry adds baseDelayMilliseconds * attempt delay. With 3 retries and 200ms base, worst-case latency is 200 + 400 + 600 = 1200ms extra. Tune these values for your workload.
  • Structured logging has a cost: LoggingBehavior serializes the full request and response objects ({@Request}, {@Response}). For high-throughput endpoints with large payloads, consider implementing a custom logging behavior that only captures essential fields.