feat(validation): ValidatePipelines warns on unresolvable transforms and missing validation providers (#4159)#4189
Merged
iancooper merged 24 commits intoJun 19, 2026
Conversation
…sembly/provider checks Issue BrighterCommand#4159: extend ValidatePipelines() with (A) missing-transform detection (producer wrap / consumer unwrap) and (B) missing validation-provider detection. Includes requirements, ADR 0064, tasks, and adversarial review records. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… provider-registrations record Scaffolding for the issue BrighterCommand#4159 ValidatePipelines checks (not yet wired): - IAmATransformerResolvabilityProbe: core role interface for non-instantiating transformer-resolvability checks (mirrors IAmASubscriberRegistryInspector). - ValidationProviderRegistrations: record carrying the sync/async provider-registered flags (uses the solution's record value-holder convention). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ServiceCollectionTransformerResolvabilityProbe answers IAmATransformerResolvabilityProbe by membership over the service types registered in the IServiceCollection, without resolving the container or instantiating the transformer (issue BrighterCommand#4159). - Test covers registered, unregistered, no-instantiation, empty, and null-services flows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ProducerValidationRules.WrapTransformResolvable reports a Warning per wrap transform declared on a publication's resolved mapper whose transformer type is not resolvable — a signal its assembly was not scanned (issue BrighterCommand#4159). Follows the existing rule pattern: the rule does not catch exceptions itself (the Specification framework reports any evaluation error, as it does for every other rule). - Test covers missing transform, all-resolvable, no-transforms declared, null RequestType, and no-mapper-resolved flows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the §3a inline try/catch guard (validation rules do not catch; the Specification<T> framework reports evaluation exceptions as Error like every existing rule — intended findings stay Warning). Drop the standalone "async DescribeTransforms overload" structural task; that describe extension is added test-first in the (A)-consumer async-only cycle (FR-5). ValidationProviderRegistrations is a record class (the solution has no record struct). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…issing transform independently Adds an FR-3 characterization fact to WrapTransformResolvableTests: a mapper declaring two distinct wrap transforms, with the probe resolving one but not the other, yields exactly one Warning (for the unresolvable transform) — a resolvable transform does not suppress it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ConsumerValidationRules.UnwrapTransformResolvable mirroring the producer WrapTransformResolvable rule: for each Subscription, describe the resolved mapper's unwrap transforms and emit one Warning per transform whose GetHandlerType() transformer type the probe reports as unresolvable — a strong signal its assembly was not scanned (BrighterCommand#4159). Subscriptions with a null RequestType, or whose request type resolves to no mapper, are skipped. The rule does not catch exceptions; the Specification<T> framework reports evaluation errors as Error like every other rule. DI registration in RegisterConsumerValidationSpecs is deferred to the plumbing cycle, where the default IAmATransformerResolvabilityProbe and the MessageMapperRegistry become resolvable from the container and the consumer-spec count assertion moves 3->4; registering it now would throw at validation time and break existing consumer-validation tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…R-5) Add an async-aware DescribeTransforms overload on TransformPipelineBuilder that unions the sync- and async-resolved mappers' declared transforms, de-duplicated by (transformer type, step), so a request type served only by an async mapper is still described. The existing 2-arg overload now delegates with includeAsync:false, preserving its behavior for the producer rule and PipelineDiagnosticWriter. ConsumerValidationRules.UnwrapTransformResolvable now describes with includeAsync:true, so an unresolvable unwrap transform declared on an async-only mapper still reports a Warning naming the GetHandlerType() transformer type (not the attribute name). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…publications (FR-5) ProducerValidationRules.WrapTransformResolvable now describes with includeAsync:true, unioning the sync- and async-resolved mappers' wrap transforms, so a publication whose request type is served only by an async mapper still reports an unresolvable wrap transform as a Warning. FR-5 is general — it applies to publications as well as subscriptions; this conforms the producer rule to it, matching ADR 0064 §2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reconcile the spec docs with the implemented code after an adversarial consistency pass: - ValidationProviderRegistrations is a record class, not a record struct (ADR §5); the PipelineValidator ctor param is therefore `ValidationProviderRegistrations? = null` with null treated as no-provider, since a class default cannot express (false,false) (ADR §B, tasks structural + (B) tasks). - Producer (A) rule uses the async-aware DescribeTransforms overload (includeAsync:true) per FR-5, like the consumer; tasks updated with a producer async-only FR-5 test task and the FR-5 coverage row now lists both sides. The describe overload (not the rule) performs the union. - Producer warning message uses an em-dash to match the code. - Consumer DI-registration sequencing note: it rides the plumbing cycle (count assertion 3 -> 4) to avoid throwing before its deps exist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HandlerPipelineValidationRules.ValidationProviderRegistered reports a Warning for each handler pipeline step whose handler type is the open-generic ValidateRequestHandler<> (sync) or ValidateRequestHandlerAsync<> (async) while the matching provider flag is not set — i.e. a validation step is present but no provider was registered to back it. Detection is provider-agnostic (it keys off the step's handler type, not the concrete attribute) and the message names the request/handler and the three provider calls (UseFluentValidation, UseDataAnnotations, UseSpecification). The rule follows the framework pattern and does not catch exceptions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tracker references in code comments add no value and age poorly; the rationale already reads inline. Removes the "(see issue ...)" tags from the producer and consumer wrap/unwrap-transform rule doc comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…elines ValidatePipelines now computes the sync/async validation-provider registrations from the service collection (presence of the open-generic ValidateRequestHandler<> / ValidateRequestHandlerAsync<> mappings) and threads them into PipelineValidator via a new trailing optional ctor parameter. The validator appends the validation-provider check to the handler-pipeline rules only when the registrations are supplied, so existing direct callers stay inert. A handler declaring a validation step with no provider now surfaces a non-blocking Warning end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ePipelines ValidatePipelines now registers the default transformer-resolvability probe (a snapshot of the service collection taken at registration time) and, in the validator factory, builds the message-mapper registry the same way DescribePipelines does and threads both into PipelineValidator through two new trailing optional ctor parameters. The validator appends the producer wrap-transform check only when both are supplied, so direct callers and pure-CQRS configurations stay inert. A publication whose resolved mapper declares a wrap transform whose transformer is not registered now surfaces a non-blocking Warning end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Consumers RegisterConsumerValidationSpecs now registers ConsumerValidationRules. UnwrapTransformResolvable as a fourth consumer specification. Its factory resolves the transformer-resolvability probe (registered by ValidatePipelines) and the message-mapper registry builder from the provider; when either is absent — e.g. AddConsumers without ValidatePipelines — it returns an inert specification, so the check only runs when validation is enabled. The consumer-spec count test is updated from three to four. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rm check A publication with no custom mapper resolves to the default JsonMessageMapper, which declares a built-in [CloudEvents] wrap transform. Checking that transform warned whenever CloudEventsTransformer was not registered (any configuration that does not auto-scan Brighter's own transforms), a false positive NFR-5 calls out. WrapTransformResolvable now short-circuits when the resolved mapper is the default (TransformPipelineDescription.IsDefaultMapper): the default mapper's transforms are Brighter built-ins and out of scope for the check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ault mapper Reconcile FR-4/AC-4, ADR 0064, and tasks with the as-built behavior: a publication resolving to the default mapper produces no (A) warning because the default mapper's transforms are Brighter built-ins and out of scope (skipped), not because the transformer is assumed to be registered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ault The async-aware DescribeTransforms overload computed IsDefaultMapper from the sync-resolved mapper alone. With a default mapper configured (as AddBrighter does), a request type registered only via RegisterAsync makes the sync side fall back to the default, so IsDefaultMapper read true and the default-mapper guard skipped the custom async mapper entirely — its declared transforms were never checked. IsDefaultMapper is now true only when every contributing mapper is a default, so a custom async mapper's transforms are evaluated even while the sync side falls back to the default. Also clarifies the describe helpers' parameter names. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the consumer The producer wrap-transform rule skips the default mapper (its transforms are Brighter built-ins, out of scope). The consumer unwrap-transform rule now does the same, so the two sibling rules treat the default mapper identically — consistent and robust even though JsonMessageMapper, the default, declares only a wrap transform today. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds: cross-cutting tests (warnings non-blocking under throwOnError; per-entity deterministic, ordered warnings); broader rule paths (consumer two-unwrap one-resolvable-one-not, consumer default-mapper, validation step in after-steps, DescribeTransforms sync/async union and de-dup); and regression tests for the custom-async-mapper-behind-a-default fix on both the producer and consumer rules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ipelines call order Uses the collection-expression [] for the inert consumer spec to match the surrounding rule code, and documents that ValidatePipelines must be the last call in the builder chain (the provider registrations and the transformer probe are snapshotted at that point) plus the throwOnError parameter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the paths a multi-agent review found unpinned: the (B) transposition case (a sync validation step with only the async provider registered), the describe union of distinct transforms ordered by descending step, and the consumer unwrap-transform spec being inert (no throw, no finding) when AddConsumers is used without ValidatePipelines so no probe is registered. Also corrects the StubTransformerResolvabilityProbe comment, which claimed a rule-level error guard that does not exist (rule exceptions surface as the framework's Error, by design). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DescribeWrapSteps and DescribeUnwrapSteps were near-identical (CodeScene flagged the duplication). Collapse them into a single DescribeSteps helper parameterised by the mapper-method finder and the attribute extractor — WrapWithAttribute and UnwrapWithAttribute both derive from TransformAttribute, so the projection to TransformStepDescription is shared. The null checks live inside the helper and the all-default test is its own predicate, so DescribeTransforms stays small (no duplication, low cyclomatic complexity). No behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c929af4 to
ec39ce1
Compare
iancooper
approved these changes
Jun 19, 2026
iancooper
left a comment
Member
There was a problem hiding this comment.
Looks good. Thanks @xbizzybone
There was a problem hiding this comment.
Our agent can fix these. Install it.
Gates Passed
4 Quality Gates Passed
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.
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.
What
Extends
ValidatePipelines()with two non-blocking (Warning) startup checks, closing #4159:Publication(wrap) /Subscription(unwrap), if the resolved message mapper declares a transform whose transformer type (the attribute'sGetHandlerType()) is not registered, emit a Warning naming the request type, transformer, entity (Topic/subscriptionName), and prompting anAutoFromAssemblies()check — the strong signal that the transformer's assembly was not scanned (the issue'sJustSayingCompressionTransformexample).ValidateRequestHandler<>/ValidateRequestHandlerAsync<>, as[ValidateRequest]/[ValidateRequestAsync]do) while no provider is registered, emit a Warning naming the request/handler andUseFluentValidation()/UseDataAnnotations()/UseSpecification().Both checks are Warning severity and never block startup, even under
ValidatePipelines(throwOnError: true)— matching your steer in the issue (ValidatePipelines force-loads the assemblies and is commonly off in production, so this is a dev-time heads-up).Scope (honest narrowing)
A discovery pass against the codebase narrowed (A) to transforms and added (B):
MessageMapperRegistryalways falls back to the defaultJsonMessageMapper<>, so "no mapper" never occurs and a silent fall-back to the default is indistinguishable from intentionally using it. Excluded as too noisy (OOS-7 / C-9).ConsumerValidationRules.HandlerRegistered(Error). Not re-implemented (OOS-8).JsonMessageMapper, which declares a built-in[CloudEvents]transform) is out of scope for (A) and skipped — its transforms are Brighter built-ins.So (A) targets the reliably-detectable case — the mapper is scanned but a transformer it declares is not — which is exactly the
JustSayingMessageMapper→JustSayingCompressionTransformexample.Design
ISpecification<T>+ValidationResultCollectormachinery evaluated byPipelineValidator— no new hosted service, no change tothrowOnError.ProducerValidationRules; (A)-consumer inParamore.Brighter.ServiceActivatorConsumerValidationRules(consumer concerns stay out of core); (B) in coreHandlerPipelineValidationRules.IAmATransformerResolvabilityProbe(core) answers "is this transformer registered?" via a non-instantiating membership lookup against theIServiceCollection(default impl in the DI assembly, mirroringIAmASubscriberRegistryInspector) — so the check never builds the transform pipeline (which throws on a missing transformer) and never instantiates a transformer.DescribeTransformsoverload unions the sync- and async-resolved mappers' transforms (de-duplicated), so a transform declared on an async-only mapper is still evaluated.docs/adr/0064-validate-pipeline-assembly-and-provider-registration.md) with requirements/tasks underspecs/0035-validate-pipeline-assembly-and-provider-registration/. Built through the spec workflow (requirements → ADR → adversarial review → tasks → TDD).Notes for review
ConfigurationExceptionwhen that request type is actually produced/consumed), which by the ADR 0053 precedent would be an Error. I kept it a Warning per your framing; it is flagged revisitable in the ADR (§3a / C-2) if you'd prefer Error or a configurable severity.ValidatePipelines()should be the last call in the builder chain (the provider flags + transformer probe are snapshotted at that point) — documented on the method.Testing
Developer tests across
tests/Paramore.Brighter.Core.Tests/Validation/andtests/Paramore.Brighter.Extensions.Tests/cover each rule's happy/negative/boundary paths (missing / all-resolvable / none / nullRequestType/ no-mapper / default-mapper / two-transforms / async-only / sync∪async union + de-dup), (B) both modalities + transposition + after-steps, the non-instantiating probe, end-to-end DI wiring, non-blocking behaviour underthrowOnError, per-entity determinism, and the inert fallback when validation isn't wired.Closes #4159.
🤖 Generated with Claude Code