Communitygithub.com

tunahanaliozturk/secure-dotnet-skills

Aegis — 12 种面向 .NET on Azure 生产级应用的判断式 Agent 技能(涵盖安全、设计、性能、并发、可观测性)。支持 Claude Code、Codex、Cursor、Gemini。

secure-dotnet-skills 是什么?

该技能集名为Aegis,提供了12种独立的判断式Agent技能,专门用于构建安全、生产级的.NET应用(特别是运行在Azure上的)。其核心能力涵盖了软件工程的多个关键维度:安全设计、架构评审、性能优化、并发处理和可观测性。每个技能都遵循严格的流程,包括“何时使用”的场景指示、逐步执行过程、以及针对.NET/Azure生态的详细检查清单(如ASP.NET Core、EF Core、Azure服务等)。技能强调“判断”而非教条——它会区分真正的反模式和偶然的不纯粹,并提供具体的C#重构建议。它适用于主流的AI编码Agent平台,如Claude Code、Codex、Cursor和Gemini,开发者可以直接将其作为技能导入,引导Agent进行深度、可操作的代码评审。

兼容平台✓Claude Code✓Codex CLI✓Cursor✓Gemini CLI
npx skills add tunahanaliozturk/secure-dotnet-skills

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

SOLID 设计评审

引导 Agent 使用具体的 .NET 视角逐一审查 C# 类或模块中的每个 SOLID 原则,明确指出代码异味、违反的原则以及最小重构方案——优先处理那些实际导致耦合或代码变更的问题,而非教科书式的吹毛求疵。

何时使用

  • 一个类变得臃肿不堪——在同一个文件中涵盖了 IO、业务逻辑、映射以及副作用。
  • 一个 PR 引入了新的类型变体,并且需要修改现有的 switch 或 if-else 链来适配它。
  • 构造函数通过 new 来创建自己的依赖,或者直接引用了 DateTime.Now / static 辅助方法。
  • 一个接口积累了多个成员以满足多个不相关的调用方,导致不得不进行空实现。

流程

  1. 确定待审查的类/模块。 说出其声明的职责。如果其名称需要用“与”或“或”来描述,这本身就是一个信号。
  2. 使用下面的 C# 透镜,按顺序逐一审视每个 SOLID 原则。 将每个原则视为一次独立的审视;不要在找到第一个问题后就停止。
  3. 明确指出代码异味和违反的原则。 “创建自己 HttpClient 和 SqlConnection 的胖构造函数” → DIP;“抛出 NotSupportedException 的基类重写” → LSP。要精确。
  4. 利用 C# 机制提出最小重构方案。 指出类型、模式或 DI 注册的变更——而不仅仅是说“应用该原则”。泛泛的“遵循 SOLID”建议不算一个发现。
  5. 根据实际影响进行优先级排序。 一个混用 EF 持久化和邮件发送的上帝服务在每个迭代中都会引起变更;一个包含 12 个成员但没有任何调用方完全使用的 IOrderService 是真正的 ISP 违规。一个恰好满足两个松散相关用例的 sealed 类,如果它只有一个变更原因,则不算违反 SRP。要区分那些造成伤害的违规和那些仅仅是不够纯粹的违规。

.NET / Azure 检查

SRP — 单一职责原则

  • 上帝服务/上帝类。 一个同时涉及 EF Core(_context.SaveChangesAsync)、发送邮件(_emailSender.SendAsync)、验证业务规则以及映射为 DTO 的单个类,有多个变更原因。将 IOrderValidator、IOrderRepository(或直接在专用持久化类中使用 DbContext)和 IOrderNotifier 提取为独立的协作对象。
  • 胖控制器。 一个包含 if/验证逻辑、直接调用数据库并格式化响应负载的 ASP.NET Core 控制器,承担了三项工作。控制器应该负责编排:调用服务、映射结果、返回 IActionResult。业务规则属于应用层/领域层。
  • 类型名称或职责中包含“与”。 OrderValidationAndPersistenceService 是一种命名异味,标志着 SRP 违规。如果你只能用“与”来描述这个类,那就拆分开。
  • 多个变更原因。 问一问:“如果更换邮件提供商,这个类需要修改吗?如果更改数据库模式呢?如果业务规则发生变化呢?” 有多个“是”意味着有多个职责。

OCP — 开闭原则

  • 在类型鉴别器上增长的 switch / if-else。 在服务内部针对每个新的 OrderType 枚举值都必须重新打开的 switch(order.Type) 块违反了 OCP。用策略模式替换:定义 IOrderHandler(或 IOrderProcessor),为每个类型注册一个实现;通过注入到协调器中的 IEnumerable<IOrderHandler> 进行解析,调度逻辑为 handler.Handles(order.Type)。
  • 硬编码的行为扩展。 如果添加一个新的折扣规则、通知渠道或导出格式需要修改现有类,而不是直接放入一个新的 IDiscountRule 或 IExporter,那么这个类没有对修改关闭。在第二个 case 出现之前就引入抽象。
  • 类型鉴别器反模式。 在处理类内部的 if (order is PremiumOrder) 或 if (typeof(T) == typeof(FooOrder)) 与上面的 switch 是等价的代码异味。多态或策略注册表可以消除这些。

