Table of Contents

Pipeline behaviors

Pipeline behaviors apply cross-cutting work (logging, validation, retries, transactions) around queries and commands:

internal sealed class LoggingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest
{
    public async ValueTask<TResponse> HandleAsync(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Executing {typeof(TRequest).Name}");
        return await next(cancellationToken);
    }
}

Register an open generic behavior:

builder.Services.AddPipelineBehavior(typeof(LoggingBehavior<,>));

Ordering and short-circuiting

The first registered behavior is the outermost. Behaviors may short-circuit by returning without calling next, and they may pass a replacement cancellation token to next.

flowchart TD
    subgraph L["LoggingBehavior (registered first, outermost)"]
        subgraph V["ValidationBehavior (registered second)"]
            H["Handler"]
        end
    end

A request enters the outermost behavior, travels inward to the handler, and the response unwinds back out through each behavior in reverse.

Registering the same behavior more than once is safe: the first registration wins and later ones are ignored, so a behavior never runs twice in one pipeline.

Targeting requests with constraints

The same IPipelineBehavior<TRequest, TResponse> contract handles queries and both command shapes. A resultless ICommand is adapted to Unit only inside the pipeline; its public handler and dispatch methods remain resultless.

The constraint on TRequest decides which requests a behavior applies to:

Constraint Applies to
IRequest Every query and command
IQueryBase Queries only
ICommandBase Both command shapes
ICommand Resultless commands only
ICommand<TResponse> Response-bearing commands only

The constraint is the only targeting mechanism you need. See the marker hierarchy.

Note

Pipeline behaviors apply to queries and commands, not notifications.

When no behavior applies, Dispatcher takes a direct handler path and skips pipeline construction entirely. Behaviors are still resolved per dispatch so scoped and transient lifetimes remain correct.