feat: add request validation handler with FluentValidation provider (#4175)#4183
Conversation
Document the decision to add request validation as an optional, additive concern: provider-agnostic abstractions (the [ValidateQuery] attribute, base handlers and RequestValidationException) plus FluentValidation as the first provider, per issue BrighterCommand#4175. Refs BrighterCommand#4175
…n provider Add request validation as an optional, additive concern (issue BrighterCommand#4175): - Paramore.Brighter.Validation: provider-agnostic abstractions. The [ValidateQuery] / [ValidateQueryAsync] attributes target abstract ValidateRequestHandler<TRequest> / ...Async base handlers that own the shared pipeline behaviour (null-guard, ask the provider for failures, throw before calling next). RequestValidationException carries a framework-agnostic list of ValidationError (property, message, attempted value, error code). - Paramore.Brighter.Validation.FluentValidation: the first provider. Concrete handlers resolve a FluentValidation IValidator<TRequest> from the container and map its failures onto ValidationError; UseFluentValidation() wires the abstract handlers to these implementations. The core is not modified. One [ValidateQuery] attribute serves every framework: the provider package maps the abstract handler to its concrete one, so DataAnnotations and the Specification pattern can be added later as additive packages. A request marked [ValidateQuery] with no registered validator throws ConfigurationException (Brighter's existing misconfiguration type). Refs BrighterCommand#4175
Cover the issue BrighterCommand#4175 proposal points and the failure paths through the FluentValidation provider: valid request (sync and async), single and multiple validation errors, missing validator (ConfigurationException), a validator that throws, a null request, cancellation-token propagation in the async handler, the structured shape of RequestValidationException, and concurrent reuse of a single handler instance. All tests pass on net10.0. (net9.0 builds but the runtime is unavailable on the authoring machine; CI exercises every target framework.) Refs BrighterCommand#4175
|
Thanks for this contribution! It helps a lot. It looks good and we should try to get it out asap.
|
|
Let's leave Darker for now, as it has a lot of structural changes for V5, but I can tag you there when it is ready to go. You are welcome to push implementations of DataAnnotations and Specifications here, too. |
Maintainer feedback on BrighterCommand#4183: move the provider-agnostic validation abstractions out of the separate Paramore.Brighter.Validation package into the core Paramore.Brighter assembly under RequestValidation/, mirroring the built-in RequestLogging/UseInbox attribute+handler pattern. Providers stay separate packages. - Rename [ValidateQuery]/[ValidateQueryAsync] -> [ValidateRequest]/ [ValidateRequestAsync]; Brighter has requests, not queries. - Rename the error type to RequestValidationError to avoid clashing with the pre-existing pipeline-validation Paramore.Brighter.ValidationError. - Use file-scoped namespaces to match the current core style. - Point the FluentValidation provider at the core directly; delete the standalone abstractions project and its slnx entry. - Drop the package-level <Authors> override (repo-wide Authors apply); add the contributor to CONTRIBUTORS.md. - Update ADR 0063 to record the decisions and resolved review questions. Contributes to BrighterCommand#4175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a test that wires AddBrighter().UseFluentValidation() and sends an invalid request, proving the extension maps the validation handlers into the pipeline. Previously the handlers were only ever registered by hand in the test harness, so UseFluentValidation() itself had no coverage. Uses a dedicated CreateAccount command with a single sync handler: AddBrighter() auto-scans every loaded Paramore.Brighter* assembly (including the test assembly), and SubscriberRegistry keeps sync and async handlers under one key, so a request that has both a sync and an async handler cannot be used through a synchronous Send. Contributes to BrighterCommand#4175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds Paramore.Brighter.Validation.DataAnnotations, the second provider for the request-validation concern. DataAnnotationsRequestHandler / ...Async validate a request against the System.ComponentModel.DataAnnotations attributes declared on the request type (e.g. [Required], [EmailAddress]) via Validator.TryValidateObject and map the failures onto RequestValidationError. UseDataAnnotationsValidation() selects the provider. Unlike a validator-based provider there is nothing to register per request — the constraints live on the request type — so a request with no annotations is simply valid (no missing-validator error). The IServiceProvider is passed to the ValidationContext so custom ValidationAttributes that resolve services work. No third-party dependency: the types ship with the framework (netstandard2.0+). Contributes to BrighterCommand#4175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers the valid path (sync + async), multiple field errors, an invalid async request, a null request (ArgumentNullException), cancellation-token propagation in the async handler, a request with no data-annotation attributes being treated as valid, and the real AddBrighter().UseDataAnnotationsValidation() registration through a synchronous Send. All green on net10.0; CI covers every TFM. Contributes to BrighterCommand#4175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds Paramore.Brighter.Validation.Specification, the third provider for the request-validation concern, built on Brighter's own Specification pattern (ADR 0040). SpecificationRequestHandler / ...Async resolve an ISpecification<TRequest> from the container, evaluate it with IsSatisfiedBy, and on failure collect the findings via the public ValidationResultCollector visitor, mapping each ValidationError (Source, Message) onto RequestValidationError. A request marked [ValidateRequest] with no registered specification throws ConfigurationException, fail-fast, matching the FluentValidation provider. UseSpecificationValidation() selects the provider. Contributes to BrighterCommand#4175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers the valid path (sync + async), a request that breaks multiple rules (both reported via And-composed specifications), an invalid async request, a missing specification (ConfigurationException), a null request (ArgumentNullException), cancellation-token propagation in the async handler, and the real AddBrighter().UseSpecificationValidation() registration through a synchronous Send. All green on net10.0; CI covers every TFM. Contributes to BrighterCommand#4175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…etime - Rename UseDataAnnotationsValidation() -> UseDataAnnotations() and UseSpecificationValidation() -> UseSpecification(), so all three providers follow the same Use<Provider>() shape as the established UseFluentValidation(). - Document that the ISpecification<TRequest> must be registered with a per-request lifetime (transient or scoped): Brighter's Specification<T> records per-evaluation state for the visitor to collect, so a single shared instance is not safe to evaluate from concurrent requests. Contributes to BrighterCommand#4175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…roviders Adds a SendAsync wiring test per provider (FluentValidation, DataAnnotations, Specification) that drives the real AddBrighter().UseX() registration through the asynchronous pipeline, closing the gap where only the synchronous Send path was exercised. Each uses a dedicated async-only command (no sync sibling) to stay clear of the sync/async handler ambiguity under Brighter's assembly auto-scan. Contributes to BrighterCommand#4175 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the thorough review, @iancooper — really appreciate it. I've pushed the changes addressing all your points:
I also took you up on the offer to add the other two providers in this PR:
The registration methods are |
iancooper
left a comment
There was a problem hiding this comment.
This looks really good, and the comments here are mostly about creating clarity rather than providing functional fixes. As this is the first PR for this, it would be nice to get the baseline right.
It would be useful to have a simple sample for these. It can be something as simple as /Users/ian_hammond_cooper/CSharpProjects/github/xbizzybone/Brighter/samples/CommandProcessor but in a separate folder Validation and include some samples that support the different validation types that we have.
The samples will also help us create the docs
| /// </summary> | ||
| internal sealed class ValidationPipeline | ||
| { | ||
| private ValidationPipeline(CommandProcessor commandProcessor, HandlerReceipt receipt) |
There was a problem hiding this comment.
Can we rename this? I found it confusing on first review, as it's a CommandProcessorBuilder, not the pipeline doing validation, which Brighter itself provides.
There was a problem hiding this comment.
Done — renamed ValidationPipeline → CommandProcessorHarness. That drops the misleading "pipeline" (Brighter owns the validation pipeline) and names it for what it actually is: a builder whose With(...)/WithAsync(...) factories assemble a real CommandProcessor. Updated the XML doc and all call sites (the local pipeline is now harness).
| public class ValidSpecificationValidationTests | ||
| { | ||
| [Fact] | ||
| public void When_a_valid_request_satisfies_its_specification_should_return_the_request() |
There was a problem hiding this comment.
Really, what you are testing here is that we don't throw an exception. The fact that the test returns the same request is just an artifact of the path having completed; it's not what you are trying to confirm. Assert.True() would work here as well, because if an exception were thrown, you would throw an exception from the test.
There was a problem hiding this comment.
Agreed, the request-equality was incidental. Switched to Assert.Null(Record.Exception(() => handler.Handle(validRequest))) — the repo's existing "does not throw" idiom — so the test confirms exactly that. I applied the same fix to the four sibling happy-path tests (DataAnnotations + Specification, sync/async) that shared the Assert.Same pattern, and renamed them ..._should_return_the_request → ..._should_not_throw to match the intent.
|
Thanks @iancooper — addressed the review:
|
…dProcessorHarness It is a builder for the CommandProcessor (Brighter owns the validation pipeline), and it bundles that processor with the HandlerReceipt the handler writes to; the new name reflects both concerns. Updates the XML doc and all call sites (the local `pipeline` variable becomes `harness`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rn the request For the happy-path tests, returning the same request was incidental to the path completing; what they confirm is that validation does not throw. Switches them to Assert.Null(Record.Exception(...)) (the repo's existing "does not throw" idiom) and renames the four affected tests from ..._should_return_the_request to ..._should_not_throw. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds samples/Validation/ (modeled on samples/CommandProcessor): three console apps - FluentValidationSample, DataAnnotationsSample and SpecificationSample - each sending one valid request (which reaches the handler) and one invalid request (rejected before the handler, printing the collected RequestValidationException.Errors), plus a README on when to use each provider and the async variant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Gates Passed
4 Quality Gates Passed
See analysis details in CodeScene
Quality Gate Profile: Clean Code Collective
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
|
Thanks for this contribution @xbizzybone. It is much appreciated. Feel free to ping us about other issues that you might want to pick up, and we will let you know. Typically, look for ones that are unassigned. Avoid the gateway for a couple of weeks, as we want to get the generic test framework in |
What
Adds request validation to Brighter as an optional, additive concern, with FluentValidation as the first provider — the starting point suggested in the issue.
Contributes to #4175
Design
Two assemblies so a single
[ValidateQuery]attribute serves every framework, while users only take the dependency they need:Paramore.Brighter.Validation(provider-agnostic, depends only on the core)[ValidateQuery]/[ValidateQueryAsync]— extendRequestHandlerAttribute, mirroringRequestLoggingAttribute; they target the abstract base handlers.ValidateRequestHandler<TRequest>/ValidateRequestHandlerAsync<TRequest>— abstract base handlers that own the shared behaviour: null-guard the request, ask the provider for failures, throw before callingnext, otherwise callbase.Handle/base.HandleAsync.RequestValidationExceptioncarrying a framework-agnosticIReadOnlyCollection<ValidationError>(property, message, attempted value, error code), so a caller catches one exception type regardless of provider.Paramore.Brighter.Validation.FluentValidation(this PR's provider)FluentValidationRequestHandler<TRequest>/...Asyncderive from the base handlers and resolve a FluentValidationIValidator<TRequest>from the container, mapping its failures ontoValidationError.UseFluentValidation()maps the abstract handlers to these implementations. The core is not modified.Because
[ValidateQuery]targets the abstract handler and the provider package maps it to a concrete one, addingParamore.Brighter.Validation.DataAnnotationsor...Specificationlater is purely additive — a new concrete handler plus aUseX()registration, with no change to the abstractions or to this package.A request marked
[ValidateQuery]with no registered validator throwsConfigurationException(Brighter's existing misconfiguration type), fail-fast, rather than silently skipping validation. An ADR (0063) is included as the first commit, per CONTRIBUTING.Scope / additive guarantees
Brighter.slnx— register the new projects.Directory.Packages.props— central version forFluentValidation(11.12.0, the last 11.x line that still supportsnetstandard2.0, which Brighter targets).netstandard2.0;net8.0;net9.0;net10.0; strong-named withBrighter.snk.Tests
xUnit, one behavior per file. Adversarial coverage through the FluentValidation provider: valid request (sync + async), single and multiple validation errors, missing validator (
ConfigurationException), a validator that throws (propagated, not swallowed), null request (ArgumentNullException), cancellation-token propagation in the async handler, the exact shape ofRequestValidationException, and concurrent reuse of a single handler instance. All green on net10.0 locally; CI covers every TFM.Open questions for the maintainer
[ValidateQuery], but the concern validates commands as well as queries. Keep it, or rename to[ValidateRequest](optionally keeping[ValidateQuery]as an alias)? Implemented as specified.Paramore.Brighter.Validationpackage; they depend only on the core, so they could equally live in the core assembly. Which do you prefer?Contributor License Agreement
Signed via CLA assistant — the
license/clacheck is green.Minor docs note: CONTRIBUTING.md still links to clahub.com for the CLA, but ClaHub has shut down; the CLA assistant GitHub App is what actually runs on PRs. Might be worth updating that link in a follow-up.
Type of Change
Checklist
Additional Notes
User-facing documentation for the BrighterCommand docs site can follow as a separate change once the API here is confirmed.