You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Library consumers need in-process, typed request/response dispatch (1:1 command → handler) with the same compile-time registration, diagnostics, and optional DI glue already available for Strategy, Factory Registry, and Event Aggregator. MediatR already solves much of the runtime problem; what is missing in DesignPatterns is a Command Router domain whose exploration value is a closed command↔handler bijection before the first Send, plus optional ordered pipeline behaviors and stream sends as capabilities of this domain—not a second Event Aggregator, and not a combined MediatR-style ISender+IPublisher umbrella.
This work is F3 admission Top-1 from Wayfinder map #244 / ROADMAP F3 (shortlist locked in #253, written in #254 / PR #255).
Solution
Ship a Command Router pattern domain:
Runtime primitives for commands, handlers, and a router with Send / TrySend / async variants (CLR command type routing, 1:1).
[RegisterCommandHandler] (dual generic / non-generic attribute shapes, consistent with Event Aggregator) driving a source generator that emits a frozen command→handler map and router glue.
Compile-time diagnostics for duplicate handlers, contract mismatch, and an Analyzer + CodeFix for implemented-but-unregistered handlers (Event Aggregator DP044 family).
Optional pipeline behaviors (ordered wrappers around the single handler) and optional stream send mode as in-domain capabilities.
Optional MSDI / Autofac registration via existing DI integration flags and extension packages—Core remains free of MSDI.
Consumers get MediatR-shaped request dispatch with DesignPatterns-shaped compile-time bijection proofs.
User Stories
As an application developer, I want to declare a command type and a single handler for it, so that I can dispatch work without a string key registry.
As an application developer, I want Send / SendAsync to return a typed result when the command declares one, so that request/response flows are explicit.
As an application developer, I want TrySend / TrySendAsync that fail explicitly without throwing when no handler is registered at runtime (manual builder path), so that I can handle missing handlers without try/catch.
As an application developer, I want throwing Send variants with a clear exception when a handler is missing on the manual path, so that fail-fast startup/runtime behavior matches Strategy/Factory conventions.
As an application developer, I want void/unit-style commands (no result) supported, so that fire-and-forget commands still go through the same router.
As an application developer, I want async handlers with CancellationToken as a first-class parameter, so that cancellation flows through dispatch.
As an application developer, I want ValueTask-friendly handler APIs, so that hot paths avoid unnecessary allocations where the library already prefers ValueTask.
As a library consumer on netstandard2.0, I want the runtime primitives to work on the shared TFM, so that older hosts can still use Command Router.
As a library consumer on net8.0, I want frozen dictionary / optimized map implementations where the rest of the library already specializes, so that dispatch stays consistent with Strategy/Factory registries.
As a developer, I want to annotate a handler with [RegisterCommandHandler] (non-generic typeof form), so that netstandard2.0 / older C# projects can register without generic attributes.
As a developer, I want to annotate a handler with [RegisterCommandHandler<TCommand>] (or command+result form as designed), so that modern C# projects get type-safe attributes.
As a developer, I want the generator to discover registrations without assembly reflection scanning, so that AOT and trim-friendly scenarios stay aligned with AGENTS.md non-goals.
As a developer, I want generated router/registry types named consistently with ROADMAP generator naming rules, so that discoverability matches Strategy/Event Aggregator.
As a developer, I want a static (parameterless-ctor) registration path that wires handlers without DI, so that console/tools samples work like Event Aggregator SubscribeAll.
As a developer, I want a DI registration path (RegisterDi + resolve-on-send or build router from IServiceProvider) when DI integration is enabled, so that handlers can take constructor-injected dependencies.
As a developer using Microsoft.Extensions.DependencyInjection, I want an AddCommandRouter (or equivalent) extension, so that host setup matches AddEventAggregator / strategy extensions.
As a developer using Autofac, I want a symmetric registration path when Autofac extensions are referenced, so that parity with other domains is preserved.
As a developer, I want handlers to default to Transient lifetime under DI, so that behavior matches Event Aggregator handler defaults and reduces accidental captive dependencies.
As a developer, I want documentation of lifetime pitfalls (singleton router resolving scoped handlers), so that existing captive-dependency vocabulary (DP060–062 family) applies.
As a developer, I want a compile-time Error when two handlers register for the same command (and result signature as applicable), so that bijection is enforced before runtime.
As a developer, I want a compile-time Error when a attributed type does not implement the expected command handler contract, so that mismatches fail at build.
As a developer, I want an Analyzer Info (with CodeFix) when a type implements the handler interface but lacks [RegisterCommandHandler] while peers in the compilation use the attribute, so that registration is hard to forget (DP044 pattern).
As a developer, I want CodeFix to insert the correct attribute form for my TFM/language version, so that fixing DP044-style findings is one action.
As a developer, I want optional ordered pipeline behaviors around the single handler, so that cross-cutting concerns (logging, validation, transactions) compose without a separate ROADMAP domain.
As a developer, I want duplicate behavior order to be a generator Error, so that pipeline ordering stays deterministic (Chain/Decorator precedent).
As a developer, I want behaviors to be skippable in an MVP milestone if marked phase-2, so that the core bijection ships without blocking on full MediatR pipeline parity—but the domain design must leave a clear extension point.
As a developer, I want an optional stream send API (IAsyncEnumerable results) as a capability of the same router domain, so that progressive results do not require a separate Stream Request Router domain.
As a developer, I want stream support to be optional/phased so that the core 1:1 non-stream router can ship first without blocking on stream generator modes.
As a developer, I want open-generic handlers to be either supported with clear rules or explicitly rejected with diagnostics, so that MediatR-like open generics do not silently mis-bind.
As a developer writing samples, I want at least one Sample sketch (manual builder + attribute/generator path) in DesignPatterns.Samples (sibling repo, separate PR), so that ROADMAP admission criterion Allow multiple HandlerOrder attributes on a single handler class #3 stays satisfied.
As a documentation reader, I want a Design Doc CommandRouter.md plus index entry, so that API/diagnostics/trade-offs are recorded like Event Aggregator.
As a documentation reader, I want an explicit comparison table vs Event Aggregator (1:N Publish) and vs MediatR (overlap allowed), so that I know when to use which primitive.
As a documentation reader, I want CHANGELOG and ROADMAP F3 Top-1 status updates when milestones land, so that backlog state stays truthful.
As a maintainer, I want new diagnostic IDs allocated in the Diagnostics module without reusing ADR-008 reserved DP067–DP071, so that Singleton lifecycle diagnostics remain undisturbed.
As a maintainer, I want AnalyzerReleases / DiagnosticIds / descriptors updated in the same generator/analyzer PRs that introduce rules, so that AGENTS.md diagnostic process is followed.
As a maintainer, I want each solution module in its own PR (Runtime, Diagnostics, SourceGenerators, Analyzers+CodeFixes, DI, Autofac, Docs, Samples), so that AGENTS.md module boundaries hold.
As a maintainer, I want generator tests to Verify both happy-path emitted sources and diagnostic cases (duplicate, contract mismatch), so that the primary compile-time seam stays locked.
As a maintainer, I want runtime unit tests for router dispatch, concurrency expectations (document snapshot/lock semantics analogous to Event Aggregator if applicable), and Try vs throw APIs, so that primitives stay trustworthy without DI.
As a maintainer, I want analyzer tests for unregistered-handler detection and CodeFix, so that IDE experience matches Strategy/Factory/Event Aggregator.
As a maintainer, I want DI extension tests that resolve a handler via IServiceProvider and successfully Send, so that the DI path does not rot.
As an application developer, I want command and result types to allow class / struct / record, so that domain modeling is not artificially constrained.
As an application developer, I want the router to refuse silently returning null for missing handlers on the throwing API, so that AGENTS.md “explicit failure over silent null” holds.
As an application developer, I want pipeline behaviors to receive the command and a next delegate (Chain-like) or decorate the handler (Decorator-like)—whichever decision is locked in Implementation Decisions—so that writing a behavior feels familiar to existing DesignPatterns users.
As an application developer, I do not want Command Router to publish events or replace Event Aggregator, so that 1:N notifications stay on IEventAggregator.
As an application developer, I do not want correlation-id request/reply messaging as part of this domain, so that Event Aggregator’s documented non-goal remains out of Command Router too (per Wayfinder rejection of Correlated Request/Reply Messenger as a thin standalone domain).
As an AOT-minded developer, I want generated maps to be trim-friendly and free of runtime reflection scans, so that publish-time binding stays compile-declared.
As a contributor, I want XML docs on all public APIs, so that GenerateDocumentationFile / TreatWarningsAsErrors continues to pass.
As a contributor, I want file-scoped namespaces and nullable enable, so that repo coding standards apply.
As a product user evaluating MediatR vs DesignPatterns, I want the README/Design Doc to state that overlapping MediatR is intentional exploration, so that “why another mediator?” is answered up front.
As a future implementer of stream mode, I want the initial public surface to avoid painting into a corner that forbids SendStreamAsync, so that capability addition does not need a breaking redesign.
As a future implementer of pipeline behaviors, I want registration attributes and generator hooks sketched even if MVP defers behavior execution, so that tickets can sequence cleanly.
As a developer debugging dispatch, I want optional traced send (status per behavior / handler) to be considered as a follow-on enhancement consistent with Strategy/Chain/Event Aggregator tracing—not required for MVP unless cheap.
As a consumer of the metapackage, I want Runtime + generators/analyzers to ship through the existing packaging layout when Package module work is done, so that Skymly.DesignPatterns users get Command Router without a special snowflake package (Package PR only if packing changes).
As a developer using plural handlers by mistake, I want the error message to tell me which command type collided and which handler types conflict, so that the diagnostic is actionable (AGENTS.md messageFormat guidance).
As a developer on a multi-project solution, I want registration discovery rules documented (same compilation / referenced generators behavior consistent with other Register* attributes), so that cross-assembly expectations match DP033-style thinking where applicable.
As a security-conscious developer, I want pipeline behaviors not to become a plugin sandbox or remoting layer, so that the domain stays an in-process primitive.
As a tester, I want tests to assert external behavior (dispatch results, diagnostics IDs/messages, generated public API shapes via Verify) rather than private generator helpers, so that refactors do not false-fail.
As a release manager, I want no version bump/tag/publish as part of feature PRs unless explicitly requested, so that AGENTS.md release policy holds.
vs Event Aggregator: EA = 1:N Publish notifications, no response. Command Router = 1:1 Send with response. Do not unify into one MediatR-like façade in this spec.
vs Strategy: Strategy is string-keyed interchangeable algorithms; Command Router is CLR-type request dispatch. No string Keys required for the default path.
vs Chain/Decorator: reuse their ordering / onion ideas for pipeline behaviors; do not require commands to be modeled as Chain contexts.
vs MediatR: overlap allowed; differentiation is attribute→frozen map + bijection diagnostics + DesignPatterns DI/generator conventions.
Modules (one PR each)
Runtime: command/handler/router primitives and exceptions under Behavioral (or agreed namespace consistent with Event Aggregator).
Diagnostics: new DP### IDs/descriptors (do not consume ADR-008 reserved DP067–DP071; allocate from the next free ID after that reservation, currently expected DP072+, confirming against DiagnosticIds.cs at implementation time).
SourceGenerators: RegisterCommandHandler generator (+ later pipeline/stream generators or modes).
Samples (sibling repo): sample project in a separate PR when API is usable.
Package: only if metapackage packing must change.
API contracts (conceptual)
Commands: marker / ICommand and ICommand<TResult> (exact interface names to match DesignPatterns naming on implement).
Handlers: ICommandHandler<TCommand> / ICommandHandler<TCommand,TResult> with async CancellationToken.
Router: ICommandRouter with Send / TrySend / async twins; explicit failure modes (no silent null).
Attributes: [RegisterCommandHandler] non-generic + generic forms; optional later [CommandPipelineBehavior(order)].
Manual builder remains available when no generator is used (Strategy/Event Aggregator precedent).
Pipeline shape (when phase 3 lands)
Prefer Chain-like next delegate onion around the terminal handler (familiar to IHandler<T> users) or Decorator-like wrap of the handler instance—implementer picks one and documents it in the Design Doc; do not ship both competing models.
Behaviors are ordered; duplicate order = Error.
Stream shape (when phase 3 lands)
Second generator/router mode binding stream handlers to SendStreamAsync-style API; non-stream MVP APIs must remain valid.
Architectural constraints (AGENTS.md)
Primitives over frameworks; Core without MSDI; async first-class; dual TFM netstandard2.0 + net8.0; Roslyn 4.8.0 baseline; nullable enable; TreatWarningsAsErrors; English for issues/PRs/commits.
No AppDomain reflection registration scans.
Diagnostic help links via existing DiagnosticHelpLinks pattern; Docs site diagnostics pages may lag in sibling Docs repo.
Testing Decisions (seams — confirmed)
What makes a good test here: assert observable external behavior only—dispatch results and failure modes, diagnostic IDs/severities/messages, and Verify snapshots of generated public sources—not private generator helpers or incidental formatting churn.
Primary seam (highest): DesignPatterns.SourceGenerators.Tests Verify of generated command↔handler map and bijection diagnostics (happy registry/router glue; duplicate handler Error; contract mismatch Error; optional DI RegisterDi snapshot when that phase lands). Prior art: RegisterEventHandlerGenerator Verify tests; Strategy/Factory registry Verify tests.
Secondary seams (later module tickets, do not elevate above primary):
Research notes (throwaway branches): docs/.Local/research/messaging-command-candidates.md, docs/.Local/research/compile-time-synergy-passers.md § Command Router.
Closest shipped sibling for copy-shape: Event Aggregator (type routing + Register* + DP044–046 family), with Strategy/Factory for registry/DI Verify patterns and Chain/Decorator for ordered pipeline capability.
After this spec: /to-tickets should split MVP → Analyzer/DI → pipeline/stream into tracer-bullet issues with native blocking edges; each /implement in a fresh session.
Confirmed test seam choice (maintainer): primary = SourceGenerators Verify bijection; secondaries as listed above.
Problem Statement
Library consumers need in-process, typed request/response dispatch (1:1 command → handler) with the same compile-time registration, diagnostics, and optional DI glue already available for Strategy, Factory Registry, and Event Aggregator. MediatR already solves much of the runtime problem; what is missing in DesignPatterns is a Command Router domain whose exploration value is a closed command↔handler bijection before the first
Send, plus optional ordered pipeline behaviors and stream sends as capabilities of this domain—not a second Event Aggregator, and not a combined MediatR-styleISender+IPublisherumbrella.This work is F3 admission Top-1 from Wayfinder map #244 / ROADMAP F3 (shortlist locked in #253, written in #254 / PR #255).
Solution
Ship a Command Router pattern domain:
Send/TrySend/ async variants (CLR command type routing, 1:1).[RegisterCommandHandler](dual generic / non-generic attribute shapes, consistent with Event Aggregator) driving a source generator that emits a frozen command→handler map and router glue.Consumers get MediatR-shaped request dispatch with DesignPatterns-shaped compile-time bijection proofs.
User Stories
Send/SendAsyncto return a typed result when the command declares one, so that request/response flows are explicit.TrySend/TrySendAsyncthat fail explicitly without throwing when no handler is registered at runtime (manual builder path), so that I can handle missing handlers without try/catch.Sendvariants with a clear exception when a handler is missing on the manual path, so that fail-fast startup/runtime behavior matches Strategy/Factory conventions.CancellationTokenas a first-class parameter, so that cancellation flows through dispatch.ValueTask-friendly handler APIs, so that hot paths avoid unnecessary allocations where the library already prefersValueTask.[RegisterCommandHandler](non-generictypeofform), so that netstandard2.0 / older C# projects can register without generic attributes.[RegisterCommandHandler<TCommand>](or command+result form as designed), so that modern C# projects get type-safe attributes.SubscribeAll.RegisterDi+ resolve-on-send or build router fromIServiceProvider) when DI integration is enabled, so that handlers can take constructor-injected dependencies.AddCommandRouter(or equivalent) extension, so that host setup matchesAddEventAggregator/ strategy extensions.[RegisterCommandHandler]while peers in the compilation use the attribute, so that registration is hard to forget (DP044 pattern).IAsyncEnumerableresults) as a capability of the same router domain, so that progressive results do not require a separate Stream Request Router domain.CommandRouter.mdplus index entry, so that API/diagnostics/trade-offs are recorded like Event Aggregator.IServiceProviderand successfullySend, so that the DI path does not rot.class/struct/record, so that domain modeling is not artificially constrained.nextdelegate (Chain-like) or decorate the handler (Decorator-like)—whichever decision is locked in Implementation Decisions—so that writing a behavior feels familiar to existing DesignPatterns users.IEventAggregator.GenerateDocumentationFile/ TreatWarningsAsErrors continues to pass.SendStreamAsync, so that capability addition does not need a breaking redesign.Skymly.DesignPatternsusers get Command Router without a special snowflake package (Package PR only if packing changes).Implementation Decisions
Scope and phasing
[RegisterCommandHandler]dual shapes; generator frozen CLR-type map;Send/TrySend(+ async); duplicate + contract-mismatch generator diagnostics; Design Doc; unit + generator Verify tests.RegisterDi/AddCommandRouter; Autofac parity.Boundary vs siblings
Publishnotifications, no response. Command Router = 1:1Sendwith response. Do not unify into one MediatR-like façade in this spec.Keysrequired for the default path.Modules (one PR each)
DP###IDs/descriptors (do not consume ADR-008 reserved DP067–DP071; allocate from the next free ID after that reservation, currently expected DP072+, confirming againstDiagnosticIds.csat implementation time).RegisterCommandHandlergenerator (+ later pipeline/stream generators or modes).API contracts (conceptual)
ICommandandICommand<TResult>(exact interface names to match DesignPatterns naming on implement).ICommandHandler<TCommand>/ICommandHandler<TCommand,TResult>with asyncCancellationToken.ICommandRouterwithSend/TrySend/ async twins; explicit failure modes (no silent null).[RegisterCommandHandler]non-generic + generic forms; optional later[CommandPipelineBehavior(order)].Pipeline shape (when phase 3 lands)
nextdelegate onion around the terminal handler (familiar toIHandler<T>users) or Decorator-like wrap of the handler instance—implementer picks one and documents it in the Design Doc; do not ship both competing models.Stream shape (when phase 3 lands)
SendStreamAsync-style API; non-stream MVP APIs must remain valid.Architectural constraints (AGENTS.md)
Testing Decisions (seams — confirmed)
What makes a good test here: assert observable external behavior only—dispatch results and failure modes, diagnostic IDs/severities/messages, and Verify snapshots of generated public sources—not private generator helpers or incidental formatting churn.
Primary seam (highest):
DesignPatterns.SourceGenerators.TestsVerify of generated command↔handler map and bijection diagnostics (happy registry/router glue; duplicate handler Error; contract mismatch Error; optional DIRegisterDisnapshot when that phase lands). Prior art:RegisterEventHandlerGeneratorVerify tests; Strategy/Factory registry Verify tests.Secondary seams (later module tickets, do not elevate above primary):
DesignPatterns.Analyzers.Tests— unregistered handler Analyzer + CodeFix (prior art: DP044/DP006/DP023/DP024).DesignPatterns.Tests— runtime router unit tests (prior art: Event Aggregator / Strategy registry tests).DesignPatterns.Extensions.DependencyInjection.Tests(+ Autofac tests) —Sendvia provider-built router (prior art: Event Aggregator / Strategy DI tests).Modules under test: SourceGenerators (primary), then Runtime, Analyzers/CodeFixes, DI/Autofac as those PRs land.
Out of Scope
Further Notes
docs/.Local/research/messaging-command-candidates.md,docs/.Local/research/compile-time-synergy-passers.md§ Command Router./to-ticketsshould split MVP → Analyzer/DI → pipeline/stream into tracer-bullet issues with native blocking edges; each/implementin a fresh session.