LSP — 里氏替换原则

  • 抛出 throw new NotSupportedException() / NotImplementedException() 的重写。 一个无法履行基类契约的子类型(例如,ReadOnlyCollection<T> 在 Add 时抛出异常)违反了 LSP。如果重写无法有意义地实现,则层次结构是错误的——优先选择组合或独立的接口。
  • 削弱后置条件的子类型。 如果基类保证 GetOrders() 永远不会返回 null,那么返回 null 的重写就违反了调用方所依赖的契约。记录不变量(通过 XML 文档或契约)并在重写中强制执行。
  • 增强前置条件的子类型。 一个添加了基类中没有的 null 检查,或者拒绝了基类接受的合法参数范围的重写,会破坏传递了有效基类契约输入的代码。子类型必须接受基类所接受的一切。
  • 调用代码中普遍存在的 is/as 向下转型。 调用方中的 if (handler is SpecialHandler sh) sh.SpecialMethod() 意味着抽象被破坏了——要么该方法应该属于基接口,要么这些类型不应该共享一个层次结构。频繁的向下转型是 LSP/抽象失败的信号。

ISP — 接口隔离原则

  • 包含不相关关注的胖接口(10 个以上成员)。 一个暴露了 CreateOrder、CancelOrder、GetInvoice、SendConfirmation、ExportToCsv、CalculateTax、ValidateAddress 和 GetShippingRates 的 IOrderService,迫使每个实现者和测试替身去 stubbing 它不用的方法。拆分为角色接口:IOrderWriter、IOrderReader、IOrderNotifier、IOrderExporter。
  • 空实现 / NotImplementedException 桩。 如果一个 mock 或测试实现中一半的接口成员都返回 throw new NotImplementedException(),那么这些成员属于不同的角色接口。空实现的存在是 ISP 违规的直接证据。
  • 客户端引入了它们从未调用的方法。 如果 PaymentService 依赖 IOrderService 但只调用 GetOrder,那么它应该依赖 IOrderReader。缩小依赖范围使耦合显式化,并降低测试成本。

DIP — 依赖倒置原则

  • 在类内部通过 new 创建具体依赖。 在服务内部构造的 new HttpClient() 创建了隐藏的依赖,绕过了 DI 容器,导致 socket 泄漏(没有连接池),并且使得类在不使用反射技巧的情况下无法测试。注入 IHttpClientFactory(类型化客户端)或通过构造函数注入命名客户端。new SqlConnection(connectionString) 同理——注入 IDbConnectionFactory 或者让 EF Core / Dapper 通过注册的 DbContext 来管理连接。
  • 依赖具体类而非抽象。 一个构造函数类型为 SqlOrderRepository 而非 IOrderRepository 的类将自身耦合到了存储技术。任何应该可复用或可独立测试的类必须仅依赖接口或抽象类型。
  • 与 DateTime.Now / 静态状态的隐藏耦合。 业务方法内部的 var now = DateTime.Now 使得类在测试中是非确定性的,并且如果没有时间旅行技巧就无法测试。引入 ISystemClock(或使用 .NET 8+ 中的 TimeProvider)并注入它。Environment.MachineName、Random.Shared 或任何其他环境相关的静态成员也是如此。
  • 静态可变单例。 服务类中的 static Dictionary<string, Order> _cache 是隐藏的共享状态,绕过了 DI 生命周期管理,并且不是并发安全的。替换为通过 DI 注入的 IMemoryCache / IDistributedCache,它支持正确的作用域和驱逐策略。

判断说明: 并非所有的 new 都是 DIP 违规。new OrderDto(order)(一个普通的数据容器)或 new List<T>()(一个标准集合)是没问题的。DIP 关注的是构造的对象具有行为、外部 IO 或配置的情况——即当用测试替身或替代实现来替换它会产生价值的时候。

红旗标志

信号为什么重要
在服务方法或构造函数内部构造 new HttpClient()绕过了 IHttpClientFactory 连接池(负载下 socket 耗尽),隐藏了依赖,并使类在没有生产 IO 的情况下无法测试。
在协调类中使用 switch(order.Type) 或 if (x is ConcreteType)每个新变体都必须重新打开;调用方对类型层次结构了解过多。替换为从 DI 解析的 IEnumerable<IHandler>。
在非抽象重写中抛出 throw new NotImplementedException() 或 throw new NotSupportedException()子类型无法履行基类契约——LSP 违规。要么正确实现该方法,要么重构层次结构。
一个拥有 10+ 成员但调用方只使用其中 2-3 个的接口强制创建大量的测试替身;表明多个角色接口被合并为一个。
通过 new AppDbContext(options) 自行构造 DbContext 的类绕过了 DI 生命周期管理(Scoped vs Transient vs Singleton),阻止了连接池,并破坏了可测试性。注入 AppDbContext 或 IDbContextFactory<AppDbContext>。
在业务方法内直接调用 DateTime.Now在测试中非确定性;与系统时钟的耦合隐藏了逻辑依赖。注入 TimeProvider(.NET 8+)或 ISystemClock。
控制器操作包含 if/switch 业务逻辑或 EF 查询控制器承担了本属于应用层/领域层的职责;对业务规则的单一更改就需要触及控制器。
用作进程内缓存的 static 可变字段没有显式锁定时线程不安全;绕过了 DI 作用域;无法被驱逐或观察。使用 IMemoryCache。

示例

参见 examples/solid-review/。

相关技能