diff --git a/AGENTS.md b/AGENTS.md index 544c576..5457451 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,7 +177,7 @@ dotnet test DesignPatterns.slnx -c Release | DP083 | GenerateBuilder 重复步骤(生成器;同名 `[BuilderStep]` 重复) | | DP084 | GenerateBuilder 未知步骤引用(生成器;After/Before 指向不存在的步骤) | | DP085 | GenerateBuilder holder 非法(生成器;`[GenerateBuilder]` 目标类型不合法) | -| DP086 | GenerateBuilder 装配契约不匹配(生成器;assemble 签名/重复标注非法) | +| DP086 | GenerateBuilder 装配契约不匹配(生成器;恰好一个 assemble,返回同步 `T` 或 `Task`/`ValueTask`;拒绝 void、裸 `Task`/`ValueTask`、多个 `CancellationToken`、重复标注) | | DP087 | Work Graph 依赖环(生成器;DependsOn 形成 cycle) | | DP088 | Work Graph 未知 DependsOn(生成器;引用未声明的 step id) | | DP089 | Work Graph 重复 step id(生成器;同一 holder 下 Id 重复) | diff --git a/CHANGELOG.md b/CHANGELOG.md index c292414..957d956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Step Builder ADR-011 + Design Doc**: records async-assemble exclusivity (`Task` / `ValueTask` emits `BuildAsync` only; product type is the inner `T`; cancellation-token rules; DP086 covers the illegal contracts) without superseding ADR-010; Design Doc, ROADMAP, and the DP086 registry line now match the Phase 2 exit ([#327](https://github.com/Skymly/DesignPatterns/issues/327), Spec [#324](https://github.com/Skymly/DesignPatterns/issues/324)). - **Work Graph Design Doc**: `docs/design/WorkGraph.md` documents dual-path API (`WorkGraphBuilder` / `[WorkGraph]`+`[WorkStep]` / Keys+`Create`), DP087–DP092, wave fail-fast execution, and non-goals vs Channel Pipeline / Composite parallel / Step Builder / TPL Dataflow; design index, ROADMAP F3 Top-3, and AGENTS pattern summary updated ([#312](https://github.com/Skymly/DesignPatterns/issues/312), Spec [#308](https://github.com/Skymly/DesignPatterns/issues/308)). - **Work Graph generator**: `[WorkGraph]` / `[WorkGraph]` + `[WorkStep]` drive `WorkGraphGenerator`, emitting `{Holder}WorkStepKeys` and `{Holder}WorkGraph.Create(resolver|dictionary)` that fills `WorkGraphBuilder`. Reports **DP087–DP092** (cycle, unknown DependsOn, duplicate id, self-dependency, unreachable Warning, contract mismatch); no MVP DI/Autofac emission ([#311](https://github.com/Skymly/DesignPatterns/issues/311), Spec [#308](https://github.com/Skymly/DesignPatterns/issues/308)). - **Work Graph diagnostic IDs**: **DP087–DP092** for the Work Graph generator (dependency cycle, unknown DependsOn, duplicate step id, self-dependency, unreachable step Warning, contract/TContext mismatch); no unregistered-`IWorkStep` Analyzer in MVP; DP067–DP071 remain ADR-008-only ([#310](https://github.com/Skymly/DesignPatterns/issues/310), Spec [#308](https://github.com/Skymly/DesignPatterns/issues/308)). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 2305e3e..1d7c5ca 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -106,7 +106,7 @@ | 名次 | 候选 | 说明 | 状态 | |------|------|------|------| | 1 | **Command Router** | MVP + 域内增强已落地:1:1 `SendAsync`/`TrySendAsync`/`SendStreamAsync`/`TrySendStreamAsync` + `[RegisterCommandHandler]`(含 `IStreamCommandHandler`)+ DP072–077 + pipeline(ADR-009)+ `AddCommandRouter` / Autofac `RegisterCommandRouter` + [Design Doc](design/CommandRouter.md)(#257–#265、#275–#277、#282–#283)。Samples #266;探索与 MediatR 的差异点见 Design Doc | [~] | -| 2 | **Builder**(Step Builder) | **MVP 完成**:`[GenerateBuilder]` / `[BuilderStep]` / `[BuilderAssemble]` + 泛型 type-state `{Holder}Builder`([ADR-010](adr/ADR-010-step-builder-type-state-markers.md))+ DP078–DP086 + [Design Doc](design/StepBuilder.md)(#288–#291)+ Samples [DesignPatterns.Samples#23](https://github.com/Skymly/DesignPatterns.Samples/issues/23) / [PR#24](https://github.com/Skymly/DesignPatterns.Samples/pull/24)。Spec [#287](https://github.com/Skymly/DesignPatterns/issues/287)。Phase 2+:async / DI / step validation | [x] | +| 2 | **Builder**(Step Builder) | **MVP 完成**:`[GenerateBuilder]` / `[BuilderStep]` / `[BuilderAssemble]` + 泛型 type-state `{Holder}Builder`([ADR-010](adr/ADR-010-step-builder-type-state-markers.md))+ DP078–DP086 + [Design Doc](design/StepBuilder.md)(#288–#291)+ Samples [DesignPatterns.Samples#23](https://github.com/Skymly/DesignPatterns.Samples/issues/23) / [PR#24](https://github.com/Skymly/DesignPatterns.Samples/pull/24)。Spec [#287](https://github.com/Skymly/DesignPatterns/issues/287)。Phase 2 async 已落地:`Task` / `ValueTask` assemble 只生成 `BuildAsync`([ADR-011](adr/ADR-011-step-builder-async-assemble.md);#325–#327,Spec [#324](https://github.com/Skymly/DesignPatterns/issues/324))。剩余 Phase 2+:DI / step validation。Async sample 仍待 [DesignPatterns.Samples#29](https://github.com/Skymly/DesignPatterns.Samples/issues/29) | [x] | | 3 | **Fork–Join Work Graph** | **MVP 完成**:`IWorkStep` / `IWorkGraph` / `WorkGraphBuilder` + `[WorkGraph]` / `[WorkStep]` + `WorkGraphGenerator`(`{Holder}WorkStepKeys` + `Create`)+ DP087–DP092 + [Design Doc](design/WorkGraph.md)(#309–#312)+ Samples [DesignPatterns.Samples#25](https://github.com/Skymly/DesignPatterns.Samples/issues/25) / [PR#28](https://github.com/Skymly/DesignPatterns.Samples/pull/28)。Spec [#308](https://github.com/Skymly/DesignPatterns/issues/308)。Phase 2+:Dop / 追踪 / DI / 未注册 Analyzer | [x] | #### 观望(已过硬门槛,未进 Top-3) @@ -245,4 +245,5 @@ State 转换表 v1 已于 0.1.0-preview4 发布;v2(guard 委托、DI 集成 - F5+ Composite 树 schema 校验:`[CompositeSchema(MaxDepth, MaxNodes)]` 契约级约束 + `[CompositePart(AllowedChildTypes)]` 节点级约束 + DP063(max depth exceeded Warning)/ DP064(child type not allowed Error)/ DP065(node count exceeded Warning)。见 [ADR-007](adr/ADR-007-composite-tree-schema-validation.md)。 - 文档体系精简:移除 RFC / Spec / Plan / Review 仓内类型;Spec 合入 Design Doc;任务与审查改用 GitHub Issue / PR。见 [DOCUMENTATION.md](DOCUMENTATION.md)。 - F3 Step Builder MVP:`[GenerateBuilder]` / `[BuilderStep]` / `[BuilderAssemble]` + type-state `{Holder}Builder`(ADR-010)+ DP078–DP086 + Design Doc(#288–#291)+ Samples [DesignPatterns.Samples#23](https://github.com/Skymly/DesignPatterns.Samples/issues/23);Spec [#287](https://github.com/Skymly/DesignPatterns/issues/287)。 +- F3 Step Builder Phase 2 async assemble:`Task` / `ValueTask` 只生成 type-state 门闩的 `BuildAsync`([ADR-011](adr/ADR-011-step-builder-async-assemble.md),不取代 ADR-010)+ DP086 文案(#325)+ 生成器(#326)+ Design Doc(#327)。Spec [#324](https://github.com/Skymly/DesignPatterns/issues/324)。Async sample 仍待 [DesignPatterns.Samples#29](https://github.com/Skymly/DesignPatterns.Samples/issues/29)。 - F3 Fork–Join Work Graph MVP:Runtime builder + 波次 fail-fast(#309)+ DP087–DP092(#310)+ Keys/`Create` 生成器(#311)+ Design Doc(#312)+ Samples [DesignPatterns.Samples#25](https://github.com/Skymly/DesignPatterns.Samples/issues/25) / [PR#28](https://github.com/Skymly/DesignPatterns.Samples/pull/28);Spec [#308](https://github.com/Skymly/DesignPatterns/issues/308)。 diff --git a/docs/adr/ADR-011-step-builder-async-assemble.md b/docs/adr/ADR-011-step-builder-async-assemble.md new file mode 100644 index 0000000..8cd7e78 --- /dev/null +++ b/docs/adr/ADR-011-step-builder-async-assemble.md @@ -0,0 +1,46 @@ +# ADR-011: Step Builder async assemble exits through BuildAsync + +| 字段 | 值 | +|------|-----| +| **状态** | Accepted | +| **日期** | 2026-09-26 | +| **关联 Issue** | [#324](https://github.com/Skymly/DesignPatterns/issues/324)(Spec)、[#325](https://github.com/Skymly/DesignPatterns/issues/325)–[#327](https://github.com/Skymly/DesignPatterns/issues/327) | + +## 背景 + +[ADR-010](ADR-010-step-builder-type-state-markers.md) 把必填完备性锁在泛型 type-state 上,并把 async `Build` / assemble 列为 MVP 非目标。产品物化若需要 I/O,作者只能把 `[BuilderAssemble]` 保持同步,或绕过生成的 `{Holder}Builder`,从而失去同一套必填门闩。 + +需要在不改 type-state 证明模型、不新增运行时类型、不新开诊断 ID 的前提下,给装配出口一条 async 路径。 + +## 决策 + +Step Builder 的 **async 只发生在 assemble 出口**。ADR-010 仍是 type-state 与必填上限的决策,**不**被本 ADR 取代。本 ADR 解除 ADR-010 第 6 点里的 async `Build` / assemble 非目标,并只在 async 出口上收窄「产品类型 = assemble 返回类型」的表述。 + +1. **互斥出口**:每个 holder 仍恰好一个 `[BuilderAssemble]`。返回同步产品 `T` 时只生成 `Build()`;返回 BCL `Task` 或 `ValueTask` 时只生成 `BuildAsync`,**不**再生成 `Build()`。不允许同一 holder 同时暴露两个出口。其它返回类型(含自定义 awaitable)不是 async 出口,按同步产品类型生成 `Build()`;裸 `Task` / `ValueTask` 见第 5 点。 +2. **返回类型跟随装配**:`BuildAsync` 的返回类型与 assemble 相同(`Task` 或 `ValueTask`)。不把 `ValueTask` 包成 `Task`,也不把任务拆成同步 `T`。 +3. **产品类型**:async 时产品类型是内层 `T`,任务只是传输。同步时产品类型仍是 assemble 返回的 `T`(ADR-010)。生成器不发明映射,直接 `return` assemble 的任务或值任务,不额外制造 async 状态机。 +4. **取消**:`BuildAsync` 始终带 `CancellationToken cancellationToken = default`。assemble 可以不接收 token,或接收至多一个 `CancellationToken`;若存在,生成器按该参数在 assemble 签名中的位置原样传入(按类型识别,不要求参数名为 `cancellationToken`)。该参数**不**参与步骤名绑定。同步 `Build()` 不接受 `CancellationToken`;同步 assemble 上的 `CancellationToken` 仍按普通参数做步名绑定(绑不到则 **DP080**)。 +5. **非法契约仍用 DP086**(不新开诊断 ID):裸 `Task` / `ValueTask`、void、重复 `[BuilderAssemble]`、多于一个 `CancellationToken`(同步或 async),以及既有的不可访问实例装配。DP078–DP085 语义不变。 +6. **type-state 不变**:必填步仍是 `NotSet` → `Set`;`BuildAsync` 与 `Build()` 一样,只在全部必填类型参数为 `Set` 时作为扩展方法存在。可选步、互斥组、`After` / `Before` 不因 async 放宽。必填上限仍为 8。 +7. **仍非目标**:async `[BuilderStep]` / 可等待 fluent 链;async assemble 上的 sync-over-async `Build()`;MSDI / Autofac / `FromServices`;步骤参数校验诊断;互斥的 type-state 擦除;新 Analyzer / CodeFix;新运行时属性或类型。 + +## 后果 + +**正面**: + +- I/O 装配可以留在生成 builder 的 type-state 门闩之后,不必绕过 `{Holder}Builder` +- 同步 schema 仍只有 `Build()`,既有调用点不变 +- 不新增诊断 ID、不新增 Core 类型;DP086 文案覆盖合法与非法契约 + +**负面**: + +- 同一 holder 不能同时提供同步与异步出口;需要两种出口时必须拆 holder +- 自定义 awaitable 不会变成 `BuildAsync`(有意:出口形状保持可判定) +- assemble 忽略取消时,`BuildAsync` 仍接受 token 但不转发;调用方不能从 `BuildAsync` 签名看出 token 是否被观察 + +## 参考 + +- [ADR-010](ADR-010-step-builder-type-state-markers.md) — type-state 与必填上限;本 ADR 不取代它 +- [docs/design/StepBuilder.md](../design/StepBuilder.md) +- Spec [#324](https://github.com/Skymly/DesignPatterns/issues/324) +- 落地切片:#325(Diagnostics,DP086 文案)→ #326(SourceGenerators)→ #327(Docs) diff --git a/docs/adr/README.md b/docs/adr/README.md index 1bfbee2..6f91c37 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -20,7 +20,8 @@ | [ADR-008](ADR-008-singleton-lifecycle-diagnostics.md) | Singleton lifecycle diagnostics | Accepted | 2026-07-08 | — | | [ADR-009](ADR-009-command-router-pipeline-onion.md) | Command Router pipeline uses Chain-like next onion | Accepted | 2026-07-28 | #264 / #275–#277 | | [ADR-010](ADR-010-step-builder-type-state-markers.md) | Step Builder required completeness via generic type-state markers | Accepted | 2026-08-03 | #287 / #288–#291 | +| [ADR-011](ADR-011-step-builder-async-assemble.md) | Step Builder async assemble exits through BuildAsync | Accepted | 2026-09-26 | #324 / #325–#327 | ## 下一个可用编号 -**ADR-011** +**ADR-012** diff --git a/docs/design/README.md b/docs/design/README.md index 8352b0f..2e8c7e6 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -17,5 +17,5 @@ | Event Aggregator | [EventAggregator.md](EventAggregator.md) | — | | Command Router | [CommandRouter.md](CommandRouter.md) | — | | State Transition Table | [StateTransitionTable.md](StateTransitionTable.md) | [ADR-005](../adr/ADR-005-state-transition-table.md) | -| Step Builder | [StepBuilder.md](StepBuilder.md) | [ADR-010](../adr/ADR-010-step-builder-type-state-markers.md) | +| Step Builder | [StepBuilder.md](StepBuilder.md) | [ADR-010](../adr/ADR-010-step-builder-type-state-markers.md)、[ADR-011](../adr/ADR-011-step-builder-async-assemble.md) | | Fork–Join Work Graph | [WorkGraph.md](WorkGraph.md) | —(对照 [ADR-006](../adr/ADR-006-composite-parallel-traversal.md)) | diff --git a/docs/design/StepBuilder.md b/docs/design/StepBuilder.md index 602979d..98e42d6 100644 --- a/docs/design/StepBuilder.md +++ b/docs/design/StepBuilder.md @@ -1,12 +1,12 @@ # Design Doc: Step Builder > **版本**:v0.2.4-preview1 -> **关联 ADR**:[ADR-010](../adr/ADR-010-step-builder-type-state-markers.md) -> **关联 Issue**:[#287](https://github.com/Skymly/DesignPatterns/issues/287)(Spec)、[#291](https://github.com/Skymly/DesignPatterns/issues/291)(本 Design Doc);落地 #288–#290 +> **关联 ADR**:[ADR-010](../adr/ADR-010-step-builder-type-state-markers.md)(type-state)、[ADR-011](../adr/ADR-011-step-builder-async-assemble.md)(async assemble) +> **关联 Issue**:[#287](https://github.com/Skymly/DesignPatterns/issues/287)(MVP Spec)、[#291](https://github.com/Skymly/DesignPatterns/issues/291)(MVP Design Doc);落地 #288–#290。Phase 2 async:[#324](https://github.com/Skymly/DesignPatterns/issues/324)(Spec)、[#327](https://github.com/Skymly/DesignPatterns/issues/327)(本页同步);落地 #325–#326 ## 概述 -**Step Builder**(模式域名:**Builder**)提供声明式、分步构造产品的编译期胶水:schema 写在独立 **holder** 上,生成器发出带泛型 type-state 的 `{Holder}Builder`,使**缺必填步时 `Build()` 不可调用**。可选步、互斥组与偏序约束由诊断与生成 fluent 方法内的运行时检查覆盖。 +**Step Builder**(模式域名:**Builder**)提供声明式、分步构造产品的编译期胶水:schema 写在独立 **holder** 上,生成器发出带泛型 type-state 的 `{Holder}Builder`,使**缺必填步时 `Build()` / `BuildAsync` 不可调用**。可选步、互斥组与偏序约束由诊断与生成 fluent 方法内的运行时检查覆盖。 文档用 **Step Builder** 称呼本域,以区别于库内既有的注册/装配 `*Builder`(`FactoryRegistryBuilder`、`CommandRouterBuilder`、`TransitionTableBuilder`、`DecoratorStackBuilder`、`CompositeTreeBuilder` 等)——那些组装的是注册表 / 管道 / 转换表,**不**证明产品构造步骤完备性。 @@ -17,7 +17,7 @@ 3. 产品物化留在用户 `[BuilderAssemble]`:生成器只门闩 + 传参,不发明映射 4. 默认可任意顺序应用步骤;可选 `After` / `Before` 与 `MutexGroup` 5. 每步至多一次;必填步 ≤ 8;可选步不计入上限、不占 type 参数 -6. MVP 同步、无 DI/Autofac、双 TFM(netstandard2.0 + net8.0) +6. 出口二选一:同步 assemble → `Build()`,`Task` / `ValueTask` assemble → `BuildAsync`([ADR-011](../adr/ADR-011-step-builder-async-assemble.md));无 DI/Autofac;双 TFM(netstandard2.0 + net8.0) ## API 面 @@ -61,8 +61,11 @@ public static class BuilderStepState - Holder:非泛型 `class`(可为 `static`),可访问性为 public / internal(含嵌套链),由 `[GenerateBuilder]` 标注 - 步骤方法:签名即 schema(方法体可空);推荐 `WithX(...)` 命名,便于参数绑定剥离 `With` 前缀 -- 装配:恰好一个 `[BuilderAssemble]`,返回非 void 产品类型;参数按名绑定到步骤(`WithUrl` → `url` / `Url` / `withUrl`,大小写不敏感回退) -- 产品类型 = assemble 返回类型(**不**在 `[GenerateBuilder]` 上重复声明) +- 装配:恰好一个 `[BuilderAssemble]`;参数按名绑定到步骤(`WithUrl` → `url` / `Url` / `withUrl`,大小写不敏感回退) + - 同步:返回产品类型 `T`(非 void)→ 只生成 `Build()` + - 异步:返回 BCL `Task` 或 `ValueTask` → 只生成 `BuildAsync`;**产品类型仍是内层 `T`**,任务只是传输 + - async 时至多一个 `CancellationToken`(按类型识别,不要求参数名);不参与步名绑定,由 `BuildAsync` 按签名位置转发。零个 token 合法 +- 产品类型**不**在 `[GenerateBuilder]` 上重复声明。同步时产品类型 = assemble 返回类型;async 时产品类型 = 任务的类型参数 `T`([ADR-011](../adr/ADR-011-step-builder-async-assemble.md)) - 可直接调用 assemble,绕过生成 fluent 类型 ### 生成器产出 @@ -74,9 +77,9 @@ public static class BuilderStepState | `{Holder}Builder`(静态入口) | `Create()` → 全 `NotSet` 的初始 builder | | `{Holder}Builder` | 仅**必填**步对应类型参数 | | `{Holder}BuilderState`(internal) | 捕获步值、`*Set` 标志、`AppliedOrder` | -| `{Holder}BuilderExtensions` | 步进扩展方法 + 门闩后的 `Build()` | +| `{Holder}BuilderExtensions` | 步进扩展方法 + 门闩后的 `Build()` 或 `BuildAsync` | -`Build()` 仅在全部必填类型参数为 `BuilderStepState.Set` 时作为扩展方法存在,内部调用 holder 的 assemble,未设置的可选步以 null 兼容实参传入。 +出口与 assemble 签名互斥([ADR-011](../adr/ADR-011-step-builder-async-assemble.md)):同步 `T` 只生成 `Build()`;`Task` / `ValueTask` 只生成 `BuildAsync(CancellationToken cancellationToken = default)`,返回类型跟随 assemble。两者都只在全部必填类型参数为 `BuilderStepState.Set` 时作为扩展方法存在,内部直接 `return` holder 的 assemble(不包 async 状态机)。未设置的可选步以 null 兼容实参传入。assemble 声明了 `CancellationToken` 时,`BuildAsync` 的 token 按该参数位置转发;未声明则接受 token 但不转发。自定义 awaitable 不是 async 出口,仍走 `Build()`。 示例(示意): @@ -113,11 +116,34 @@ var request = HttpRequestSchemaBuilder.Create() .Build(); ``` +同一 holder **不能**再标第二个 `[BuilderAssemble]`。把同步装配换成 async 后,生成器不再发出 `Build()`: + +```csharp +[BuilderAssemble] +public static Task AssembleAsync( + string url, + string method, + string? header, + string? bearerToken, + string? basicAuth, + CancellationToken cancellationToken = default) => + LoadAsync(url, method, header, bearerToken, basicAuth, cancellationToken); + +var request = await HttpRequestSchemaBuilder.Create() + .WithUrl("https://example.com") + .WithMethod("GET") + .WithBearerToken("…") + .BuildAsync(); +``` + +`ValueTask` 同理,`BuildAsync` 返回 `ValueTask`。assemble 可以不声明 `CancellationToken`;`BuildAsync` 仍始终接受 token(默认 `default`),此时不转发。产品类型仍是 `HttpRequest`。 + ## 证明模型 | 约束 | 机制 | |------|------| -| 必填步齐全 | 泛型 type-state;缺步则无 `Build()`(ADR-010) | +| 必填步齐全 | 泛型 type-state;缺步则无 `Build()` / `BuildAsync`(ADR-010) | +| 装配出口 | 同步 `T` → 仅 `Build()`;`Task` / `ValueTask` → 仅 `BuildAsync`(ADR-011) | | 必填步数量 | ≤ 8;超额 **DP078**;可选步不计 | | 步至多一次 | 必填:应用后该步扩展从 `NotSet` 接收端消失;可选:再应用抛 `InvalidOperationException` | | 互斥组 | Schema:≥2 个**必填**同组 → **DP081**;应用时同组已设 → `InvalidOperationException` | @@ -134,15 +160,15 @@ var request = HttpRequestSchemaBuilder.Create() |----|----------|------------------| | **DP078** | 必填 `[BuilderStep]` > 8 | 将多余步标 `Required = false` 或拆分 holder | | **DP079** | 有 `[GenerateBuilder]` 无 `[BuilderAssemble]` | 添加返回产品的装配方法 | -| **DP080** | assemble 参数名绑不到任何步骤 | 重命名参数或补步骤 | +| **DP080** | assemble 参数名绑不到任何步骤(async `CancellationToken` 除外,见 ADR-011) | 重命名参数或补步骤 | | **DP081** | 同一 `MutexGroup` 内 ≥2 个必填步 | 至多保留一个必填,或移出组 / 改为可选 | | **DP082** | `After`/`Before` 约束成环 | 修正偏序元数据 | | **DP083** | 同名步骤重复(含 `With` 剥离后冲突) | 重命名使步名唯一 | | **DP084** | `After`/`Before` 指向不存在的步骤 | 使用 `nameof` 兄弟步或删除约束 | | **DP085** | holder 非法(非 class、泛型、不可访问等) | 改为可托管步骤的非泛型 class | -| **DP086** | assemble 契约非法(重复标注、void、不可访问实例装配等) | 修正签名或去掉重复标注 | +| **DP086** | assemble 契约非法:重复标注、void、裸 `Task`/`ValueTask`、多个 `CancellationToken`、不可访问实例装配等 | 恰好一个 assemble,返回同步 `T` 或 `Task`/`ValueTask`,至多一个 `CancellationToken` | -无本域 Analyzer / CodeFix(MVP)。 +无本域 Analyzer / CodeFix。 ## 不变量 / 兼容基线 @@ -163,10 +189,10 @@ var request = HttpRequestSchemaBuilder.Create() `GenerateBuilderGenerator`(`IIncrementalGenerator`): 1. 收集 `[GenerateBuilder]` holder → 校验可访问性 / 非泛型(失败 → DP085) -2. 收集 `[BuilderStep]` / `[BuilderAssemble]` → 缺装配 DP079;契约失败 DP086;重复步 DP083 +2. 收集 `[BuilderStep]` / `[BuilderAssemble]` → 缺装配 DP079;契约失败 DP086(含裸 `Task`/`ValueTask`、多个 `CancellationToken`、重复装配);重复步 DP083 3. 必填计数、互斥 schema、偏序图 → DP078 / DP081 / DP082 / DP084 4. assemble 参数绑定 → DP080 -5. 成功则发出入口类型、泛型 builder、state、扩展方法(含运行时互斥/偏序/至多一次检查) +5. 成功则发出入口类型、泛型 builder、state、扩展方法(含运行时互斥/偏序/至多一次检查)。同步 assemble 发 `Build()`;async assemble 发 `BuildAsync`,直接返回 assemble 调用 ### 诊断检测逻辑 @@ -190,6 +216,14 @@ var request = HttpRequestSchemaBuilder.Create() 泛型 arity 与可读性边界;超额应拆分 schema 或降级为可选(DP078)。 +### 为何 async 只在装配出口,且与 `Build()` 互斥 + +type-state 证明的是必填步已应用,不证明步骤本身可等待([ADR-010](../adr/ADR-010-step-builder-type-state-markers.md))。把 async 限制在唯一 `[BuilderAssemble]` 上,避免 fluent 链变成 `Task`。一个 holder 只有一个装配方法,签名即出口:同步 `T` 只生成 `Build()`,`Task` / `ValueTask` 只生成 `BuildAsync`。再发明一个会阻塞的 `Build()` 会让调用方误以为存在同步产品([ADR-011](../adr/ADR-011-step-builder-async-assemble.md))。 + +### 为何产品类型是内层 `T` + +调用方要构造的是产品(例如 `HttpRequest`),不是 `Task`。任务类型只决定 `BuildAsync` 的传输(`Task` 还是 `ValueTask`)。生成器直接返回 assemble 的任务,不包一层 async 状态机。 + ## 与生态的边界 ### vs 注册 / 装配 `*Builder`(本库内) @@ -213,28 +247,32 @@ var request = HttpRequestSchemaBuilder.Create() ### vs 手写 fluent builder -手写可任意设计阶段接口;本域用属性 + 生成器换取统一诊断与 type-state,代价是必填上限与 MVP 同步-only。 +手写可任意设计阶段接口;本域用属性 + 生成器换取统一诊断与 type-state,代价是必填上限,以及同步 `Build()` 与 `BuildAsync` 互斥。 ### vs Decorator / Chain 非服务契约叠层或请求管道;仅构造期。 -## 已知局限(非目标 / Phase 2+) +## 已知局限(非目标) -- **无** MVP async assemble / `Build` -- **无** MVP MSDI / Autofac / `FromServices` 步进注入 +- **无** async `[BuilderStep]` / 可等待 fluent 链 +- **无** async assemble 上的 sync-over-async `Build()` +- **无** 自定义 awaitable 的 `BuildAsync`(仅 BCL `Task` / `ValueTask`;裸 `Task` / `ValueTask` 为 DP086) +- **无** MSDI / Autofac / `FromServices` 步进注入 +- **无** 步骤参数校验诊断 - **无** Director 框架或厚 GoF Director 层次 - **无** 互斥的 type-state 擦除(仅诊断 + 运行时拒绝) - **无** Analyzer 主门闩或 interface-per-subset 默认编码 - **无** 合并/重命名既有注册 `*Builder` API -HTTP 请求示例已落地于 sibling [DesignPatterns.Samples#23](https://github.com/Skymly/DesignPatterns.Samples/issues/23) / [PR#24](https://github.com/Skymly/DesignPatterns.Samples/pull/24)。 +同步 HTTP 请求示例已落地于 sibling [DesignPatterns.Samples#23](https://github.com/Skymly/DesignPatterns.Samples/issues/23) / [PR#24](https://github.com/Skymly/DesignPatterns.Samples/pull/24)。`BuildAsync` 示例仍待 [DesignPatterns.Samples#29](https://github.com/Skymly/DesignPatterns.Samples/issues/29)。 ## 参考 -- [ADR-010](../adr/ADR-010-step-builder-type-state-markers.md) — type-state + 必填上限 8 -- Spec:[Builder / Step Builder (#287)](https://github.com/Skymly/DesignPatterns/issues/287) -- 落地:#288 Runtime → #289 Diagnostics(DP078–DP086)→ #290 SourceGenerators → #291 Docs → Samples [DesignPatterns.Samples#23](https://github.com/Skymly/DesignPatterns.Samples/issues/23) +- [ADR-010](../adr/ADR-010-step-builder-type-state-markers.md) — type-state + 必填上限 8(不因 async 取代) +- [ADR-011](../adr/ADR-011-step-builder-async-assemble.md) — async assemble 出口 / `BuildAsync` +- Spec:[Builder / Step Builder (#287)](https://github.com/Skymly/DesignPatterns/issues/287);Phase 2 async [#324](https://github.com/Skymly/DesignPatterns/issues/324) +- 落地:#288 Runtime → #289 Diagnostics(DP078–DP086)→ #290 SourceGenerators → #291 Docs → Samples [DesignPatterns.Samples#23](https://github.com/Skymly/DesignPatterns.Samples/issues/23)。Phase 2:#325 Diagnostics(DP086 文案)→ #326 SourceGenerators → #327 Docs - [docs/ROADMAP.md](../ROADMAP.md) F3 Top-2 - [AGENTS.md](../../AGENTS.md) — 模式摘要与诊断表 - [FactoryRegistry.md](FactoryRegistry.md) — 注册 `FactoryRegistryBuilder` 对照 diff --git a/docs/design/WorkGraph.md b/docs/design/WorkGraph.md index 860c983..2630ed4 100644 --- a/docs/design/WorkGraph.md +++ b/docs/design/WorkGraph.md @@ -217,7 +217,7 @@ await generated.RunAsync(new PrepContext(), CancellationToken.None); | | Work Graph | Step Builder | |---|---|---| | 证明对象 | 执行就绪 DAG | 构造步完备(type-state) | -| 时间 | `RunAsync` 运行时波次 | `Build()` 前编译期门闩 | +| 时间 | `RunAsync` 运行时波次 | `Build()` / `BuildAsync` 前编译期门闩 | ### vs Command Router