Masterclass: MediatR Pipeline Behaviors & Validation Architecture

Architectural Purpose

Pipeline behaviors act as cross-cutting interceptors (similar to ASP.NET Core middleware, but specifically scoped to MediatR request execution flows) enabling logging, validation, and transaction management without cluttering handlers.


1. Technical Q&A: Pipeline Behaviors, Middleware & C# 12

Q1: Why did we need to wire up the validator and pipeline behavior? Why won’t it run without wiring?

Answer:

  • Dependency Injection (DI) Container ignorance: The .NET runtime and MediatR do not automatically scan your entire hard drive for classes.
  • Explicit Registration: AddValidatorsFromAssembly tells the DI container “scan this project for anything inheriting from AbstractValidator and register it,” while AddOpenBehavior tells MediatR: “wrap all requests in this behavior class before reaching the handler.” Without these configuration hooks, the classes remain isolated text files that the runtime never instantiates.

Q2: Does a pipeline behavior mean it has become a middleware? Why do we see await next() without a explicit delegate property?

Answer:

  • Conceptual Middleware: Yes! It is effectively middleware for your CQRS pipeline. Just as ASP.NET Core middleware intercepts HTTP requests coming into the web server, a MediatR IPipelineBehavior intercepts requests travelling from your API controller/endpoint down to your Command Handler.
  • Where is the delegate? The delegate is passed as a parameter into the Handle method: RequestHandlerDelegate<TResponse> next. In C#, a RequestHandlerDelegate<TResponse> is a wrapper function delegate representing the next step in the pipeline (either another behavior or the final handler itself). Invoking await next() executes that next step and awaits its return value.

Q3: Can you explain the class signature and C# 12 primary constructor syntax used here?

Signature:

public class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators) 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
  • Primary Constructor ((IEnumerable<IValidator<TRequest>> validators)): Introduced in C# 12, this allows you to declare constructor parameters directly on the class header. Behind the scenes, the compiler automatically generates a private backing field and assigns the parameter so you can use validators anywhere inside the class without boilerplate field declarations.
  • Generic Constraints (where TRequest : notnull): Restricts TRequest so that it cannot be a nullable reference type, enforcing safe handling throughout the pipeline.

2. Essential Concepts Cheat Sheet

Pipeline Behavior Flow

  1. Incoming Request: Hits the pipeline behavior first.
  2. Validation Check: Runs all registered IValidator<TRequest> instances concurrently (Task.WhenAll).
  3. Fail-Fast Guard: If errors exist, throws ValidationException immediately, aborting execution.
  4. Delegation: If validation passes, calls await next() to proceed to the Command Handler.