Align architecture, protocol, API, storage, security and deployment documentation with default mutual measurements and the Agent link board.
9.3 KiB
9.3 KiB
C# 代码风格
基线是 dotnet/runtime 编码风格,再加上本项目的补充规则。所有能自动检查的规则都写在 .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<T>+JsonStringEnumMemberName);新增枚举值不属于破坏性变更,但所有消费方都必须能处理未知值。 - 可空性:公共成员必须标注;使用
!抑制警告时,同一行要写注释说明理由。
4. 错误处理
- 预期内的失败不抛异常:超时、拒绝连接、认证失败、校验失败都返回结构化结果,其中包含
ErrorCode。 - 异常只用于程序缺陷或无法继续运行的状态;只在边界(会话循环、请求管线、进程入口)捕获,并记录日志。
- 禁止空
catch;捕获OperationCanceledException时必须确认它确实来自自己的取消令牌。
5. 异步与并发
- 所有 I/O 都是异步的,并且接受、传递
CancellationToken(作为最后一个参数)。 - 禁止使用
.Result、.Wait()、Thread.Sleep、async void(事件处理函数除外),BannedSymbols.txt强制执行。 - 库代码中的
await不需要ConfigureAwait(false),因为三个程序都没有同步上下文,统一省略即可。 - 生产者和消费者之间使用有界
Channel<T>,明确指定BoundedChannelFullMode。 - 共享可变状态要么只由单个循环拥有(推荐),要么使用
lock保护;不要混用多种方式。
6. 时间
- 时间来源一律通过
TimeProvider获得:墙钟使用GetUtcNow(),耗时使用GetTimestamp()/GetElapsedTime()。 DateTime.Now、DateTime.UtcNow、DateTimeOffset.Now、Stopwatch.StartNew列入禁用名单。测试使用FakeTimeProvider驱动窗口和超时(这个包只出现在测试项目中)。
7. NativeAOT 与轻量化
- 序列化只使用
System.Text.Json源生成上下文(或手写的二进制编解码);禁用无上下文的JsonSerializer.Serialize<T>(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<T>、stackalloc(≤ 256 B)、ArrayPool<byte>.Shared;每个包的处理过程中不分配内存、不写日志。 - 新增任何第三方包之前,需要先更新 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 第 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 构建。