Skip to content

IPipelineBehavior<TRequest, TResult>

IPipelineBehavior<TRequest, TResult> is the extension point for cross-cutting concerns. Behaviors wrap every handler invocation in a chain.

Interface signature

csharp
public interface IPipelineBehavior<TRequest, TResult>
{
    Task<Result<TResult>> HandleAsync(
        TRequest request,
        RequestHandlerDelegate<TResult> next,
        CancellationToken ct);
}

// next is:
public delegate Task<Result<TResult>> RequestHandlerDelegate<TResult>();

Calling await next() continues to the next behavior or the handler. Not calling next() short-circuits the pipeline and returns immediately.

Short-circuiting

Return a failure (or even a success) without calling next() to stop the pipeline. The handler is never invoked.

csharp
// Early return — handler is never called
if (isInvalid)
    return Result.Failure<TResult>(Error.Validation(...));

// Continue normally
return await next();

Complete example — ValidationBehavior with FluentValidation

This behavior applies to all commands (ICommand<TResult>). It runs all registered validators and aggregates errors into a single ValidationError.

csharp
using FluentValidation;
using MonadicSharp;

public class ValidationBehavior<TRequest, TResult>(
    IEnumerable<IValidator<TRequest>> validators)
    : IPipelineBehavior<TRequest, TResult>
    where TRequest : ICommand<TResult>
{
    public async Task<Result<TResult>> HandleAsync(
        TRequest request,
        RequestHandlerDelegate<TResult> next,
        CancellationToken ct)
    {
        if (!validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);

        var validationResults = await Task.WhenAll(
            validators.Select(v => v.ValidateAsync(context, ct)));

        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .ToList();

        if (failures.Count > 0)
        {
            var details = failures
                .Select(f => new FieldError(f.PropertyName, f.ErrorMessage))
                .ToList();

            return Result.Failure<TResult>(Error.Validation(details));
        }

        return await next();
    }
}

Corresponding FluentValidation validator

csharp
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(x => x.UserId)
            .NotEmpty().WithMessage("UserId is required.");

        RuleFor(x => x.Items)
            .NotEmpty().WithMessage("Order must contain at least one item.")
            .ForEach(item => item
                .ChildRules(i =>
                {
                    i.RuleFor(x => x.ProductId).NotEmpty();
                    i.RuleFor(x => x.Quantity).GreaterThan(0);
                }));

        RuleFor(x => x.ShippingAddress)
            .NotEmpty().MaximumLength(500);
    }
}

Registration

csharp
// Register FluentValidation validators
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

// Register the behavior (open-generic — covers all ICommand<T>)
builder.Services.AddBehavior(typeof(ValidationBehavior<,>));

Example — async authorization behavior

Behaviors can inject scoped services, including IHttpContextAccessor or a custom ICurrentUser.

csharp
public class AuthorizationBehavior<TRequest, TResult>(ICurrentUser currentUser)
    : IPipelineBehavior<TRequest, TResult>
    where TRequest : IAuthorizedRequest
{
    public async Task<Result<TResult>> HandleAsync(
        TRequest request,
        RequestHandlerDelegate<TResult> next,
        CancellationToken ct)
    {
        var authorized = await currentUser.HasPermissionAsync(request.RequiredPermission, ct);

        if (!authorized)
            return Result.Failure<TResult>(Error.Forbidden("User lacks required permission."));

        return await next();
    }
}

Example — transaction behavior

Wraps a command in a database transaction. Commits on success, rolls back on failure.

csharp
public class TransactionBehavior<TRequest, TResult>(AppDbContext db)
    : IPipelineBehavior<TRequest, TResult>
    where TRequest : ICommand<TResult>
{
    public async Task<Result<TResult>> HandleAsync(
        TRequest request,
        RequestHandlerDelegate<TResult> next,
        CancellationToken ct)
    {
        await using var tx = await db.Database.BeginTransactionAsync(ct);

        var result = await next();

        if (result.IsSuccess)
            await tx.CommitAsync(ct);
        else
            await tx.RollbackAsync(ct);

        return result;
    }
}

Released under the MIT License.