feat(validation)!: run startup checks as pluggable rules - #5
Merged
Conversation
A rule implementing IRequestFlowValidationRule reads the whole registration picture off RequestFlowValidationContext and returns RequestFlowValidationProblem values carrying a stable code from ProblemCodes. The model reports lifetimes as RequestFlowLifetime rather than the container's ServiceLifetime, since RequestFlow.Abstractions takes no dependency on the DI package. Handlers and stages carry the contract they implement, so a kind layered on a core contract, such as ICommandHandler, comes through under its own. RequestFlowModelBuilder builds a model by hand and BuildContext wraps one in the context a rule receives, which is how a rule is tested without a container. Nothing consumes these types yet.
The six built-in registration checks move out of RegistrationValidator and behind IRequestFlowValidationRule, so an application or a package adds its own checks with AddValidationRule<T>() and reports into the same exception. A rule that throws is reported as RF0107 and the rules after it still run. RegistrationSnapshot projects the registry into the RequestFlowModel a rule sees, which is why HandlerRegistration now carries the lifetime chosen by the AddRequestFlow call that found it. A request implementing more than one IRequest<TResponse> now fails the freeze with RF0106. The extra contract used to pass the freeze and fail every dispatch under it with ResponseTypeMismatchException. BREAKING CHANGE: RequestFlowValidationException.Problems holds RequestFlowValidationProblem values instead of strings, and the public constructor takes the same list, so a call site passing strings no longer compiles. Message lines now read "RF0101: ...", so anything matching the old text breaks. Repeated registrations collapse into one problem each, where a request with three handlers used to report two identical lines. Aliased stage declarations report a single RF0104 naming every declaration in the collision, and RF0104 no longer fires for two closings of one stage class on a request with more than one handler, since that request already fails RF0101.
AddCqrs registers CommandQuerySplitRule, so a request implementing both ICommand<T> and IQuery<T> fails the freeze with CQRS0001 instead of resolving through whichever typed dispatcher the caller happened to reach for. The rule checks contract assignability rather than response shapes, so it also catches ICommand<A> next to IQuery<B>. CqrsProblemCodes is public and ships in RequestFlow.Cqrs.Abstractions, so an assembly referencing only the contracts can match the code instead of a literal string.
docs/validation-rules.md walks the rule contract, the model a rule reads, registration and lifetime, and how to test a rule without a container. It also lists every built-in code, since a caller matching ProblemCodes needs to know what already fires. The exception, lifetime, registration, and stage pages pick up the code-carrying problems and the narrowed RF0104.
Orders.Api dispatches commands and queries, filters a validation stage by handler marker, and contributes two validation rules of its own. Orders.Api.Violations holds the types those rules reject, in an assembly scanned only under --break-rules, so the startup failure is reproducible from a checkout.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Startup validation was one method with every check inlined. Adding a check meant editing
RegistrationValidator, and a package built on RequestFlow had no way to add one at all.Validation now runs as a list of rules. A rule reads the whole registration picture off
RequestFlowValidationContextand reports into the same exception as the built-in checks. The built-in checks are rules themselves, andAddValidationRule<T>()appends to the list.RequestFlow.Abstractions, so a rule can ship in an assembly that never references the DI package. Lifetimes come through asRequestFlowLifetimeinstead of the container'sServiceLifetime, and a handler or stage reports the contract it implements asContractType, so a kind layered on a core contract, such asICommandHandler, arrives under its own name. Every list on the model is read-only.RequestFlowModelBuilderbuilds aRequestFlowModelby hand andBuildContextwraps one in the context a rule receives, which is how a rule is unit tested without a container.RF0107, and the rules after it still run.RF0106catches a request implementing more than oneIRequest<TResponse>, which used to pass the freeze and then fail every dispatch under it withResponseTypeMismatchException.CQRS0001catches a request thatAddCqrsclassifies as both a command and a query.RF0104now reports one problem per stage-class collision instead of a line per colliding pair, and no longer fires for two closings of one stage class on a request with more than one handler. That request already failsRF0101, and the collision surfaces once the duplicate handler is gone.docs/validation-rules.mdcovers writing, registering, and testing a rule.samples/Orders.Apiis a minimal API wired with two custom rules, a validation stage, and a logging stage;samples/Orders.Api.Violationsis the same wiring with the mistakes left in, so the exception can be read against real code.BREAKING CHANGE:
RequestFlowValidationException.ProblemsholdsRequestFlowValidationProblemvalues (stable code, message, offending type) instead of strings, and the public constructor takes the same list, so a call site passing strings no longer compiles. Message lines now readRF0101: ..., so anything matching the old text breaks. Repeated registrations collapse into one problem each, where a request with three handlers used to report two identical lines.ProblemCodesandCqrsProblemCodesship in the abstractions packages, so matchProblemCodes.UnhandledRequestrather than a literal string.