Skip to content

[Runtime Configuration] Dynamic Interceptors:运行时启用、禁用与原子替换 Client/Server 拦截器 #263

Closed

Description

@SunSi12138

总跟踪:#262
架构背景:#67
分析与实施基线:dev
模块定位:本 issue 是 Runtime Dynamic Configuration 的第一类实现模板,代表 pure immutable snapshot / next-logical-call semantics。

Use case

SharpLink 当前 Client / Server interceptor 在 Builder 阶段配置,Build 后冻结为数组并贯穿实例整个生命周期。这个模型简单、性能好,但不能满足运行时策略切换,例如:

  • 临时开启审计 / tracing interceptor;
  • 动态开启或关闭鉴权、租户检查、请求标记等 middleware;
  • 灰度替换 interceptor pipeline;
  • Hosting / 配置中心未来驱动 interceptor 组合变化;
  • 运行中关闭一个高成本 interceptor,而不重建 Client / Server。

本 issue 的目标不是把 interceptor list 改成线程安全 List<T>,而是在保持现有 fast path 的前提下,把 interceptor pipeline 从“Build 时永久冻结”升级为“运行时可原子发布的 immutable generation”。

当前实现与问题定位

Builder / BuildPlan

当前 Builder 侧将 interceptor 收集到 list,并在 Compile / BuildPlan 阶段复制为数组:

  • src/SharpLink.Client/SharpClientBuilder.cs
  • src/SharpLink.Client/ClientBuildPlan.cs
  • src/SharpLink.Server/SharpLinkServerBuilder.cs
  • src/SharpLink.Server/ServerBuildPlan.cs

这一部分作为初始配置来源应继续保留,不要删除 Build/Compile freeze。

Client 热路径

重点文件:

  • src/SharpLink.Client/SharpLinkClient.cs
  • src/SharpLink.Client/SharpLinkClient.Invokers.cs
  • src/SharpLink.Client/SharpLinkClient.Telemetry.cs
  • src/SharpLink.Client/SharpLinkClient.Interceptors.cs

当前 InvokeUnaryAsync / OneWay / ClientStreaming / ServerStreaming / DuplexStreaming 都有显式 fast path:

if (SharpLinkTelemetry.ClientCallsEnabled)
{
    ...
}
else if (_clientInterceptors.Length != 0)
{
    ... intercepted ...
}
else
{
    ... direct ...
}

Telemetry path 内部又会再次检查 _clientInterceptors.Length。

更重要的是,ClientInterceptorState.InvokeNextAsync(...) 当前在每一步链调用时继续读取:

_client._clientInterceptors.Length
_client._clientInterceptors[index]

如果只把 _clientInterceptors 从 readonly 数组改成可替换字段,会出现错误 generation 混用:

RPC A capture/进入 interceptor A(old)
    ↓
运行时替换 pipeline
    ↓
A 调用 next
    ↓
读取到 interceptor B(new)

同一个 logical RPC 可能执行 old[0] -> new[1]。这是本 issue 必须消除的首要 correctness bug。

Server 热路径

重点文件:

  • src/SharpLink.Server/SharpLinkServer.cs
  • src/SharpLink.Server/SharpLinkServer.Interceptors.cs

当前 InvokeServiceCoreAsync(...):

if (_serverInterceptors.Length == 0)
{
    return generated-stub direct invocation;
}

return InvokeInterceptedWithOwnedArgumentsAsync(...);

Server 侧的 ServerPipelineFacts 已经把 interceptor array 保存为字段,因此其结构天然接近正确的 generation capture 模式;需要做的是只在调用入口读取一次当前 snapshot,并将该 snapshot 一路传入,不能在 helper 中重新读全局字段。

已有测试资产

优先扩展而不是另建重复 harness:

  • test/SharpLink.IntegrationTests/InterceptorIntegrationTests.cs

现有测试已经覆盖:

  • Client / Server interceptor context;
  • Client short-circuit;
  • Server structured rejection;
  • cancellation / mapped failure;
  • response / stream failure;
  • continuation 必须 join;
  • unary / one-way / streaming 多种 shape;
  • synchronization-context 等异步边界。

动态化测试应继续复用这个集成测试体系。


