Request Validation
Brighter can validate a request in its handler pipeline before your business handler runs. Validation is opt-in per handler: you mark the handler's Handle/HandleAsync method with [ValidateRequest], and you choose how the request is validated by registering one of three provider packages — FluentValidation, DataAnnotations, or Brighter's own Specification pattern. When a request is invalid, the pipeline throws a RequestValidationException carrying the individual failures, and your business handler never runs.
Request Validation vs. Pipeline Validation
These two features sound alike but solve different problems. Read this first so you land on the right page:
Request validation (this page) checks the data in a request at runtime — is the name present, is the email well-formed — and rejects bad input before your handler runs.
Pipeline validation checks that your handler pipelines are wired correctly at startup — that every request has a handler, that attributes resolve — and reports configuration mistakes before any message flows.
They even use different error types. Request validation produces a RequestValidationError; pipeline validation produces a ValidationError. If you are configuring .ValidatePipelines(), you want the other page.
Why Validate in the Pipeline
Validating in the pipeline, rather than inside your handler, has several benefits:
Reject bad input early. Invalid data is caught before it reaches your business logic or your database — no half-applied work, no exceptions thrown deep in domain code.
Keep validation as an orthogonal concern. Validation is a cross-cutting concern, so it belongs in middleware, not copied into every handler.
Simplify your handler and domain code. Lifting validation out of the handler keeps it focused on business logic instead of guarding its inputs. By the time your
Handlemethod runs, the request is already known to be valid.Catch failures in one place. Every provider throws the same
RequestValidationException, so code at the edge — for example an API mapping a failure to HTTP 422 — catches one exception type regardless of which validation framework you chose.
Request validation builds on Brighter's standard attribute-and-handler middleware model. If you are new to that model, read Building a Pipeline of Request Handlers first; this page assumes you understand how an attribute inserts a handler into the pipeline.
How It Works
You mark the target handler's Handle method with [ValidateRequest]:
As with any pipeline attribute, step orders this handler relative to other attributes on the method, and timing runs it before the target handler. timing defaults to HandlerTiming.Before, so [ValidateRequest(step: 1)] is equivalent. See Building a Pipeline of Request Handlers for how step and timing work in general.
What makes validation provider-agnostic is how the attribute is wired:
[ValidateRequest]targets an abstract base handler,ValidateRequestHandler<TRequest>, that owns the shared behaviour — guard the request, ask a provider for the failures, throw if there are any, otherwise call the next handler.Registering a provider maps that abstract handler to a concrete implementation.
UseFluentValidation(),UseDataAnnotations(), andUseSpecification()each register the mapping for you.
Because the attribute points at the abstract handler and the provider supplies the concrete one, the same [ValidateRequest] attribute works for every framework. You switch providers by changing the UseX() call, not by touching your handlers.
When the provider finds failures, the handler throws a RequestValidationException and your business handler never runs.
Using an External Bus? When validation runs in a handler dispatched from a message broker, a thrown
RequestValidationExceptionis an unhandled exception leaving the pipeline. By default Brighter's message pump logs it and acknowledges (discards) the message — it does not automatically requeue or dead-letter. Read Error Handling to choose what should happen to an invalid message (requeue, reject to a Dead Letter Queue, and so on).
Quick Start
This example validates a RegisterUser command with DataAnnotations — the provider with the least ceremony, because the rules live on the request itself.
1. Declare the rules on the request with DataAnnotations attributes:
2. Mark the handler with [ValidateRequest]:
3. Register Brighter and the provider. UseDataAnnotations() maps the [ValidateRequest] attribute to the DataAnnotations implementation:
4. Send a request and handle failures. A valid request reaches the handler; an invalid one is rejected before the handler runs:
The invalid request prints both failures and never reaches RegisterUserHandler:
A complete, runnable version of this is in Brighter/samples/Validation/DataAnnotationsSample/.
Choosing a Provider
All three providers share the same [ValidateRequest] attribute and throw the same RequestValidationException; they differ in where the rules live and what dependency you take.
FluentValidation
A separate IValidator<TRequest> class
FluentValidation
You want fluent, reusable rule sets kept apart from the request type.
DataAnnotations
Attributes on the request type
None (built into .NET)
The rules are simple and you want them to travel with the request.
Specification
An ISpecification<TRequest> you compose
None (ships in core)
You want composable domain rules expressed as specifications, with no extra dependency.
You are not locked in: because only the UseX() registration differs, you can start with one provider and move to another without changing your handlers. Pick one provider per service — the registration maps the validation handler globally, so a single service uses a single provider.
FluentValidation
Reach for FluentValidation when you want fluent, reusable rule sets that live apart from the request type. Each validated request has an IValidator<TRequest> registered in the container, and the provider resolves it to validate the request.
Install the Paramore.Brighter.Validation.FluentValidation package, then write a validator:
Register the validator and call UseFluentValidation():
UseFluentValidation() only maps the [ValidateRequest] attribute to the FluentValidation handler — it does not discover your validators. You register each IValidator<TRequest> yourself, either explicitly as above or in bulk with services.AddValidatorsFromAssembly(...). If a request is marked with [ValidateRequest] but no validator is registered for it, the provider throws a ConfigurationException (see Missing validator is a configuration error).
DataAnnotations
Reach for DataAnnotations when the rules are simple and you want them to travel with the request. The constraints are declared as attributes — [Required], [EmailAddress], [Range], and so on — directly on the request type, so nothing else needs registering.
The Quick Start above is a complete DataAnnotations example: the RegisterUser request carries its own [Required] and [EmailAddress] constraints, and registration is a single call:
Because the rules live on the request itself, there is no separate validator to register and therefore nothing to leave unregistered — unlike the FluentValidation and Specification providers, DataAnnotations has no missing-validator failure mode.
Specification
Reach for Brighter's own Specification pattern when you want to express rules as composable domain specifications without taking an extra dependency — the Specification<T> type ships in the core Paramore.Brighter package. Each validated request has an ISpecification<TRequest> registered, built by composing rules with And/Or. Each rule pairs a predicate with the ValidationError it reports when the predicate is not satisfied.
Install the Paramore.Brighter.Validation.Specification package, then build the specification:
And evaluates both rules, so an order that breaks both is reported with both errors. Register the specification and call UseSpecification():
As with FluentValidation, UseSpecification() only maps the attribute to the Specification handler — you register each ISpecification<TRequest> yourself, and a missing one throws a ConfigurationException.
Two error types, one mapping. The rules in your specification report Brighter's core
ValidationError(the same type used by pipeline validation). When validation fails, the provider maps those ontoRequestValidationErrorinstances on theRequestValidationException— so you authorValidationErrors but catchRequestValidationErrors.
Lifetime: register per request
Brighter's Specification<T> records per-evaluation state — it stores the results of the most recent IsSatisfiedBy call so it can report which rules failed. Register it with a per-request lifetime (transient or scoped), not as a singleton, so concurrent requests stay isolated. The registration above uses AddTransient; a singleton would let one request's evaluation overwrite another's.
Handling Validation Failures
When a request is invalid, the pipeline throws a RequestValidationException. Its Errors property is an IReadOnlyCollection<RequestValidationError> — one entry per failure, framework-agnostic regardless of which provider produced it:
PropertyName and ErrorMessage identify the field and the problem; the optional AttemptedValue and ErrorCode carry the rejected value and a machine-readable code when the provider supplies them. Catch the exception and read the failures:
Because every provider throws the same exception type, edge code can handle validation failures in one place. In a web API, for example, you can catch RequestValidationException and map its Errors to an HTTP 422 (Unprocessable Entity) response without caring whether FluentValidation, DataAnnotations, or the Specification provider rejected the request.
If you are validating on an External Bus consumer rather than at an API edge, the exception is caught by Brighter's message pump, not by your calling code. What happens to the message then — acknowledge, requeue, or dead-letter — is governed by your error-handling strategy; see Error Handling.
Missing validator is a configuration error
Marking a handler with [ValidateRequest] but registering no validator for that request is treated as a configuration error, not a silent skip. The FluentValidation and Specification providers resolve a validator (IValidator<TRequest> / ISpecification<TRequest>) from the container; if none is registered, the provider throws a ConfigurationException — Brighter's standard misconfiguration type — so the mistake fails fast rather than letting an unvalidated request through.
DataAnnotations has no separate validator to register (the rules live on the request), so it has no missing-validator failure mode.
Async Validation
Every provider has an asynchronous counterpart for the async pipeline. Mark the handler's HandleAsync method with [ValidateRequestAsync] instead of [ValidateRequest], and dispatch with SendAsync:
Registration is identical — the same UseFluentValidation() / UseDataAnnotations() / UseSpecification() call wires up both the sync and async pipelines. As before, an invalid request throws a RequestValidationException before HandleAsync runs.
Writing a Custom Provider
If your team has an in-house validation framework, you can plug it in as a provider. A provider is two pieces: a concrete handler that derives from the abstract base and turns your rules into RequestValidationErrors, and a UseX() extension that maps the abstract handler to your concrete one.
Provide a matching ValidateRequestHandlerAsync<TRequest>-derived handler (and register it the same way) if you support the async pipeline. For the full pattern, read ADR-0063 and one of the shipped providers under Brighter/src/Paramore.Brighter.Validation.*.
Further Reading
Building a Pipeline of Request Handlers — the attribute-and-handler middleware model request validation builds on.
Pipeline Validation and Diagnostics — the other validation feature: startup checks of your pipeline configuration.
Error Handling — what happens to an invalid message on an External Bus consumer, and how to choose requeue or dead-letter.
Dispatching An Async Request — the async pipeline used by
[ValidateRequestAsync].Basic Concepts — Commands, Events, and Requests.
Design rationale: ADR-0063 Request validation handler and ADR-0040 Specification pattern.
Working samples:
Brighter/samples/Validation/—FluentValidationSample,DataAnnotationsSample, andSpecificationSample.
Darker queries. Request validation currently applies to Brighter requests. The equivalent for Darker queries is planned but not yet shipped, so it is out of scope for this page.
Last updated
Was this helpful?
