版本:v0.2.4-preview1 关联 Issue:#256(Spec)、#260(本 Design Doc)、#282 / #265 / #283(stream send)
Command Router 提供进程内、轻量的 1:1 命令分发:按命令 CLR 类型路由到唯一已注册的 handler,支持无返回值、带 TResult、以及 IAsyncEnumerable<TItem> stream 三种契约,以及显式失败的 SendAsync / TrySendAsync / SendStreamAsync / TrySendStreamAsync。
与 Event Aggregator(1:N pub/sub)互补:后者按事件类型扇出到多个订阅者;本域按命令类型双射到单个 handler。Stream 是同一域内能力,不是独立的 Stream Request Router。
- 最小 API:
SendAsync/TrySendAsync/SendStreamAsync/TrySendStreamAsync+ 手动CommandRouterBuilder - 1:1 双射:每个命令 CLR 类型至多一个 handler(void 或 result 或 stream);编译期用 DP073/DP074 强制,运行时用 builder 拒绝重复注册
- 异步一等:
HandleAsync/SendAsync返回ValueTask;stream 返回IAsyncEnumerable<TItem>;均接受CancellationToken - 显式失败:缺失 handler 时
Send*/SendStream*抛CommandHandlerNotFoundException;TrySend*/TrySendStream*返回false/CommandSendAttempt.Failed - 不侵入 Core 的 DI 依赖;可选
AddCommandRouter与生成器RegisterDi - 编译期胶水:
[RegisterCommandHandler](含 stream handler)→{Command}CommandHandlerRegistry;无字符串*Keys(路由键为 CLR 类型)
| 类型 | 职责 |
|---|---|
ICommand / ICommand<out TResult> |
可选标记接口(约定/文档用;不参与路由) |
ICommandHandler<in TCommand> |
无返回值 handler |
ICommandHandler<in TCommand, TResult> |
带结果 handler |
IStreamCommandHandler<in TCommand, out TItem> |
stream handler(IAsyncEnumerable<TItem>) |
ICommandRouter |
按命令类型分发(含 stream) |
CommandRouter |
默认不可变实现 |
CommandRouterBuilder |
手动注册 → Build() |
CommandSendAttempt<TResult> |
TrySendAsync / TrySendStreamAsync 结果包装 |
CommandHandlerNotFoundException |
Send* / SendStream* 缺失 handler |
命名空间:DesignPatterns.Behavioral。
public interface ICommand { }
public interface ICommand<out TResult> { }路由键是 TCommand 的 CLR 类型,不要求实现上述标记。
public interface ICommandHandler<in TCommand>
{
ValueTask HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}
public interface ICommandHandler<in TCommand, TResult>
{
ValueTask<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}
public interface IStreamCommandHandler<in TCommand, out TItem>
{
IAsyncEnumerable<TItem> HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}IAsyncEnumerable<T> 在 netstandard2.0 经 Microsoft.Bcl.AsyncInterfaces 提供(与运行时包依赖一致)。
public interface ICommandRouter
{
ValueTask SendAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default);
ValueTask<TResult> SendAsync<TCommand, TResult>(
TCommand command,
CancellationToken cancellationToken = default);
ValueTask<bool> TrySendAsync<TCommand>(
TCommand command,
CancellationToken cancellationToken = default);
ValueTask<CommandSendAttempt<TResult>> TrySendAsync<TCommand, TResult>(
TCommand command,
CancellationToken cancellationToken = default);
IAsyncEnumerable<TItem> SendStreamAsync<TCommand, TItem>(
TCommand command,
CancellationToken cancellationToken = default);
CommandSendAttempt<IAsyncEnumerable<TItem>> TrySendStreamAsync<TCommand, TItem>(
TCommand command,
CancellationToken cancellationToken = default);
}| API | 缺失 handler | 已注册但契约不符 |
|---|---|---|
SendAsync* |
CommandHandlerNotFoundException |
InvalidOperationException |
TrySendAsync(void) |
false |
InvalidOperationException |
TrySendAsync(result) |
CommandSendAttempt<TResult>.Failed |
InvalidOperationException |
SendStreamAsync |
CommandHandlerNotFoundException |
InvalidOperationException |
TrySendStreamAsync |
CommandSendAttempt<IAsyncEnumerable<TItem>>.Failed |
InvalidOperationException |
契约不符包括:对同一 TCommand 用 void/result API 调用 stream handler,或用 SendStreamAsync 调用非 stream handler(含 TItem 类型不匹配)。
var router = new CommandRouterBuilder()
.Register(new PingHandler())
.Register<GetTotalCommand, decimal>(new GetTotalHandler())
.Register<EnumerateItemsCommand, string>(new EnumerateItemsStreamHandler())
.Build();
await router.SendAsync(new PingCommand());
var total = await router.SendAsync<GetTotalCommand, decimal>(new GetTotalCommand());
await foreach (var item in router.SendStreamAsync<EnumerateItemsCommand, string>(new EnumerateItemsCommand()))
{
// ...
}- Builder 非线程安全;
Build()后的CommandRouter对并发Send*/SendStream*安全。 - 同一
TCommand重复Register(含 void/result/stream 混注)→ArgumentException。 - net8.0 实现可用
FrozenDictionary优化只读映射。
| 特性 | 形态 | 适用 TFM |
|---|---|---|
[RegisterCommandHandler(typeof(FooCommand))] |
非泛型 | 全部(netstandard2.0+) |
[RegisterCommandHandler<FooCommand>] |
泛型 | #if NET7_0_OR_GREATER(C# 11+ generic attributes) |
AttributeUsage:Class,Inherited = false,AllowMultiple = true。
同一 handler 上泛型 + 非泛型指向同一命令类型时,生成器会去重;不同 handler 争用同一命令(含 stream 与 non-stream 争用)→ DP073。
Stream handler 复用同一 [RegisterCommandHandler] / [RegisterCommandHandler<TCommand>];不引入独立 stream 特性。实现 IStreamCommandHandler<TCommand, TItem> 且标注该特性即可驱动 registry 绑定到 Register 的 stream 重载。
RegisterCommandHandlerGenerator 扫描 [RegisterCommandHandler],按命令类型生成 {Command}CommandHandlerRegistry(剥离末尾 Command 后缀后再拼接,例如 PingCommand → PingCommandHandlerRegistry)。契约可为 ICommandHandler<*> 或 IStreamCommandHandler<,>:
| 成员 | 路径 | 条件 |
|---|---|---|
RegisterAll(CommandRouterBuilder) |
静态:new Handler() + Register |
handler 有公共无参构造 |
CreateRouter() |
静态:内部 RegisterAll + Build |
同上 |
RegisterDi(IServiceCollection, ServiceLifetime) |
DI:注册 handler 实现 | DesignPatterns_EnableDiIntegration=true |
RegisterAll(CommandRouterBuilder, IServiceProvider) |
DI:从容器解析 + Register |
同上 |
RegisterAutofac(ContainerBuilder) |
Autofac 胶水 | Autofac 集成 flag |
RegisterAll(CommandRouterBuilder, ILifetimeScope) |
Autofac 胶水 | 同上 |
不生成字符串 *Keys(与 Strategy / Factory 不同):路由键仅为 CLR 类型。
静态路径仅含公共无参构造的 handler;DI/Autofac 路径包含全部有效 handler(与其它生成器同源约定)。
[RegisterCommandHandler<PingCommand>]
public sealed class PingHandler : ICommandHandler<PingCommand>
{
public ValueTask HandleAsync(PingCommand command, CancellationToken ct = default) => default;
}
var router = PingCommandHandlerRegistry.CreateRouter();
await router.SendAsync(new PingCommand());PingCommandHandlerRegistry.RegisterDi(services); // 默认 Transient
services.AddCommandRouter((builder, sp) =>
PingCommandHandlerRegistry.RegisterAll(builder, sp));AddCommandRouter 默认将 ICommandRouter 注册为 Singleton:构建时冻结 handler 映射。RegisterDi 默认 Transient 不表示每次 Send 新实例——Singleton router 在 Build 时解析并持有 handler(captive 语义与 Event Aggregator 的 SubscribeAll(aggregator, provider) 同类;相关诊断见 DP060–DP062 / DP066)。
var builder = new ContainerBuilder();
PingCommandHandlerRegistry.RegisterAutofac(builder); // 默认 InstancePerDependency
builder.RegisterCommandRouter((routerBuilder, scope) =>
PingCommandHandlerRegistry.RegisterAll(routerBuilder, scope));RegisterCommandRouter 默认 InstanceSharing.Shared(singleton),captive 语义与 MSDI AddCommandRouter / Event Aggregator Autofac SubscribeAll(aggregator, lifetimeScope) 同类。
Pipeline behaviors(void / result;ADR-009)
按 ADR-009(实现 #275–#277 / #264):
- Chain-like
next洋葱(不做 Decorator wrap;不复用HandlerPipeline) - 双接口对齐 void/result handler:
ICommandPipelineBehavior<TCommand>/ICommandPipelineBehavior<TCommand, TResult> - 按命令封闭注册:
[CommandPipelineBehavior<TCommand>(order)](AllowMultiple);越小越先 inbound - 短路:void 不调
next即停;result 用返回值(MediatR 形) - 手动
CommandRouterBuilder一等支持;本批不做 behavior DI / 未注册 Analyzer - 生成器 Error:重复 order(DP075)、孤儿 behavior(DP076)、契约不匹配(DP077)
- 本批无 stream pipeline behaviors:
UseBehavior/[CommandPipelineBehavior]仅包裹 void/result 终端;对 stream handler 调用UseBehavior时,Build因契约不匹配抛InvalidOperationException(生成器路径亦要求 void/result 终端契约)
- 同一域:不单独建 Stream Request Router;API 落在
ICommandRouter/CommandRouterBuilder - 双射:每个
TCommand至多一个 handler——void、result、stream 三选一;stream + non-stream 争用同一命令 → DP073 / builderArgumentException - 生成器:复用
[RegisterCommandHandler];DP074 文案含IStreamCommandHandler<,>;静态 + DIRegisterDi与 void/result 对等 - 非目标(本批):stream pipeline behaviors;独立 stream 域 / 独立路由枢纽
| ID | 级别 | 归属 | 触发条件 | 消息要点 |
|---|---|---|---|---|
| DP072 | Info | Analyzer + CodeFix | 实现 ICommandHandler<*> 但未标 [RegisterCommandHandler],且编译内已存在该命令类型的同伴注册 |
提示补特性 |
| DP073 | Error | Generator | 两个不同 handler 声明同一命令类型(含 stream 与 non-stream) | 保持 1:1 双射 |
| DP074 | Error | Generator | 标注了特性但未实现 ICommandHandler<*> / IStreamCommandHandler<,> |
修正契约或 For 类型 |
| DP075 | Error | Generator | 同一 command 上多个 [CommandPipelineBehavior] 共用同一 order |
保持洋葱顺序确定 |
| DP076 | Error | Generator | [CommandPipelineBehavior] 声明的 command 无终端 [RegisterCommandHandler] |
补 handler 或移除 behavior |
| DP077 | Error | Generator | 标注了特性但未实现对应 ICommandPipelineBehavior 契约 |
修正契约或命令类型参数 |
- 扫描当前编译(含引用程序集)中已有
[RegisterCommandHandler]的命令类型集合。 - 若集合为空 → 不报告任何 DP072(避免在尚未采用生成器路径的项目中噪声)。
- 否则:对实现该集合中命令契约、却缺少匹配特性的具体非抽象类报告 Info。
- 跳过抽象类、私有嵌套类。
CodeFix:在 C# 11+ 且元数据可用时优先插入 [RegisterCommandHandler<TCommand>],否则 [RegisterCommandHandler(typeof(TCommand))]。
- 1:1:每个命令 CLR 类型至多一个 handler——void、result 或 stream(builder 与 DP073)。
- Router 在
Build后不可变;并发Send*/SendStream*安全。 - 缺失 handler:
SendAsync/SendStreamAsync抛异常;TrySendAsync/TrySendStreamAsync不抛(仅缺失场景)。 - 已注册但契约不符(void / result / stream /
TItem):对应Send*/TrySend*/SendStream*/TrySendStream*抛InvalidOperationException。 ICommand/ICommand<TResult>不强制;生成器与运行时均不校验标记接口。- Pipeline behaviors 仅适用于 void/result 终端;stream 无 pipeline(本批非目标)。
- netstandard2.0 / net8.0(运行时核心,两者均须可用并随包分发);stream 在 netstandard2.0 依赖
Microsoft.Bcl.AsyncInterfaces - Roslyn 组件基线 4.8.0
- DI:独立包
DesignPatterns.Extensions.DependencyInjection,services.AddCommandRouter(...) - Autofac:独立包
DesignPatterns.Extensions.Autofac,生成器RegisterAutofac/RegisterAll(..., ILifetimeScope)+ 打包扩展RegisterCommandRouter(...)(#263)
CommandRouter 持有 IReadOnlyDictionary<Type, object>(net8.0 可冻结)。CommandRouterBuilder 在字典中写入 handler 实例;Build 复制为只读映射,并可把 void/result pipeline onion 冻入终端 handler。
分发时按 typeof(TCommand) 查找;void / result / stream 分别转型为 ICommandHandler<TCommand> / ICommandHandler<TCommand, TResult> / IStreamCommandHandler<TCommand, TItem>。
RegisterCommandHandlerGenerator(IIncrementalGenerator + ForAttributeWithMetadataName):
- 收集非泛型 / 泛型特性标注。
- 按命令类型分组;去重同 handler 双形态;异 handler 冲突(含 stream vs non-stream)→ DP073,且不为该命令发 registry。
- 契约不匹配(无
ICommandHandler<*>且无IStreamCommandHandler<,>)→ DP074。 - 生成
{Command}CommandHandlerRegistry(命名空间 = 命令类型命名空间);stream 走 arity-2Register。
| ID | 归属 | 语义 |
|---|---|---|
| DP072 | Analyzer | 未注册实现(peer-presence Info)+ CodeFix |
| DP073 | Generator | 重复命令(双射破坏) |
| DP074 | Generator | 契约不匹配 |
| DP075 | Generator | pipeline behavior 重复 order |
| DP076 | Generator | pipeline behavior 无终端 handler(孤儿) |
| DP077 | Generator | pipeline behavior 契约不匹配 |
命令分发天然是封闭类型集合;字符串键会复制 Strategy/Factory 的横切约定却无收益。因此不生成 *Keys,也不引入 DP025 类字面量键校验。
Command Router 在构建后冻结映射,适合「启动时装配、运行时只读发送」。Event Aggregator 保留运行时 Subscribe / Unsubscribe,适合动态订阅。两者故意不对齐可变性模型。
公开 API 统一异步语义:*Async 后缀用于 ValueTask 路径;stream 用 SendStreamAsync / TrySendStreamAsync(返回 IAsyncEnumerable / CommandSendAttempt,非 ValueTask)。规格叙述里的 Send / TrySend 指语义族,而非同步重载——无同步 Send。
AllowMultiple = true 允许同一类声明多个命令,或泛型/非泛型双写;双射约束落在「每个命令类型恰好一个 handler 类型」,由 DP073 在编译期强制。
本库以技术探索为目的,允许与 MediatR 能力重叠。下表用于说明当前实现的设计取向差异,供选型参考,非「不实现」的理由(见 AGENTS.md「项目是什么」)。
| Command Router | Event Aggregator | |
|---|---|---|
| 基数 | 1:1 命令 → handler | 1:N 事件 → handlers |
| 枢纽 | ICommandRouter |
IEventAggregator |
| 分发 | SendAsync / TrySendAsync / SendStreamAsync / TrySendStreamAsync |
PublishAsync(无请求/响应) |
| 可变性 | Build 后不可变 |
运行时 Subscribe / Unsubscribe |
| 重复语义 | DP073:异 handler 同命令 → Error | DP045:同 handler 同事件重复标注 → Error;允许多 handler |
| 未注册 | DP072(peer-presence;当前覆盖 void/result 实现) | DP044(peer-presence) |
| 契约 | DP074(含 stream) | DP046 |
| DI / Autofac | AddCommandRouter / RegisterCommandRouter |
AddEventAggregator(Autofac 仅生成器 SubscribeAll) |
| Command Router | 典型 MediatR | |
|---|---|---|
| 范围 | 进程内、编译期双射 + 显式 Try* + 域内 pipeline/stream | 请求/通知、行为管道、流式请求等完整媒介 |
| 路由 | CLR 命令类型 → 单 handler(void / result / stream 三选一) | Request/Notification + pipeline behaviors |
| 编译期 | [RegisterCommandHandler] + DP072–074(stream 复用同一特性) |
通常约定扫描或显式注册;诊断模型不同 |
| 管道 | Chain-like next(ADR-009;#275–#277 / #264);无 stream pipeline |
IPipelineBehavior 一等公民(含可作用于 stream 的生态扩展) |
| 流式 | 已落地:IStreamCommandHandler + SendStreamAsync / TrySendStreamAsync(#282 / #265);同域、同双射 |
IStreamRequest / CreateStream 等一等公民 |
重叠被允许:本域的探索重点是「编译期双射证明 + 显式失败 primitives」,而非替代 MediatR 产品面。
- 不做跨进程 / 持久化 / 重试策略
- 不做请求/响应关联 ID
- 不做独立 Stream Request Router 域(stream 已并入本域)
- 本批不做 stream pipeline behaviors;void/result pipeline 见 ADR-009
- 本批不做 traced send(后续可选域内能力)
- DP072 peer-presence 当前针对
ICommandHandler<*>;stream 未注册提示若需对称覆盖可另开 Analyzer 切片 - Samples 在 sibling 仓跟踪 #266
- 进程内 Mediator / Command 变体(Behavioral)
- EventAggregator.md — 1:N 对照
- AGENTS.md — 项目规则与诊断表
- docs/DEVELOPMENT.md — 通用开发约定
- docs/ROADMAP.md F3 Top-1
- Spec:#256
- 落地切片:#257–#263(Diagnostics → Runtime → Generator → Analyzer → DI → Autofac);pipeline #275–#277 / #264;stream #282 / #265 / #283