目标语义

1. 生效点:next logical RPC

Interceptor 更新成功后:

  • 更新之后新开始的 logical RPC 使用新 pipeline;
  • 更新之前已经开始的 RPC 从头到尾使用旧 pipeline;
  • 一个 RPC 绝不能混用两个 interceptor generation。

这里的“logical RPC”包括:

  • Unary;
  • OneWay;
  • Client Streaming;
  • Server Streaming;
  • Duplex Streaming。

Client 侧 generation 应在公开 Invoke* 入口处 capture,而不是延迟到第一个 interceptor next(),也不要在 stream MoveNextAsync() 时重新读取。

Server 侧 generation 应在开始 service invocation/interceptor dispatch 时 capture,并一直传递到该 interceptor chain 完成。

2. 顺序保持

若当前发布:

[A, B, C]

一次调用必须完整看到:

A -> B -> C -> terminal -> C -> B -> A

运行时更新不得改变已经开始调用的顺序。

3. Disabled state

零 interceptor 必须继续走 direct fast path:

snapshot = empty
    ↓
no pipeline allocation
no continuation state
no no-op interceptor call
    ↓
direct invocation

不要引入 NoopInterceptor 或永远存在的 no-op pipeline。

4. 更新是 control-plane 操作

更新允许:

  • 分配新数组 / snapshot;
  • 校验 null / ordering / argument;
  • 在专用 update gate 下串行化多个 writer;

请求路径不允许:

  • 全局 lock;
  • ReaderWriterLockSlim;
  • 每调用复制 interceptor list;
  • 每调用构造新的 snapshot object;
  • 在 interceptor 每一层重新读取动态字段。

推荐内部模型

最小实现优先:immutable array snapshot

Interceptor 本身没有像 Admission 那样必须迁移的 limiter/token state,因此第一版不需要引入复杂 generation registry。

推荐直接使用“数组引用就是 snapshot”:

private ISharpLinkClientInterceptor[] _clientInterceptors;
private ISharpLinkServerInterceptor[] _serverInterceptors;

数组发布后必须被视为 immutable;所有输入序列必须复制,绝不能把 caller-owned mutable array/List 直接保存。

读取:

var interceptors = Volatile.Read(ref _clientInterceptors);

发布:

Volatile.Write(ref _clientInterceptors, candidate);

或使用等价的 Interlocked.Exchange。

如果实现者认为需要附带 generation id / diagnostics,可使用内部 sealed snapshot:

internal sealed class ClientInterceptorPipeline
{
    internal readonly ISharpLinkClientInterceptor[] Interceptors;
    internal readonly long Generation;
}

但不得因为 snapshot class 在每个 RPC 新建对象。Snapshot 只能在 update path 创建。

生命周期 / ownership

本 issue 不改变 interceptor instance 的 ownership contract。

如果当前 Builder 接收 caller-created interceptor instance,则运行时替换后:

  • 框架不能擅自 Dispose / DisposeAsync 被移除 interceptor;
  • in-flight call 通过 captured array/reference 自然保持旧 interceptor 可达;
  • snapshot 无额外 native/resource ownership 时,不需要 drain manager。

若未来要引入 framework-owned disposable interceptor factory,应另开 lifecycle issue,不在本实现顺带扩张。


具体实施步骤

下面按实现顺序执行。不要跳过 generation consistency 测试后再“补”。

Phase 1 — 保留 Builder 初始语义,允许 runtime field 发布

修改候选文件:

  • src/SharpLink.Client/SharpLinkClient.cs
  • src/SharpLink.Server/SharpLinkServer.cs

要求:

  1. BuildPlan 仍然生成独立 interceptor array。
  2. Client / Server 构造时用 BuildPlan 的 array 初始化 runtime snapshot。
  3. runtime 字段允许 atomic replacement。
  4. 不把 Builder 的 List<T> 直接共享给 runtime。
  5. 空 pipeline 使用统一 empty array,禁止 null element。

Phase 2 — Client 在 logical invocation 入口 capture 一次

修改:

  • src/SharpLink.Client/SharpLinkClient.Invokers.cs

每个公开 invocation 入口都先 capture:

var interceptors = Volatile.Read(ref _clientInterceptors);

