|
| 1 | +# 63. Request validation handler |
| 2 | + |
| 3 | +Date: 2026-06-15 |
| 4 | + |
| 5 | +## Status |
| 6 | + |
| 7 | +Proposed |
| 8 | + |
| 9 | +## Context |
| 10 | + |
| 11 | +Commands and queries typically need their input validated before the business |
| 12 | +handler runs. Bad input can cause exceptions deep in the pipeline, SQL injection |
| 13 | +risks, or unnecessary database load. MediatR has a well-established pattern for |
| 14 | +this: a pipeline behavior that resolves an `IValidator<TRequest>` and validates |
| 15 | +the request before calling `next`. |
| 16 | + |
| 17 | +Brighter already expresses cross-cutting concerns as attribute + handler pairs in |
| 18 | +the request pipeline (for example `RequestLoggingAttribute` + |
| 19 | +`RequestLoggingHandler<TRequest>`, and `UseInboxAttribute` + |
| 20 | +`UseInboxHandler<TRequest>`). A validation concern fits the same shape: an |
| 21 | +attribute marks the target `Handle`/`HandleAsync` method, and a decorator handler |
| 22 | +runs before the business handler. |
| 23 | + |
| 24 | +Issue [#4175](https://github.com/BrighterCommand/Brighter/issues/4175) proposes: |
| 25 | + |
| 26 | +- a `[ValidateQuery]` attribute and a corresponding `ValidateRequestHandler`; |
| 27 | +- the decorator resolves `IValidator<TRequest>` from the DI container and |
| 28 | + validates before calling `next`; |
| 29 | +- on failure, throw a `RequestValidationException` with structured error details; |
| 30 | +- support for three frameworks over time — Brighter's Specification pattern, |
| 31 | + FluentValidation, and `System.ComponentModel.DataAnnotations`. |
| 32 | + |
| 33 | +The maintainer noted that "FluentValidation might be a good starting PR". |
| 34 | + |
| 35 | +## Decision |
| 36 | + |
| 37 | +We add validation as an **optional, additive concern**. The provider-agnostic |
| 38 | +abstractions live in the **core** `Paramore.Brighter` assembly (they depend only |
| 39 | +on the core and carry no third-party reference, exactly like the built-in |
| 40 | +`RequestLogging`/`UseInbox` attribute+handler pairs), and each validation |
| 41 | +framework ships as **its own provider package**, so users only take the dependency |
| 42 | +they need. |
| 43 | + |
| 44 | +**Core — `Paramore.Brighter`, under a `RequestValidation/` sub-folder**: |
| 45 | + |
| 46 | +- `ValidateRequestAttribute` / `ValidateRequestAsyncAttribute` |
| 47 | + (namespace `Paramore.Brighter.RequestValidation.Attributes`) — extend |
| 48 | + `RequestHandlerAttribute`; `GetHandlerType()` points at the **abstract** base |
| 49 | + handlers below, mirroring `RequestLoggingAttribute`. |
| 50 | +- `ValidateRequestHandler<TRequest>` / `ValidateRequestHandlerAsync<TRequest>` |
| 51 | + (namespace `Paramore.Brighter.RequestValidation.Handlers`) — **abstract** base |
| 52 | + handlers extending `RequestHandler<TRequest>` / `RequestHandlerAsync<TRequest>`. |
| 53 | + They own the shared pipeline behaviour (null-guard the request, ask the provider |
| 54 | + for failures, throw on any failure, otherwise call `base.Handle`/`base.HandleAsync`) |
| 55 | + and expose one abstract method, `Validate` / `ValidateAsync`, that returns the |
| 56 | + failures. |
| 57 | +- `RequestValidationException` and `RequestValidationError` |
| 58 | + (namespace `Paramore.Brighter.RequestValidation`) — the exception carries a |
| 59 | + framework-agnostic `IReadOnlyCollection<RequestValidationError>` (property, |
| 60 | + message, attempted value, error code), so a caller catches **one** exception type |
| 61 | + regardless of provider. `RequestValidationError` is named to avoid a clash with |
| 62 | + the pre-existing `Paramore.Brighter.ValidationError`, which reports findings from |
| 63 | + Brighter's *startup pipeline validation* (ADR 0053) — a different concern. |
| 64 | + |
| 65 | +**Provider packages** (separate assemblies, this PR): |
| 66 | + |
| 67 | +- `Paramore.Brighter.Validation.FluentValidation` — derives from the abstract base |
| 68 | + handlers and implements `Validate`/`ValidateAsync` by resolving a FluentValidation |
| 69 | + `IValidator<TRequest>` from the container and mapping its failures onto |
| 70 | + `RequestValidationError`. `UseFluentValidation()` maps the abstract handler types |
| 71 | + to the FluentValidation implementations on the container. |
| 72 | +- `Paramore.Brighter.Validation.DataAnnotations` — validates with |
| 73 | + `System.ComponentModel.DataAnnotations`; `UseDataAnnotations()`. |
| 74 | +- `Paramore.Brighter.Validation.Specification` — validates with Brighter's own |
| 75 | + Specification pattern (ADR 0040); `UseSpecification()`. |
| 76 | + |
| 77 | +Because the attribute targets the abstract handler and each provider package maps |
| 78 | +it to a concrete implementation, **one `[ValidateRequest]` attribute works for |
| 79 | +every framework**: adding a further provider is purely additive — a new concrete |
| 80 | +handler plus a `UseX()` registration — with no change to the abstractions or to |
| 81 | +the existing provider packages. |
| 82 | + |
| 83 | +**Missing validator** is treated as a configuration error: if a request is marked |
| 84 | +with `[ValidateRequest]` but no validator is registered for it, the provider handler |
| 85 | +throws `ConfigurationException` (Brighter's existing misconfiguration type), |
| 86 | +fail-fast, rather than silently skipping validation. |
| 87 | + |
| 88 | +## Consequences |
| 89 | + |
| 90 | +- Validation is opt-in per request via one attribute; the framework is chosen by |
| 91 | + which provider package is registered. The core gains no third-party dependency — |
| 92 | + only a small, dependency-free abstraction, consistent with the existing built-in |
| 93 | + middleware. |
| 94 | +- A single `RequestValidationException` / `RequestValidationError` model is shared |
| 95 | + by all current and future providers, so edge code (e.g. an API mapping to HTTP |
| 96 | + 422) catches one type. |
| 97 | +- Adding further frameworks is additive and leaves the shipped code untouched. |
| 98 | + |
| 99 | +### Resolution of the open questions (maintainer feedback on PR #4183) |
| 100 | + |
| 101 | +- **Naming.** Renamed `[ValidateQuery]` → `[ValidateRequest]` (and the async |
| 102 | + variant). "Query" was a holdover from the Darker write-up of the issue; Brighter |
| 103 | + has requests, not queries. No alias is kept. |
| 104 | +- **Placement.** The abstractions are folded into the core `Paramore.Brighter` |
| 105 | + assembly (under `RequestValidation/`) rather than a separate |
| 106 | + `Paramore.Brighter.Validation` package, following the same pattern as the other |
| 107 | + built-in attribute+handler pairs. Providers remain separate packages. |
| 108 | +- **Issue tracking.** This PR delivers FluentValidation, DataAnnotations and |
| 109 | + Specification providers; it "Contributes to" #4175. |
| 110 | +- **Darker.** The same requirement exists for Darker V5; it carries a lot of |
| 111 | + structural change for V5 and is intentionally out of scope here, to follow as |
| 112 | + separate work. |
0 commit comments