DSoftStudio Mediator

← Back to Documentation

Pipeline Behaviors

Pipeline behaviors wrap handler execution, forming a chain:

Behavior1 → Behavior2 → … → Handler

Each behavior can run logic before and after the next step in the pipeline. The next parameter is an IRequestHandler — calling next.Handle(request, ct) advances the chain via interface dispatch (virtual call) instead of a delegate invocation, enabling zero-allocation behavior pipelines.

Pipeline Allocation

Allocations stay flat at 72 B regardless of pipeline depth:

xychart-beta
    title "Allocation vs Pipeline Depth"
    x-axis "Behaviors" [0, 1, 3, 5]
    y-axis "Bytes" 0 --> 100
    line [72, 72, 72, 72]
Behaviors DSoft (alloc) MediatR (alloc)
0 72 B 272 B
1 72 B 544 B
3 72 B 800 B
5 72 B 1,088 B

Example

public class LoggingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public async ValueTask<TResponse> Handle(
        TRequest request,
        IRequestHandler<TRequest, TResponse> next,
        CancellationToken ct)
    {
        Console.WriteLine($"Handling {typeof(TRequest).Name}");
        return await next.Handle(request, ct);
    }
}

Register behaviors as open generics:

services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));

Scoped, not Transient: one Transient component registers the request’s whole pipeline chain as Transient, so every dispatch re-resolves and re-links the chain instead of reusing the one already built for the scope. Register a component Transient when it must be constructed per dispatch — a Scoped instance is shared by every dispatch in the scope, including concurrent ones, so per-dispatch state or a non-thread-safe field needs it. A behavior that keeps its state in locals, as the one above does, needs nothing.

Behaviors execute in registration order. The first registered behavior is the outermost wrapper.

Returning without calling next.Handle stops the chain there: no later behavior runs and neither does the handler, which is how a behavior serves a cached or rejected result. Post-processors are not skipped by this — they sit outside the chain and receive whatever response the behavior returned.

Register behaviors before PrecompilePipelines(). That call decides whether a chain is built for each request/response pair at all; if none exists by then, behaviors added afterwards never run. See Registration Order.

See Also