然后整个调用只使用这个局部 snapshot。

推荐结构:

var interceptors = Volatile.Read(ref _clientInterceptors);

if (SharpLinkTelemetry.ClientCallsEnabled)
{
    invocation = InvokeUnaryWithTelemetryAsync(..., interceptors, ...);
}
else if (interceptors.Length != 0)
{
    invocation = InvokeUnaryInterceptedAsync(..., interceptors, ...);
}
else
{
    invocation = InvokeUnaryDirect(...);
}

必须覆盖全部 RPC shape,不只 Unary。

Phase 3 — Telemetry path 不得二次读取全局 interceptor field

修改:

  • src/SharpLink.Client/SharpLinkClient.Telemetry.cs

当前 telemetry helper 内又检查 _clientInterceptors.Length。动态化后,这会造成:

Invoke* capture generation N
↓
telemetry enabled
↓
配置更新 N+1
↓
telemetry helper 再读全局字段
↓
调用实际使用 N+1

因此 telemetry helper 必须接收 outer invocation 已 capture 的 snapshot:

InvokeUnaryWithTelemetryAsync(..., ISharpLinkClientInterceptor[] interceptors, ...)

并基于该参数决定 intercepted/direct。

Phase 4 — ClientInterceptorState 保存 snapshot,不得读取 client 全局数组

修改:

  • src/SharpLink.Client/SharpLinkClient.Interceptors.cs

ClientInterceptorState 增加 captured pipeline field:

private readonly ISharpLinkClientInterceptor[] _interceptors;

构造时传入当前 snapshot。

把:

_client._clientInterceptors.Length
_client._clientInterceptors[index]

全部替换为:

_interceptors.Length
_interceptors[index]

ClientContinuationState 继续通过 owner state 执行,因此只要 owner state 持有 snapshot,就能保证 continuation 无论异步多久都不会跨 generation。

Phase 5 — Server capture 一次并一路传入

修改:

  • src/SharpLink.Server/SharpLinkServer.Interceptors.cs

在 InvokeServiceCoreAsync 进入 interceptor 选择处:

var interceptors = Volatile.Read(ref _serverInterceptors);
if (interceptors.Length == 0)
{
    return direct stub invocation;
}

然后:

  • InvokeInterceptedWithOwnedArgumentsAsync(...) 接收 interceptors 参数;
  • ServerPipelineFacts 使用传入数组;
  • helper 内不再次读取 _serverInterceptors。

Server 当前 ServerPipelineFacts 已持有数组,不要改成每层去读 server field。

Phase 6 — 增加 runtime update control path

具体 public API 命名可在实现 PR 中根据现有 surface 决定,但底层必须支持“完整替换”。建议优先 full replacement,而不是先暴露大量并发 Add/Remove:

概念接口:

client.ReplaceInterceptors(...);
server.ReplaceInterceptors(...);

或:

client.UpdateInterceptors(builder => ...);
server.UpdateInterceptors(builder => ...);

无论采用哪种 API,都必须满足:

  1. candidate 在 publish 前完整构建;
  2. null interceptor 在 publish 前失败;
  3. 输入 sequence 复制;
  4. 多 writer 更新若基于“读取旧状态后增删”,必须由 control-path gate 串行化,否则会 lost update;
  5. invalid candidate 不能改变当前已发布 snapshot;
  6. Dispose/Stop 后 update 的异常行为与项目现有 runtime mutation API 保持一致;不要静默成功。

如果最终只暴露“完整 replacement”,并发 writer 可以明确 last-writer-wins;如果暴露 mutation builder,则必须 serialize read-modify-publish。

Phase 7 — 文档与 diagnostics

