Skip to content

feat(validation): ValidatePipelines warns on unresolvable transforms and missing validation providers (#4159)#4189

Merged
iancooper merged 24 commits into
BrighterCommand:masterfrom
xbizzybone:feature/4159-validate-pipelines-assembly-provider-checks
Jun 19, 2026
Merged

feat(validation): ValidatePipelines warns on unresolvable transforms and missing validation providers (#4159)#4189
iancooper merged 24 commits into
BrighterCommand:masterfrom
xbizzybone:feature/4159-validate-pipelines-assembly-provider-checks

Conversation

@xbizzybone

Copy link
Copy Markdown
Contributor

What

Extends ValidatePipelines() with two non-blocking (Warning) startup checks, closing #4159:

  • (A) Missing-transform check. For each Publication (wrap) / Subscription (unwrap), if the resolved message mapper declares a transform whose transformer type (the attribute's GetHandlerType()) is not registered, emit a Warning naming the request type, transformer, entity (Topic/subscription Name), and prompting an AutoFromAssemblies() check — the strong signal that the transformer's assembly was not scanned (the issue's JustSayingCompressionTransform example).
  • (B) Missing validation-provider check (your add-on in the issue comment). For each handler whose pipeline declares a request-validation step (a step targeting the open generic ValidateRequestHandler<> / ValidateRequestHandlerAsync<>, as [ValidateRequest]/[ValidateRequestAsync] do) while no provider is registered, emit a Warning naming the request/handler and UseFluentValidation() / 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):

  • Mapper-missing is not reliably detectable on the DI path: MessageMapperRegistry always falls back to the default JsonMessageMapper<>, 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).
  • Subscription handler-missing is already detected by the existing ConsumerValidationRules.HandlerRegistered (Error). Not re-implemented (OOS-8).
  • The default mapper (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 JustSayingMessageMapperJustSayingCompressionTransform example.

Design

  • New rules plug into the existing ISpecification<T> + ValidationResultCollector machinery evaluated by PipelineValidator — no new hosted service, no change to throwOnError.
  • (A)-producer lives in core ProducerValidationRules; (A)-consumer in Paramore.Brighter.ServiceActivator ConsumerValidationRules (consumer concerns stay out of core); (B) in core HandlerPipelineValidationRules.
  • A small role interface IAmATransformerResolvabilityProbe (core) answers "is this transformer registered?" via a non-instantiating membership lookup against the IServiceCollection (default impl in the DI assembly, mirroring IAmASubscriberRegistryInspector) — so the check never builds the transform pipeline (which throws on a missing transformer) and never instantiates a transformer.
  • An async-aware DescribeTransforms overload unions the sync- and async-resolved mappers' transforms (de-duplicated), so a transform declared on an async-only mapper is still evaluated.
  • Full rationale in ADR 0064 (docs/adr/0064-validate-pipeline-assembly-and-provider-registration.md) with requirements/tasks under specs/0035-validate-pipeline-assembly-and-provider-registration/. Built through the spec workflow (requirements → ADR → adversarial review → tasks → TDD).

Notes for review

  • Severity is Warning, deliberately. The condition is deterministic (the build path throws a ConfigurationException when 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.
  • Consistent with every existing rule, the new rules do not catch exceptions — an inspection exception surfaces as the framework's Error (ADR §3a).
  • 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/ and tests/Paramore.Brighter.Extensions.Tests/ cover each rule's happy/negative/boundary paths (missing / all-resolvable / none / null RequestType / 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 under throwOnError, per-entity determinism, and the inert fallback when validation isn't wired.

Closes #4159.

🤖 Generated with Claude Code

xbizzybone and others added 22 commits June 16, 2026 21:45
…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>
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

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>
@xbizzybone xbizzybone force-pushed the feature/4159-validate-pipelines-assembly-provider-checks branch from c929af4 to ec39ce1 Compare June 17, 2026 11:12
codescene-delta-analysis[bot]

This comment was marked as outdated.

@iancooper iancooper left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Thanks @xbizzybone

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@iancooper iancooper merged commit 6fc4822 into BrighterCommand:master Jun 19, 2026
25 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ValidatePipeline should check all required assemblies will be scanned

2 participants