# C# 代码风格 基线是 [dotnet/runtime 编码风格](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md),再加上本项目的补充规则。所有能自动检查的规则都写在 `.editorconfig` 或 `BannedSymbols.txt` 里,违反即编译失败(`EnforceCodeStyleInBuild` + `TreatWarningsAsErrors`);无法自动检查的规则由代码评审把关。 ## 1. 格式与命名(`.editorconfig` 强制) - 使用 Allman 大括号(左大括号独占一行),四空格缩进,不使用 Tab;`if`/`for` 等语句必须加大括号。 - 使用文件作用域 namespace;namespace 与目录一致(`MoShiro.Agent.Sessions` 对应 `Sessions/`)。 - `using` 放在文件顶部、namespace 之外,`System.*` 排在最前面,其余按字母排序。 - 始终显式写出可见性,并且放在修饰符的第一位;`static readonly` 的顺序固定。 | 元素 | 规则 | 示例 | | --- | --- | --- | | 类型、方法、属性、常量、局部函数 | PascalCase | `SessionTable`、`MaxFrameBytes` | | 私有和内部实例字段 | `_camelCase` | `_sessions` | | 静态字段 | `s_camelCase` | `s_idleTimeout` | | 线程静态字段 | `t_camelCase` | `t_buffer` | | 局部变量、参数、主构造函数参数 | camelCase | `windowSeq` | | 接口 | `I` 前缀 | `IMeasurement` | | 异步方法 | `Async` 后缀 | `SendReportAsync` | - `var` 只在右侧类型显而易见时使用(`new`、强制转换);类型名使用语言关键字(`int`,而不是 `Int32`)。 - 用 `nameof(...)` 代替字符串字面量;不写多余的 `this.`。 - 字段放在类型的最前面;一个文件只放一个主要类型,文件名与类型名相同。 - 内部类和私有类默认使用 `sealed`(或 `static`),除非确实需要被继承。 ## 2. 命名语义 - 名称里带单位:`timeoutMs`、`rttUs`、`payloadBytes`。API 内部统一用 `TimeSpan`,只在边界(线协议、配置、JSON)使用带单位后缀的整数。 - 不使用 `Manager`、`Helper`、`Util`、`Service`(ASP.NET 约定位置除外)之类的模糊名称,名称要说明职责:`UploadQueue`、`SessionTable`、`ResultWriter`。 - 布尔值使用 `is`/`has`/`can` 前缀;返回“成功与否加输出值”的方法使用 `TryXxx(out ...)`。 ## 3. 类型与数据 - 契约和领域值使用 `sealed record` 或 `readonly record struct`,并且不可变。 - 热路径上的计数器和缓冲区使用可变 `struct` 或 `sealed class`,所有权要写清楚(谁创建、谁归还池)。 - ID 使用强类型(`readonly record struct NodeId(string Value)`),不直接传裸 `string`。 - 枚举序列化成 snake_case 字符串(`JsonStringEnumConverter` + `JsonStringEnumMemberName`);新增枚举值不属于破坏性变更,但所有消费方都必须能处理未知值。 - 可空性:公共成员必须标注;使用 `!` 抑制警告时,同一行要写注释说明理由。 ## 4. 错误处理 - 预期内的失败不抛异常:超时、拒绝连接、认证失败、校验失败都返回结构化结果,其中包含 `ErrorCode`。 - 异常只用于程序缺陷或无法继续运行的状态;只在边界(会话循环、请求管线、进程入口)捕获,并记录日志。 - 禁止空 `catch`;捕获 `OperationCanceledException` 时必须确认它确实来自自己的取消令牌。 ## 5. 异步与并发 - 所有 I/O 都是异步的,并且接受、传递 `CancellationToken`(作为最后一个参数)。 - 禁止使用 `.Result`、`.Wait()`、`Thread.Sleep`、`async void`(事件处理函数除外),`BannedSymbols.txt` 强制执行。 - 库代码中的 `await` 不需要 `ConfigureAwait(false)`,因为三个程序都没有同步上下文,统一省略即可。 - 生产者和消费者之间使用有界 `Channel`,明确指定 `BoundedChannelFullMode`。 - 共享可变状态要么只由单个循环拥有(推荐),要么使用 `lock` 保护;不要混用多种方式。 ## 6. 时间 - 时间来源一律通过 `TimeProvider` 获得:墙钟使用 `GetUtcNow()`,耗时使用 `GetTimestamp()`/`GetElapsedTime()`。 - `DateTime.Now`、`DateTime.UtcNow`、`DateTimeOffset.Now`、`Stopwatch.StartNew` 列入禁用名单。测试使用 `FakeTimeProvider` 驱动窗口和超时(这个包只出现在测试项目中)。 ## 7. NativeAOT 与轻量化 - 序列化只使用 `System.Text.Json` 源生成上下文(或手写的二进制编解码);禁用无上下文的 `JsonSerializer.Serialize(T)` 重载。 - 不做程序集扫描、`Activator.CreateInstance`、`Type.GetType(string)`、动态代理或 `Reflection.Emit`。 - Agent:不使用 DI 容器,在 `Program.cs` 中手工组装对象,依赖关系一眼可见;不使用 LINQ;不使用 `HttpClient`(Agent 的 WebSocket 使用 `ClientWebSocket`)。 - Backend:使用 `CreateSlimBuilder` + Minimal API;只能通过 `Microsoft.Data.Sqlite` 执行手写的参数化 SQL,禁止字符串拼接 SQL。 - 热路径:使用 `Span`、`stackalloc`(≤ 256 B)、`ArrayPool.Shared`;每个包的处理过程中不分配内存、不写日志。 - 新增任何第三方包之前,需要先更新 [project-layout.md](project-layout.md) 第 3 节的依赖表,并在 PR 中说明理由。 ## 8. 日志 - 统一使用 logfmt 字段名:`ts`、`level`、`msg`、`agent`、`dialer`、`session`、`task`、`result`、`err`。 - Backend 使用 `Microsoft.Extensions.Logging` 源生成的 `[LoggerMessage]`;Agent 使用 `NodeLog`,两者输出的字段名相同。 - 令牌、`session_key` 和 MAC 不能出现在日志里;IP 地址可以记录。 - 每个会话或连接的生命周期事件记录为 Info;逐包事件不记录,只累加计数。 ## 9. 节点能力边界(Agent 与 Fleet 节点侧) - 由 `BannedSymbols.Node.txt` 在编译期强制:禁止启动进程、动态加载程序集或本机库、使用 `HttpClient`、在配置目录以外写文件。唯一的豁免点是 `Fleet.Hosting.ConfigStore`,豁免必须写 `#pragma` 并附上理由注释。注册在 `fleet-enroll` 中完成,更新由节点宿主机上的更新器完成,常驻进程当前不需要这些能力。 - 节点不能新增任何“由远端触发的非采集动作”对应的 RPC 方法。凡是新增 RPC 方法的 PR,都要在描述中说明它不会扩大权限边界。 - Backend 下发的任何参数都要先经过 `ParamSpec` 校验,再经过本地硬上限(`BudgetGuard`)检查,两道校验都不能省略。 - 日志中不能出现私钥、签名、`session_key`、令牌或 MAC。 ## 9.1 Fleet 的通用性 - `backend/fleet/**` 不能引用任何 `MoShiro.*`,也不能出现产品相关的概念、命名或默认值(例如端口 7443、`moshiro` 字样),由 `ArchitectureTests` 和代码评审把关。 - 新增的公开 API 如果只有 MoShiro 使用,应先放在 MoShiro 中,等出现第二个使用者后再下沉到 Fleet。 - Fleet 不提供 exec、shell、文件读写或远程下发代码的方法;确需此类能力时走 extension 方案,见 [security.md](../security/security.md) 第 9 节。 - Fleet 公开 API 的变更,PR 描述需说明是否属于破坏性变更(不使用 PublicApiAnalyzers)。 ## 10. 声明式扩展的写法 - Kind:只放在 `Protocol/Catalog/Kinds`,一个 Kind 一个静态字段;参数一律使用 `P.Int`、`P.Enum` 等 `ParamSpec` 写出默认值和上下限,禁止写没有上下限的参数。 - 积木:实现拨号方 / 监听方接口,在 `BlockCatalog` 中登记 Kind 到积木的映射;积木内部不能读取全局配置,所有输入都来自参数。 - 指标:只放在 `Analysis/Metrics`,一行一个 `MetricDef`,并加入 `MetricRegistry.All`;指标 id 使用 `领域.名称`(如 `tcp.stall`),导出值为 `id.统计量`(如 `rtt.p95`)。已发布的 id 不能改名,只能新增。 - 算子:纯函数,输入列视图,输出累加器更新;每个算子都要有边界用例测试(空输入、单个样本、乱序、时钟回绕)。 - 模型:只读取指标,不读取原始列;评分函数需要有单调性测试(指标变差时,分数不能升高)。 ## 11. 测试 - 使用 xUnit v3。测试项目与被测项目一一对应,放在同名目录下的 `tests/` 中。 - 测试名采用 `方法_场景_期望` 格式,例如 `Merge_TwoHistograms_PreservesP95`。 - 协议编解码必须有“黄金字节”测试:固定的输入对应固定的字节序列。布局发生变化时测试会失败,提醒升级协议版本。 - 采集端必须覆盖:逐包记录的完整性(发送数 = 记录数)、丢失标记、重复、乱序、非法长度、重放、会话过期、版本不兼容。 - 分析模型必须有“黄金原始块 → 期望输出”的测试;模型升版时,同时更新期望输出并说明原因。 - 端到端测试应针对 AOT 发布产物运行,而不是 JIT 构建。 ## 12. 提交与评审 - 提交信息使用 `<范围>: <摘要>` 格式,范围取 `dialer`、`agent`、`backend`、`shared`、`web`、`docs`、`scripts`;是否触发 CI 的标记见 [AGENTS.md](../../AGENTS.md)。 - 修改 `backend/shared/MoShiro.Protocol` 的 PR 必须说明兼容性影响(新增字段、行为变化、需要升级版本)。 - 修改 `backend/openapi/v1.json` 的 PR 必须按照 [api.md](../api/api.md) 第 7 节的「版本与兼容性」判断是否属于破坏性变更。