Conversation
Requirements and design for opt-in pipeline validation and diagnostic reporting across all three configuration paths (AddBrighter, AddProducers, AddConsumers). ADR 0053 accepted after six revisions covering dry-run PipelineBuilder.Describe(), Specification<T> move, ValidationRule<T>, SubscriberRegistry handler type tracking, and sync/async mapper distinction. Closes #2176 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Pipeline Validation At Startup (Draft)This is a design-phase PR — spec files and ADR only, no implementation code yet. My review focuses on the architecture, design decisions, and potential issues to address before or during implementation. Overall AssessmentThis is a well-thought-out ADR addressing a real pain point for Brighter users. The layered architecture mirrors Brighter's own configuration paths, the opt-in approach preserves backward compatibility, and the use of the existing Design Issues to Resolve Before Implementation1. Generic
|
|
@claude We have considered and rejected point 2: "Describe() on a Generic Class Ignores Its Type Parameter Two issues: (1) Moving Describe to a separate class carries the risk of model drift between the two. This approach ensures that the path used to build pipelines matches the one we describe. Both these lead us to prefer adding a non-generic method that takes the type |
|
Claude finished @iancooper's task in 32s —— View job PR Review: Pipeline Validation At Startup (Draft)
Response on Point 2:
|
Address reviewer feedback about the non-generic method on a generic class. Document the two reasons: drift prevention (shared code with Build) and no runtime instance (validator works with Type objects at registration time). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The underlying dictionaries are keyed by Type, so the generic constraint adds nothing and would force the validator to use MakeGenericMethod() reflection. Non-generic overloads match the validator's runtime Type iteration pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Plain Dictionary matches existing _observers field — both are written during single-threaded DI registration and read-only after. Contrasts with PipelineBuilder's ConcurrentDictionary caches which are lazily populated during concurrent message dispatch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace underspecified IAmAValidationState flag with concrete design: BrighterValidationHostedService resolves IDispatcher? (no-op if present), ServiceActivatorHostedService resolves IAmAPipelineValidator? (runs if present). No shared mutable state — decisions based on immutable DI registrations. Includes scenario matrix for all three configurations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The detailed example only checked BeforeSteps while the summary section correctly used BeforeSteps.Concat(AfterSteps). After-steps can also be misconfigured, so the broader form is correct. Both instances now match. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace undefined IsBackstopAttribute()/IsResilienceAttribute() helpers with marker interfaces following the existing IAmA* naming convention. Built-in handlers implement the appropriate interface; third-party handlers can opt in. Both code examples now use the interfaces consistently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All() returns true on empty collections, so PumpHandlerMatch vacuously passes when no handlers are registered. This is intentional — the HandlerRegistered rule catches that case with a more specific message. Added comment to make the dependency explicit for future maintainers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A resilience pipeline before a backstop could intentionally catch and act on exceptions before passing them on. Unusual and probably not intended, but not provably wrong — so we warn rather than block startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add implementation ordering: ADR 0053 marker interfaces must be implemented before analyzer diagnostics in this ADR - BRT008: suppress when getRequestType/MapRequestType is provided to avoid false positives from dynamic dispatch overriding static type - BRT008: support non-generic Subscription with requestType: typeof(X) in addition to generic Subscription<T> - BRT006: specify parameter-name-based step extraction via AttributeData.ConstructorArguments matched against IMethodSymbol.Parameters - Add 4 new BRT008 test cases for non-generic and dynamic dispatch scenarios - Replace "Non-generic subscriptions" limitation with "Dynamic request type dispatch" limitation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review: Pipeline Validation At Startup (ADR-0053 + ADR-0054)This is a design/specification PR (ADRs, requirements, spec tracking). No implementation code is included. Feedback is on the architectural design and completeness. Overall AssessmentThe design is well-reasoned and addresses a real pain point. The layered architecture mirrors Brighter's own three configuration paths, the Specification + ValidationRule pattern is idiomatic, and the opt-in integration via Design Gaps1. How does ADR-0053 shows: foreach (var description in _pipelineBuilder.Describe())But 2. Sync/async attribute rule may over-fire on custom handlers The rule: d.BeforeSteps.Concat(d.AfterSteps).All(step =>
{
var stepIsAsync = typeof(IHandleRequestsAsync).IsAssignableFrom(step.HandlerType);
return d.IsAsync == stepIsAsync;
})For steps whose handler type implements neither 3. Backstop ordering rule message doesn't identify which attribute is misordered The check fires if any backstop step > any resilience step but the error is reported at handler level. Consider a pipeline with backstops at steps 1 and 5, and resilience at step 3 — the step-5 backstop is misordered but step-1 is fine. The error 4. Exception type for startup failure
// Callers can catch ConfigurationException and unwrap details directly
throw new PipelineValidationException(result);5. The ADR says the move "requires a 6. When Correctness Concerns7. Validation rule message wording is ambiguous The rule fires when 8. FR-3 (missing mapper for published type) not covered in rule design The requirements list FR-3: "No mapper registered for a published type" as a validation target. The ADR's Minor Issues9. Returning 10. ADR-0054 status inconsistency with PR scope ADR-0053 is "Accepted" and ADR-0054 is "Proposed". If ADR-0054 is a follow-up, it should be in a separate PR to keep scope clear. If it is intended to be accepted here, its status should be updated before merge. 11. Spec
What's Well Done
|
Address review feedback asking how the validator gets descriptions for all registered request types given that PipelineBuilder<TRequest> is generic. - Add parameterless Describe() overload that iterates all registered request types via SubscriberRegistry.GetRegisteredRequestTypes() - Add GetRegisteredRequestTypes() to IAmASubscriberRegistry alongside the existing GetHandlerTypes(Type) - Make describe-only constructor public (not internal) for testability without [InternalsVisibleTo] - Add sequence diagram showing end-to-end flow from validator through PipelineBuilder to SubscriberRegistry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…interface AttributeAsyncConsistency rule now explicitly checks both IHandleRequests and IHandleRequestsAsync rather than only checking async and implicitly treating unknown types as sync. Throws InvalidOperationException if a pipeline step handler implements neither — this is a programming error invariant, not a validation finding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: Pipeline Validation At Startup (ADR-0053 + ADR-0054)This is a design/documentation PR — two new ADRs plus the spec structure. No implementation code yet. Overall this is a well-thought-out design with good separation of concerns. A few issues worth addressing before or during implementation. Strengths
Issues1.
|
Eliminate ValidationRule<T> coordinator by enriching Specification<T> to carry its own validation metadata. Each specification now encapsulates both the predicate and the error information, using the visitor pattern to collect detailed ValidationResults from the specification graph on failure. Key changes to ADR 0053: - Rename PipelineValidationError → ValidationError (generic) - Rename PipelineValidationSeverity → ValidationSeverity - Add ValidationResult (bool Success + ValidationError) - Add ISpecificationVisitor<TData, TResult> and ValidationResultCollector - Specification<T> gains two new constructors: simple (predicate + error factory) and collapsed (Func<T, IEnumerable<ValidationResult>>) - Calling code invokes visitor after IsSatisfiedBy returns false - Not() gains overload with error factory for validation use - Update all rule examples, project layout table, and consequences Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updates Copyright © 2024 → Copyright © 2026 in all files new to this branch. Fixes ConsumerOwnsValidation XML doc to reflect that AddConsumers() unconditionally sets the flag (not only alongside ValidatePipelines). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r registry The Phase 6 fix excluded open generic handler types (e.g. ExceptionPolicyHandler<>) from assembly scanning entirely, which prevented both DI registration and subscriber registry addition. Open generics must be in DI so ServiceProviderHandlerFactory can resolve closed types at runtime, but must not appear in the subscriber registry where their generic type parameters pollute GetRegisteredRequestTypes(). Split the registration: EnsureHandlerIsRegistered() adds to DI only, Add() continues to do both. RegisterHandlersFromAssembly now routes open generics to the DI-only path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts: # .claude/settings.json # specs/.current-spec
PR Review: Pipeline Validation At StartupThis is a well-designed, spec-driven feature backed by ADRs 0053 and 0054. The architecture is clean, validation is genuinely opt-in, and the 🔴 High Priority1.
|
…der at startup Microsoft.Extensions.DependencyInjection does not support optional constructor injection. BrighterValidationHostedService now injects IServiceProvider and resolves IAmAPipelineDiagnosticWriter in StartAsync, matching the pattern used by ServiceActivatorHostedService. This prevents a DI exception when ValidatePipelines() is called without DescribePipelines(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…scribePipelines are used BrighterValidationHostedService no longer calls Describe() — that responsibility belongs exclusively to BrighterDiagnosticHostedService. This prevents the diagnostic report being logged twice when both extension methods are registered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- #7: Change IAmASubscriberRegistryInspector return types from Type[] to IReadOnlyCollection<Type> for idiomatic interface design - #3: Resolve IAmASubscriberRegistryInspector via interface in ValidatePipelines/DescribePipelines with fallback to concrete type; add PipelineBuilder describe-only constructor accepting the interface - #5: Remove duplicated XML doc sentence in BrighterPipelineValidationOptions - #4: Make MapperMethodDiscovery return MethodInfo? (nullable), remove null-forgiving operators, add descriptive ConfigurationException on build-path null - #6: Use IHandleRequests/IHandleRequestsAsync interface checks in AttributeAsyncConsistency instead of base class chain walking Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tor and diagnostic writer ValidatePipelines() and DescribePipelines() were creating PipelineValidator and PipelineDiagnosticWriter without passing publications, subscriptions, mapper registry, or consumer validation specs — so producer validation, consumer validation, and publication/subscription diagnostic logging were all silently no-ops. - Resolve IAmAProducerRegistry and IAmConsumerOptions from DI to extract publications and subscriptions at service resolution time - Resolve ISpecification<Subscription> services for consumer validation - Build MessageMapperRegistry for mapper info in diagnostic output - Register consumer validation specs (PumpHandlerMatch, HandlerRegistered, RequestTypeSubtype) in AddConsumers() - Add regression tests for producer and consumer validation wiring Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
PR Review: Pipeline Validation At Startup - see full review below |
Review: Pipeline Validation At StartupThis is a well-designed feature addressing a real pain point. The layered architecture mirrors the three Brighter configuration paths, the ADR is excellent, and test coverage is comprehensive. Below are findings organised by severity. Bugs and Correctness1. Unused The field is injected and stored but never used in 2. Async detection inconsistency between validation rules
3. The message "will never execute on failure" is not correct. When a backstop has a higher step number than a resilience handler, the backstop IS inner and DOES execute on failure — it fires on each individual attempt before resilience has exhausted its retries. The real problem is it acts too early. Suggest: Breaking Changes4. The constructor went from 2 parameters ( Design Observations5.
6. Duplicate subscriber registry resolution pattern The fallback cast via 7. Using 8. These return lambda-only Missing Coverage9. Mapper registration not validated The ADR identifies "No mapper registered for a published type" as a startup-discoverable error ( 10. Both backstops and resilience collections are drawn from Minor Notes
SummaryPriority items before merge:
Excellent ADR documentation and test coverage throughout. |
# Conflicts: # specs/.current-spec
# Conflicts: # samples/WebAPI/WebAPI_Dapper/Greetings_Sweeper/Greetings_Sweeper.csproj # samples/WebAPI/WebAPI_Dapper/Salutation_Sweeper/Salutation_Sweeper.csproj # samples/WebAPI/WebAPI_Dynamo/Greetings_Sweeper/Greetings_Sweeper.csproj # samples/WebAPI/WebAPI_EFCore/Greetings_Sweeper/Greetings_Sweeper.csproj # samples/WebAPI/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj # samples/WebAPI/WebAPI_mTLS_TestHarness/TodoApi/TodoApi.csproj
… silently ignores Mismatched sync/async attributes cause a ConfigurationException at pipeline build time, not silent ignoring. Updated error messages and XML doc to accurately reflect this behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: Pipeline Validation At StartupThis is a well-designed and well-tested feature. The architecture is sound, the ADR is thorough, and the test coverage is extensive. Below are observations ranging from bugs to minor nits. Issues1.
|
…ePipelines Allow developers to control whether pipeline validation and diagnostics run at startup (FR-14) and whether validation errors terminate the host or are logged as warnings (FR-15). Both flags use simple bool parameters so the caller can source the decision from IHostEnvironment, IConfiguration, or any other mechanism. - ValidatePipelines(bool enabled = true, bool throwOnError = true) - DescribePipelines(bool enabled = true) - BrighterPipelineValidationOptions gains ThrowOnError property - BrighterValidationHostedService respects ThrowOnError - ServiceActivatorHostedService respects ThrowOnError - Updated requirements (FR-14, FR-15, AC-13, AC-14), ADR 0053, and tasks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Health Improved
(1 files improve in Code Health)
Gates Failed
Enforce critical code health rules
(2 files with Deep, Nested Complexity, Bumpy Road Ahead)
Enforce advisory code health rules
(3 files with Complex Method)
Gates Passed
2 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| PipelineDiagnosticWriter.cs | 1 critical rule | 9.10 | Suppress |
| ServiceActivatorHostedService.cs | 2 critical rules | 10.00 → 9.24 | Suppress |
| Enforce advisory code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| PipelineDiagnosticWriter.cs | 1 advisory rule | 9.10 | Suppress |
| ConsumerValidationRules.cs | 1 advisory rule | 9.47 | Suppress |
| ServiceCollectionBrighterBuilder.cs | 1 advisory rule | 9.10 → 8.82 | Suppress |
View Improvements
| File | Code Health Impact | Categories Improved |
|---|---|---|
| TransformPipelineBuilder.cs | 9.10 → 9.39 | Code Duplication |
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.
|
|
||
| if (_options.Value.ConsumerOwnsValidation) | ||
| { | ||
| var diagnosticWriter = _serviceProvider.GetService<IAmAPipelineDiagnosticWriter>(); | ||
| diagnosticWriter?.Describe(); | ||
|
|
||
| var validator = _serviceProvider.GetService<IAmAPipelineValidator>(); | ||
| if (validator != null) | ||
| { | ||
| var result = validator.Validate(); | ||
|
|
||
| if (_options.Value.ThrowOnError) | ||
| { | ||
| result.ThrowIfInvalid(); | ||
| } | ||
| else | ||
| { | ||
| foreach (var error in result.Errors) | ||
| { | ||
| _logger.LogError("Pipeline validation error from {Source}: {Message}", error.Source, error.Message); | ||
| } | ||
| } | ||
|
|
||
| foreach (var warning in result.Warnings) | ||
| { | ||
| _logger.LogWarning("Pipeline validation warning from {Source}: {Message}", warning.Source, warning.Message); | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
❌ New issue: Bumpy Road Ahead
StartAsync has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function
|
|
||
| if (_options.Value.ConsumerOwnsValidation) | ||
| { | ||
| var diagnosticWriter = _serviceProvider.GetService<IAmAPipelineDiagnosticWriter>(); | ||
| diagnosticWriter?.Describe(); | ||
|
|
||
| var validator = _serviceProvider.GetService<IAmAPipelineValidator>(); | ||
| if (validator != null) | ||
| { | ||
| var result = validator.Validate(); | ||
|
|
||
| if (_options.Value.ThrowOnError) | ||
| { | ||
| result.ThrowIfInvalid(); | ||
| } | ||
| else | ||
| { | ||
| foreach (var error in result.Errors) | ||
| { | ||
| _logger.LogError("Pipeline validation error from {Source}: {Message}", error.Source, error.Message); | ||
| } | ||
| } | ||
|
|
||
| foreach (var warning in result.Warnings) | ||
| { | ||
| _logger.LogWarning("Pipeline validation warning from {Source}: {Message}", warning.Source, warning.Message); | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
❌ New issue: Deep, Nested Complexity
StartAsync has a nested complexity depth of 4, threshold = 4
PR Review: Pipeline Validation At StartupThis is a well-designed feature that addresses a real pain point. The layered architecture mirrors Brighter's own three configuration paths cleanly, the Specification pattern with visitor is elegant, and the test coverage is solid. The ADR documentation is thorough. Potential Bugs / Issues1. BackstopAttributeOrdering only checks BeforeSteps In HandlerPipelineValidationRules.BackstopAttributeOrdering(), the rule inspects only d.BeforeSteps. AfterSteps are silently ignored. If a user inadvertently places a backstop attribute with HandlerTiming.After, the misordering is undetected. This may be intentional (backstops are always pre-handler), but there is no guard or comment explaining why AfterSteps is excluded. 2. OrNot/Not composition methods silently short-circuit AndSpecification deliberately avoids short-circuit evaluation so the visitor can collect errors from both children — both Left and Right are always evaluated. But the OrNot and Not() helpers on Specification use short-circuit C# operators (|| and &&), meaning other._lastResults may be stale when the visitor runs. None of the current rule sets use OrNot, so this isn't currently a bug, but it creates a trap for future rule authors who compose specs with OrNot. 3. Redundant AddOptions call in ValidatePipelines() In BrighterPipelineValidationExtensions.cs, both Configure(...) and AddOptions() are called. Configure calls AddOptions internally, so the explicit AddOptions call is redundant. Harmless but noisy. Breaking Changes (worth noting in release notes)4. ServiceActivatorHostedService constructor changed The constructor gained two new required parameters (IServiceProvider and IOptions). DI resolution will still work because IOptions has a default even when unconfigured, and AddConsumers() registers BrighterPipelineValidationOptions unconditionally. However, any user instantiating ServiceActivatorHostedService directly (in test setup, or custom hosted service composition) will get a compile error. Since this is V10.X the break may be intentional, but it should be called out in the changelog. 5. Open generic handlers excluded from subscriber registry (behavior change) Previously, open generic types like ExceptionPolicyHandler<> were added to the registry with TRequest as the registered request type — which was incorrect. The fix is right: open generics are now DI-registered without being added to the subscriber registry. But any project relying on the prior broken behavior will silently change. The assembly scanning move from the builder constructor to BrighterHandlerBuilder is a good structural improvement. Design Observations / Minor Suggestions6. Mapper registration not validated for publications The ADR and PR description cite "No mapper registered for a published type" as an FR. ProducerValidationRules only checks RequestType presence and IRequest assignability — no mapper lookup is performed. This is likely deferred (mapper introspection without instantiation requires ResolveMapperInfo()), but the gap between the stated FR and what ships warrants a follow-up issue or a TODO in the code. 7. _lastResults statefulness should have an inline comment The ADR acknowledges that _lastResults makes Specification stateful and safe only because startup is single-threaded. A one-line comment on the field — "Startup validation is single-threaded; this is set by IsSatisfiedBy and read by the visitor immediately after. Not safe for concurrent use." — would help future maintainers who encounter it in isolation. 8. No description output when using ValidatePipelines() without DescribePipelines() BrighterValidationHostedService.StartAsync validates but never calls Describe(). For CQRS-only users (no ServiceActivatorHostedService), there is no automatic pipeline description at startup. When a validation error fires, there is no pipeline layout output to help diagnose the problem. This is arguably by design but worth documenting. 9. Visitor precondition (must follow IsSatisfiedBy) is undocumented The flow in PipelineValidator.EvaluateSpecs is correct — IsSatisfiedBy must precede Accept to populate _lastResults on leaf nodes. If the contract were violated, the collector would silently return empty results. A note on ValidationResultCollector or ISpecificationVisitor that it must be used after IsSatisfiedBy would prevent future misuse. What's Done Well
Overall this is ready for serious review by the core team. The items to resolve before merge are: item 1 (AfterSteps gap in backstop ordering), item 6 (mapper validation gap vs. stated FR), and item 4 (changelog entry for the constructor break). Items 2, 7, 8, and 9 are robustness suggestions, not blockers. |
We lack validation of our pipelines and the ability to discern what they do easily for diagnostics.
We need both static analysis, with the ability to request validation and receive a report, that follows a similar model to validate methods for ASP.NET or describe other .NET event processing frameworks, and dynamic analysis that extends our recent Roslyn Analyser additions.
This remains one of the weakest parts of our offering, leading to criticism that it is difficult to set Brighter up.
This will close #2176