至少同步:

  • interceptor XML docs;
  • runtime configuration 文档(若 [Runtime Configuration] 动态配置 / Hot Reload 长期跟踪 #262 后续建立统一文档,可先在本 issue/PR 记录);
  • 明确 Build AddInterceptor 是 initial configuration;
  • 明确 runtime replacement 的 next-logical-call semantics;
  • 明确 interceptor instances 的 ownership 不因动态替换改变。

禁止实现

以下实现即使测试“看起来能跑”也不接受:

禁止 1:直接把 array field 改成可写,但链内继续读取

_client._clientInterceptors[index]

这是 generation mixing。

禁止 2:CopyOnWrite 只做一半

_clientInterceptors = newArray;

如果 invocation / telemetry / continuation 后面又重新读 field,仍然错误。

禁止 3:Concurrent collection 进入热路径

不要使用:

ConcurrentBag<IInterceptor>
ConcurrentDictionary<int, IInterceptor>
ImmutableList<T>.Builder // per-call
lock (_interceptorGate) // per-call

发布单位应该是一个完整 immutable pipeline reference。

禁止 4:关闭状态使用 Noop pipeline

零 interceptor 应保持 direct branch,不要让所有调用都穿过统一 pipeline abstraction。

禁止 5:动态替换时 dispose 旧 interceptor

除非另有明确 framework ownership contract,否则旧 instance 可能仍被 in-flight call 使用,也可能本来就是 caller-owned。


测试实施手册

优先扩展:

  • test/SharpLink.IntegrationTests/InterceptorIntegrationTests.cs

如测试文件过大,可拆:

  • DynamicInterceptorIntegrationTests.cs

但应复用当前 InterceptorHarness / generated test service。

A. 基础 enable / disable / replace

Client 与 Server 分别覆盖:

  • Build 时无 interceptor,runtime enable 后下一次调用执行 interceptor。
  • Build 时有 interceptor,runtime disable 后下一次调用走 direct path。
  • A 替换为 B 后,新调用只执行 B。
  • [A, B] 替换为 [C, D] 后,顺序完全按新 snapshot。
  • 替换为空数组等价 disable。
  • invalid/null candidate 被拒绝,旧 snapshot 保持可用。

B. 最关键 generation consistency 测试

必须有“人为阻塞 interceptor”的 deterministic test,不要只靠 stress 猜竞态。

Client 示例:

published = [A, B]
call #1 enters A
A blocks before calling next
runtime replace => [X, Y]
release A
call #1 MUST continue B
call #2 MUST execute X -> Y

必须断言 call #1 没有执行 Y。

Server 同样做一套。

这是本 issue 的核心回归测试。

C. Async continuation generation consistency

现有 interceptor 支持 async before-next / after-next / continuation join。

覆盖:

  • interceptor 在 await 前后发生 update;
  • next() 已发起但未完成时发生 update;
  • continuation 被延迟,恢复后仍使用旧 snapshot;
  • exception / cancellation unwind 仍走原 generation。

D. RPC shapes

至少分别覆盖:

  • Unary;
  • OneWay;
  • Client Streaming;
  • Server Streaming;
  • Duplex Streaming。

Streaming 测试要求:

  1. 创建/开始一个使用 old pipeline 的 stream;
  2. stream 尚未完成时更新 pipeline;
  3. old stream 继续保持 old generation;
  4. 新建 stream 使用 new generation;
  5. update 不导致 stream orphan / duplicate completion。

E. Telemetry interaction

因为 Client telemetry path 当前会再次读 _clientInterceptors,必须有专门测试:

  • telemetry enabled + interceptor update;
  • call 在 outer Invoke* 开始后发生 update,telemetry helper 不得切 generation;
  • telemetry disabled 仍保持 direct fast path。

如果测试环境难以在精确 outer capture 点阻塞,可通过内部 test hook 或专用 interceptor barrier 构造,不要用不稳定 Task.Delay 竞争。

F. 并发 stress

至少:

N worker threads continuously invoke RPC
1 updater repeatedly cycles:
[] -> [A] -> [A,B] -> [C] -> []

检查:

  • 无 IndexOutOfRangeException;
  • 无 NullReferenceException;
  • 每个 call 只记录一个合法完整 generation;
  • Stop/Dispose 与 update 并发不死锁;
  • logical invocation accounting 不 underflow / leak。

G. Ownership / GC

若 interceptor 不由框架拥有:

  • replacement 不调用 Dispose;
  • in-flight old call 完成前 old interceptor 仍可使用;
  • call 完成且没有其他引用后,old snapshot 不被 server/client 永久持有。

如涉及 collectible ALC interceptor,增加弱引用测试,确保 current runtime 不因历史 snapshot 永久 root 已不使用 assembly;如果此验证需要额外生命周期设计,应记录并拆 issue,不要用全局 cache 修补。


Benchmark / 性能门禁

本 issue 修改 Client / Server invocation hot path,因此 benchmark 是 merge gate,不是可选项。

建议新增:

  • test/SharpLink.Benchmarks/InterceptorBenchmarks.cs

或扩展现有 Runtime hot-path benchmarks。

必测场景

End-to-end RPC:

  1. NoInterceptors — baseline;
  2. ClientInterceptorDisabledDynamicPath(即支持动态化后的空 snapshot);
  3. OneClientNoopInterceptor;
  4. OneServerNoopInterceptor;
  5. ClientAndServerNoopInterceptor;
  6. steady state after many replacements(确认更新不会让热路径退化/累积结构)。

至少报告:

  • Mean / throughput;
  • allocation B/op;
  • threading/lock diagnostics;
  • 如项目 benchmark 支持,关注 p95/p99 或配合 load test。

性能要求

Disabled path:

  • 0 新增 per-call allocation;
  • 无 request-path lock;
  • 不构造 pipeline object;
  • 仅允许必要的 snapshot pointer load + branch;
  • 相对改造前 baseline 的差异应处于 benchmark 噪声范围;若稳定退化 > 约 3%–5%,必须解释并继续优化。

Enabled path:

  • 动态支持不应在每层 interceptor 增加 global volatile read;
  • continuation pool 现有优化不得因为动态化失效;
  • snapshot capture 只发生一次。

Update path 不需要按 RPC-rate 优化,但不得阻塞正在执行的 RPC。


验证命令

至少执行并在 PR 记录结果:

dotnet build Sharplink.slnx -c Release

dotnet test --project test/SharpLink.UnitTests/SharpLink.UnitTests.csproj -c Release

dotnet test --project test/SharpLink.Generator.Tests/SharpLink.Generator.Tests.csproj -c Release

dotnet run --project test/SharpLink.IntegrationTests/SharpLink.IntegrationTests.csproj -c Release -- --timeout 120s

Benchmark 使用仓库现有 BenchmarkDotNet 项目,至少运行新增 interceptor benchmark 与相关 Runtime hot-path benchmark,并把 before/after 结果贴到 PR。

如果改动触及 dynamic module / ALC 生命周期,补:

  • RuntimeAssemblyIntegrationTests / 对应 ALC tests;
  • NativeAOT smoke(若 public API/generic rooting 有变化)。

风险清单

R1 — 同一 RPC 混用 generation

最高风险。Client 当前实现天然会发生,必须通过 state 保存 snapshot 解决。

R2 — Telemetry 二次读取

Outer invocation capture 以后,telemetry helper 若再读全局 field,会破坏语义。

R3 — Streaming capture 时机错误

不要在每个 MoveNextAsync() 读 snapshot。Generation 属于 logical RPC,不属于 stream item。

R4 — 调用方传入 mutable collection

必须复制。否则 publish 后 caller 还能修改 array/list,原子 publication 失去意义。

R5 — 更新与 Dispose 竞态

Control API 必须遵守 server/client lifecycle;不得出现 dispose 后把新 snapshot 重新发布到已停止实例。

R6 — 错误 ownership

不要把“从 pipeline 移除”解释为“framework 可以 dispose interceptor”。

R7 — 为统一 API 引入 hot-path abstraction

本 issue 优先保证 runtime model 与性能;不要为了未来统一 RuntimeConfiguration API 给每次 RPC 套多层 interface/virtual dispatch。


Out of scope

  • 不在本 issue 动态化 timeout / retry / exception mapper;这些属于 Dynamic Call Policy。
  • 不改变 interceptor 的业务语义、context 字段、short-circuit contract。
  • 不引入 interceptor dependency injection lifetime / factory ownership 新模型。
  • 不把 Telemetry 本身改成动态配置系统。
  • 不统一所有 Runtime Dynamic Configuration 的最终 public control surface;仅提供本模块可工作的更新入口。

建议提交拆分

一个聚焦 PR 可以包含多个 commit:

  1. refactor: capture interceptor snapshots per logical call
  2. feat: add runtime interceptor replacement control path
  3. test: add dynamic interceptor generation tests
  4. perf: add interceptor dynamic-path benchmarks
  5. docs: document interceptor hot reload semantics

不要把 Admission 或其他模块混入同一 PR。


Definition of Done

  • Builder / BuildPlan 初始 interceptor freeze 仍然有效。
  • Client / Server runtime interceptor pipeline 可 enable / disable / replace。
  • 更新采用 immutable candidate + atomic publication。
  • caller-owned input collection 在 publish 前被复制。
  • Client 每个 logical RPC 只读取一次 interceptor snapshot。
  • Client telemetry path 使用同一 captured snapshot。
  • ClientInterceptorState / continuation 不再读取全局 pipeline field。
  • Server 每个 service invocation 只 capture 一次 interceptor snapshot。
  • ServerPipelineFacts 始终持有该次调用的固定 snapshot。
  • in-flight call 在更新期间不会混用 generation。
  • Unary / OneWay / ClientStreaming / ServerStreaming / DuplexStreaming 都有动态更新测试。
  • enable / disable / replace / invalid update / concurrent update 都有测试。
  • update 与 Stop/Dispose 并发无死锁、无重新发布到 disposed instance。
  • disabled path 无新增 per-call allocation、无 lock。
  • BenchmarkDotNet before/after 已提交,性能变化符合 [Runtime Configuration] 动态配置 / Hot Reload 长期跟踪 #262 门禁。
  • interceptor ownership contract 未被隐式改变。
  • 文档明确 next-logical-call semantics。

Checks

Activity

  1. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    source_sha=aa339f2c9f1f4316e414cc0a8063c4c6bc671e91 run_id=32390082497 artifact_id=9414448183
  2. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    no candidate validation run found
  3. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    run_id=32393104885 status=in_progress conclusion=pending
  4. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    status=failure run_id=32393104885 head=77d10314f374a825da321fdb7354b4e4039767bc
  5. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    run_id=32393573041 status=in_progress conclusion=pending
  6. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    status=failure run_id=32393573041 head=9cd174c6cfdd0d6200081426ae9e84749fcd1939
  7. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    run_id=32393953657 status=in_progress conclusion=pending
  8. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    status=failure run_id=32393953657 head=444667e0a98046ce6ab92b2fcc80b31ed117d937
  9. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    run_id=32395007957 status=in_progress conclusion=pending
  10. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    status=success run_id=32395007957 head=6b2625f9e4ee7bf7200d488ce33229a286bc18b9
  11. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    run_id=32395690568 status=in_progress conclusion=pending
  12. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    run_id=32396027217 name=Issue 263 Candidate BenchmarkDotNet status=in_progress conclusion=pending
  13. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor
    run_id=32396762874 name=Issue 263 Paired Disabled BenchmarkDotNet status=in_progress conclusion=pending
  14. added a commit that references this issue on Aug 21, 2026
    866dd75
  15. self-assigned this
    on Aug 21, 2026
  16. SunSi12138 commented on Aug 21, 2026

    @SunSi12138
    OwnerAuthor

    Implemented and merged via #266 into dev as squash commit 866dd7557b14a4593cde0f4f3df263720ea9f943.

    Completion notes:

    • Client/Server interceptor pipelines are runtime-replaceable immutable snapshots with next-logical-RPC semantics.
    • All five RPC shapes retain one captured generation; telemetry, delayed continuations, stream enumeration, fault/cancellation unwind, and concurrent replacements were covered during review.
    • Lifecycle races against Faulted/Stop were fixed and regression-tested; removed interceptors remain caller-owned.
    • Zero-interceptor direct fast path is preserved; benchmark evidence remains within the documented gate.
    • The PR was originally based on an older dev. Before merge, its branch was fast-forwarded to GitHub's conflict-free integration merge with current dev (c488cf5), then PR Quick fix(generator): allocate arrays after generic element ranks (#873) #881 and CodeQL [P3][3.0][Generator] 服务 required 成员未纳入构造校验,生成 activator 才报 CS9035 #878 both passed on that integrated head. Final merge was performed with squash.

    Closing as completed.

  17. removed their assignment
    on Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions