Files
simapi-net/csharp-simapi-coding-style.md
T
2026-04-14 13:38:19 +08:00

829 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# C# .NET Web API 编码规范(SimApi 框架版)
> **适用范围**:所有基于 `Simcu.SimApi` 框架的 .NET Web API 项目,适用于任何 AI 辅助编程工具(Claude、ChatGPT、GitHub Copilot、Cursor 等)。
>
> **使用方式**:将本文档作为上下文提供给 AI,或在对话开头粘贴。
>
> **核心原则**:优先遵循已有代码的风格;本文档描述的是偏好,不是必须逐条套用的模板。
---
## 一、技术栈
```
NuGet: Simcu.SimApi
.NET 8 / 9 / 10
C# 12 / 14
Nullable: enable
ImplicitUsings: enable
```
---
## 二、SimApi 核心概念(必读)
### 2.1 响应格式
**所有接口统一输出 JSON,HTTP 状态码始终 200,错误信息在 `code` 字段**
```json
{ "code": 200, "message": "成功", "data": { ... } }
```
不要用 HTTP 4xx/5xx 表达业务错误。
### 2.2 常见错误(AI 容易犯的错)
| ❌ 错误 | ✅ 正确 |
|---------|---------|
| `SupportedMethod` 写多个方法 | 默认仅 `POST`,按需显式添加 |
| `WorkerNum = 50` | 默认是 `5` |
| 存储路径不加斜杠 `/avatars/file.jpg` | 路径必须以 `/` 开头 |
| `s.Endpoint = "http://minio:9000/"` | `ServeUrl`/`Endpoint` **不能以 `/` 结尾** |
| `synapse.PublishEvent(...)` | 方法名是 `synapse.Event(...)` |
| `synapse.CallRpcAsync(...)` | 方法名是 `synapse.Rpc<T>(...)` |
| HTTP 状态码 4xx/5xx 表示错误 | **所有错误均 HTTP 200**,错误在 JSON `code` 字段 |
| `SimApiStorageOptions = Configuration.GetSection(...)` | 用 `options.ConfigureSimApiStorage(s => {...})` |
| `[HttpGet]` / `[HttpPut]` / `[HttpDelete]` | **默认仅 POST**,其他方法需在 `SupportedMethod` 显式添加 |
### 2.3 异常处理流程
```
请求进入
└─ SimApiExceptionMiddleware(捕获所有异常 → HTTP 200 + code 字段)
└─ SimApiAuthMiddleware(解析 Token
└─ [SimApiSign] Filter
└─ [SimApiAuth] Filter
└─ OnActionExecuting(模型验证 → code 400
└─ Action 执行
└─ SimApiResponseFilter(封装响应)
```
---
## 三、C# 语言特性偏好
### 3.1 命名空间
使用**文件范围命名空间**,不用花括号块:
```csharp
// ✅
namespace MyApp.Controllers;
// ❌
namespace MyApp.Controllers { }
```
### 3.2 主构造函数(依赖注入)
使用**主构造函数**注入依赖,不写传统构造函数:
```csharp
// ✅
public class OrderController(DataContext db) : SimApiBaseController
{
}
// ❌
public class OrderController : SimApiBaseController
{
private readonly DataContext _db;
public OrderController(DataContext db) { _db = db; }
}
```
### 3.3 集合表达式
优先 `[]`,避免冗余的 `new`
```csharp
// ✅
string[] tags = [];
string[] roles = ["admin", "manager"];
// ❌
var tags = new string[] { };
var roles = new string[] { "admin", "manager" };
```
### 3.4 字符串
优先字符串插值,不用 `string.Format`
```csharp
// ✅
var msg = $"用户 {user.Name} 不存在";
// ❌
var msg = string.Format("用户 {0} 不存在", user.Name);
```
### 3.5 Null 处理
```csharp
var key = app?.Key; // 安全访问
var name = user?.Name ?? "匿名"; // 空合并
config ??= new Dictionary<string, string>(); // 空合并赋值
```
---
## 四、命名规范
| 类型 | 规则 | 示例 |
|------|------|------|
| 类、接口、枚举 | PascalCase | `AdminController``ResPermission` |
| 方法名 | PascalCase | `UserList``ApplicationEdit` |
| 属性名 | PascalCase | `AccountId``LicenseTotal` |
| 私有字段 | `_camelCase`(如有) | `_logger` |
| 局部变量、参数 | camelCase | `var user``var appId` |
| 常量 | PascalCase | `MaxRetryCount``DefaultRole` |
| 路由路径 | 全小写 + 连字符 | `/device/refresh-context` |
| 配置键 | PascalCase:PascalCase | `"Sms:Templates:Verify"` |
| Redis 缓存 Key | `模块:子类型:标识` | `"Sms:Verify:登陆:手机号"` |
---
## 五、项目目录结构
推荐**极简扁平化**,无 Service 层、无 Repository 层:
```
项目名/
├── Controllers/ # 控制器(含业务逻辑)
│ └── Dtos/ # 请求/响应 DTO
├── Models/ # EF Core 实体 + DataContext
├── Helpers/ # 工具类 / 框架扩展点
├── Migrations/ # EF Core 迁移(自动生成,勿手改)
└── Program.cs # 入口 + DI + 中间件(无 Startup.cs
```
业务逻辑直接在 Controller 中通过 `db`EF Core DbContext)操作数据库。可复用的横切逻辑抽取到 `Helpers/`
> 如果项目较复杂,也可以选择标准分层(Controllers → Services → Models),但需在项目内保持一致,不要混用。
---
## 六、Program.cs 规范
使用顶级语句(无 `Main` 方法、无 `Startup` 类):
```csharp
var builder = WebApplication.CreateBuilder(args);
// 1. SimApi 框架配置
builder.Services.AddSimApi(options =>
{
options.RedisConfiguration = builder.Configuration.GetConnectionString("Redis");
options.EnableSimApiAuth = true; // 按需开启
options.EnableSimApiDoc = true; // 按需开启
options.EnableSimApiStorage = false; // 按需开启
options.EnableJob = false; // 按需开启
options.EnableSynapse = false; // 按需开启
// Swagger 配置
options.ConfigureSimApiDoc(doc =>
{
doc.DocumentTitle = "接口文档";
doc.ApiGroups = [
new("api", "公共接口"),
new("admin", "管理接口")
];
doc.SupportedMethod = [SubmitMethod.Post]; // 默认仅 POST
});
// 存储配置(按需)
// options.ConfigureSimApiStorage(s => { ... });
});
// 2. 数据库
builder.Services.AddDbContext<DataContext>(opt =>
opt.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
// 3. 框架扩展点(用接口注册)
builder.Services.AddScoped<AesBodyProviderBase, AesBodyProvider>();
builder.Services.AddScoped<SimApiSignProviderBase, SimApiSignProvider>();
// 4. 项目自定义服务
builder.Services.AddScoped<ResPermission>();
builder.Services.AddSingleton<JsonSchemaHelper>();
var app = builder.Build();
// 5. 启动时自动迁移(同步写法)
app.Services.CreateScope().ServiceProvider
.GetRequiredService<DataContext>().Database.Migrate();
// 6. 框架中间件
app.UseSimApi();
app.Run();
```
### SimApiOptions 常用配置
```csharp
options.RedisConfiguration = "localhost:6379"; // Redis(多模块共用)
options.EnableSimApiAuth = false; // Token 认证
options.EnableSimApiDoc = false; // Swagger
options.EnableSimApiStorage = false; // S3 存储
options.EnableJob = false; // Hangfire 任务调度
options.EnableSynapse = false; // MQTT 通信
options.EnableCoceSdk = false; // Coce 统一身份
// 以下默认 true,通常不需要改:
options.EnableCors = true;
options.EnableSimApiException = true;
options.EnableSimApiResponseFilter = true;
options.EnableForwardHeaders = true;
options.EnableLowerUrl = true;
options.EnableVersionUrl = true;
options.EnableLogger = true;
```
---
## 七、控制器规范
### 7.1 基类与依赖注入
所有 Controller 继承 `SimApiBaseController`,依赖通过**主构造函数**注入:
```csharp
[SimApiAuth]
public class DeviceController(DataContext db) : SimApiBaseController
{
}
```
### 7.2 路由规则
**默认全部使用 POST**(除非在 `SupportedMethod` 中显式添加其他方法):
```csharp
// 方法上写完整路径
[HttpPost("/device/list")]
[HttpPost("/application/refresh-key")]
// 类上写前缀,方法上写相对路径
[Route("/platform")]
public class PlatformController(DataContext db) : SimApiBaseController
{
[HttpPost("device/detail")] // 最终路由:/platform/device/detail
[HttpPost("bot/generate")] // 最终路由:/platform/bot/generate
}
```
路由路径全小写,多词用连字符 `-` 分隔。
### 7.3 鉴权 Attribute
| Attribute | 用途 |
|-----------|------|
| `[SimApiAuth]` | 要求登录用户 |
| `[SimApiAuth("admin")]` | 要求 admin 角色 |
| `[SimApiAuth("admin,manager")]` | 逗号分隔,OR 关系 |
| `[SimApiSign(KeyProvider = typeof(XxxProvider))]` | API 签名验证 |
> 鉴权 Attribute 写在 **Controller 类** 上,不写在方法上。
### 7.4 接口分组与文档
```csharp
[ApiExplorerSettings(GroupName = "platform")]
[SimApiDoc("设备", "获取设备详情")]
[HttpPost("device/detail")]
public Device DeviceDetail(...)
```
### 7.5 方法返回值
| 场景 | 返回类型 |
|------|----------|
| 写操作(新增/修改/删除) | `void`(框架自动返回 `{"code":200}` |
| 单条查询 | 直接返回 Entity(如 `Account``Device` |
| 列表查询 | `Entity[]`(数组,不用 `List<T>` |
| 分页查询 | `PageResponse<Entity[]>` |
| 有状态响应 | `SimApiBaseResponse` |
| 复杂组合响应 | 对应 DTO |
> **不使用** `ActionResult<T>` 或 `IActionResult`(除非使用 `[AesBody]` 等框架 Attribute)。
### 7.6 方法参数
```csharp
// 普通请求体
[FromBody] DeviceDto.SerialAddRequest request
// AES 加密请求体
[AesBody(KeyProvider = typeof(AesBodyProvider))] BotDto.BotChatRequest request
// 查询字符串(直接写,不加 Attribute)
string appId
```
### 7.7 错误处理
使用基类的 `ErrorWhen` 系列方法,**不手动 throw、不返回错误码**:
```csharp
ErrorWhenNull(entity); // null 则报错(默认 404
ErrorWhenNull(entity, 404, "用户不存在"); // 自定义状态码和消息
ErrorWhen(condition, 400, "已经共享过了"); // condition 为 true 则报错
ErrorWhenFalse(condition, 403, "你无权操作"); // condition 为 false 则报错
```
在 Controller 外部(如 Helper 中)需要抛出异常时,使用 `SimApiException`
```csharp
throw new SimApiException(404, "App不存在");
throw new SimApiException(403, "没有权限修改对应资源");
```
### 7.8 当前登录信息
```csharp
// 通过基类属性获取(需 EnableSimApiAuth
var userId = LoginInfo?.Id;
var userRole = LoginInfo?.Type; // string[]
// 权限检查示例
ErrorWhen(!LoginInfo.Type.Contains("admin"), 403, "需要管理员权限");
```
### 7.9 私有辅助方法
Controller 内部可复用的逻辑提取为 `private` 方法,不独立成 Service
```csharp
private void CheckAppId(string appId)
{
ErrorWhen(!db.Applications.Any(x => x.AccountId == LoginInfo.Id && x.Id == appId), 403, "无权操作");
}
```
---
## 八、DTO 规范
### 8.1 组织方式
DTO 文件放在 `Controllers/Dtos/`,按业务域命名 `XxxDto.cs`。使用嵌套容器类:
```csharp
namespace MyApp.Controllers.Dtos;
public abstract class AdminDto
{
public class UserEditRequest
{
public required string Id { get; set; }
public required string Name { get; set; }
}
public class ApplicationListRequest : SimApiBasePageRequest
{
public string? Keyword { get; set; }
}
}
```
### 8.2 命名规则
| 类型 | 格式 | 示例 |
|------|------|------|
| 请求 DTO | `[动作]Request` | `UserEditRequest``DeviceSerialAddRequest` |
| 响应 DTO | `[动作]Response` | `GenerateResponse``TokenResponse` |
| 数据载体 | `[含义]Data` | `GenerateData``AgentItem` |
引用时用全限定名:`AdminDto.UserEditRequest``PlatformDto.GenerateResponse`
### 8.3 属性规则
```csharp
public class DeviceSerialAddRequest
{
public required string Verify { get; set; } // 必填
public required string AppId { get; set; }
public required string Name { get; set; }
[Range(1, 10000)] public required int Num { get; set; } // 范围校验
public string? Remark { get; set; } // 可选
public int Status { get; set; } = 1; // 有默认值
}
```
### 8.4 框架内置通用 DTO(优先复用)
| 框架 DTO | 用途 |
|----------|------|
| `SimApiStringIdOnlyRequest` | 只有 `Id` 字段的请求 |
| `SimApiOneFieldRequest<T>` | 只有一个 `Data` 字段的请求 |
| `SimApiBasePageRequest` | 分页请求基类(含 `Page``Count` |
| `SimApiBaseResponse` | 通用状态响应(可传 code + message |
| `SimApiBaseResponse<T>` | 带数据的响应 |
| `PageResponse<T>` | 分页响应(含 `Total``Page``Count``List` |
---
## 九、Entity 规范
### 9.1 基类
所有实体继承 `SimApiBaseModel`(自动提供 `Id``CreatedAt``UpdatedAt`):
```csharp
using SimApi.Models;
namespace MyApp.Models;
public class Account : SimApiBaseModel
{
public required string Name { get; set; }
public required string Username { get; set; }
public string? Password { get; set; }
public required string Role { get; set; } = "user";
public int Status { get; set; } = 1;
}
```
### 9.2 对象映射(MapData
`SimApiBaseModel` 提供 `MapData` 方法,用于 DTO ↔ Entity 映射:
```csharp
// 跳过 Id/CreatedAt/UpdatedAt,映射同名同类型且源值不为 null 的属性
entity.MapData(dto);
// 映射所有字段
entity.MapData(dto, mapAll: true);
// 只映射指定字段
entity.MapData(dto, new[] { "Name", "Email" });
// 手动更新 UpdatedAt
entity.UpdateTime();
```
### 9.3 属性规则
- 必填字段用 `required`,可选字段用 `?`,有默认值的直接赋值
- 外键命名:`[关联实体]Id`,如 `AccountId``AppId``ServiceId`
- **不配置导航属性**,不配置 EF Fluent API,依赖 Convention 自动映射
### 9.4 DataContext
只定义 DbSet,不做任何 Fluent API 配置:
```csharp
public class DataContext(DbContextOptions<DataContext> options) : DbContext(options)
{
public required DbSet<Account> Accounts { get; set; }
public required DbSet<Application> Applications { get; set; }
public required DbSet<Device> Devices { get; set; }
}
```
---
## 十、EF Core 查询风格
```csharp
// 列表查询(排序 + ToArray
db.Accounts.OrderBy(x => x.CreatedAt).ToArray();
// 动态条件查询(AsQueryable 后追加 Where
var query = db.Devices
.Where(x => x.ApplicationId == appId)
.OrderBy(x => x.CreatedAt)
.AsQueryable();
if (!string.IsNullOrEmpty(request.Serial))
query = query.Where(x => x.Serial == request.Serial);
// 分页查询(框架扩展方法 Paginate)
var list = query.Paginate(request.Page, request.Count).ToArray();
var total = query.Count();
return new PageResponse<Device[]> { List = list, Total = total, Page = request.Page, Count = request.Count };
// 单条查询
db.Accounts.Find(id); // 主键用 Find
db.Accounts.FirstOrDefault(x => x.Username == username); // 其他条件用 FirstOrDefault
// 写操作
db.Add(entity); // 新增
db.Update(entity); // 修改
db.Remove(entity); // 删除
db.SaveChanges(); // 统一在所有操作完成后调用一次
// 存在性判断(不用 Count
db.AppServices.Any(x => x.ServiceId == request.Id)
```
---
## 十一、框架扩展点(Helpers/
### 11.1 AES 加密请求体
实现 `AesBodyProviderBase`,提供解密密钥:
```csharp
public class AesBodyProvider(DataContext db) : AesBodyProviderBase
{
public override string? AppIdName { get; set; } = "appId";
public override string? GetKey(string? appId)
{
var application = db.Applications.Find(appId);
return application?.Key;
}
}
```
客户端提交格式:`{"data": "Base64(AES-256-CBC 加密 JSON)"}`
静态工具类(无需注入):
```csharp
string cipher = SimApiAesUtil.Encrypt("明文", "任意长度密钥");
string plain = SimApiAesUtil.Decrypt(cipher, "任意长度密钥");
```
### 11.2 API 签名验证
实现 `SimApiSignProviderBase`
```csharp
public class SimApiSignProvider(DataContext db) : SimApiSignProviderBase
{
public override string? AppIdName { get; set; } = "appId";
public override string TimestampName { get; set; } = "timestamp";
public override string NonceName { get; set; } = "nonce";
public override string SignName { get; set; } = "sign";
public override int QueryExpires { get; set; } = 5;
public override bool DuplicateRequestProtection { get; set; } = true;
public override string[] SignFields { get; set; } = ["userId"];
public override string? GetKey(string? appId)
{
return db.Applications.Find(appId)?.SecretKey;
}
}
```
> 扩展点用**接口注册**`builder.Services.AddScoped<AesBodyProviderBase, AesBodyProvider>()`
### 11.3 Helper 使用原则
Helper 仅用于以下场景,**不承担 CRUD 业务逻辑**:
1. **框架扩展点**:继承 `XxxProviderBase` 并 override 方法
2. **横切关注点**:权限校验、短信发送
3. **纯工具静态类**:无状态工具方法
4. **有状态单例/Scoped 服务**:如 JSON Schema 验证
---
## 十二、框架功能模块
### 12.1 认证(SimApiAuth
```csharp
// Program.cs
options.EnableSimApiAuth = true;
options.RedisConfiguration = "..."; // 必须
// Token 通过 Header 传入:Token: <value>
// DI 注入 SimApiAuth 服务
public MyController(SimApiAuth auth) { }
// 登录
string token = auth.Login(loginItem); // 自动生成 GUID token
string token = auth.Login(loginItem, "custom-token");
auth.Update(loginItem, token);
// 查询/退出
SimApiLoginItem? info = auth.GetLogin(token);
auth.Logout(token);
// SimApiLoginItem 结构
// { Id: string, Type: string[], Meta: Dictionary<string,string>, Extra: object? }
```
自动路由(开启 `EnableSimApiAuth` 后可用):
- `POST /auth/check` — 检测登录状态
- `POST /auth/logout` — 退出登录
- `POST /user/info` — 获取用户信息(需登录)
### 12.2 对象存储(SimApiStorage
```csharp
// Program.cs
options.EnableSimApiStorage = true;
options.ConfigureSimApiStorage(s =>
{
s.Endpoint = "http://minio:9000"; // 不能以 / 结尾
s.AccessKey = "admin";
s.SecretKey = "pass";
s.Bucket = "my-bucket";
s.ServeUrl = "http://cdn.example.com/my-bucket"; // 不能以 / 结尾
});
// DI 注入
public MyController(SimApiStorage storage) { }
// 路径必须以 / 开头
storage.GetUploadUrl("/avatars/user1.jpg"); // 返回上传地址和下载地址
storage.GetDownloadUrl("/files/doc.pdf"); // 默认 10 分钟过期
storage.GetDownloadUrl("/files/doc.pdf", expire: 3600);
storage.UploadFile("/path/file.jpg", stream, "image/jpeg"); // 服务端直传
storage.FullUrl("/path/file"); // 路径转完整 URL
storage.GetPath("http://cdn.../my-bucket/path/file"); // URL 转路径
```
### 12.3 Redis 缓存(SimApiCache
```csharp
public MyService(SimApiCache cache) { }
cache.Set("key", value); // 永不过期
cache.Set("key", value, new DistributedCacheEntryOptions {
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
string? raw = cache.Get("key"); // 原始字符串
int? val = cache.Get<int>("key"); // 反序列化
// key 自动加前缀 SimApi:Cache:
```
### 12.4 任务调度(Hangfire
```csharp
// Program.cs
options.EnableJob = true;
options.ConfigureSimApiJob(job =>
{
job.DashboardUrl = "/jobs";
job.DashboardAuthUser = "admin";
job.DashboardAuthPass = "Admin@123!";
job.Servers = [
new SimApiJobServerConfig { Queues = ["default"], WorkerNum = 5 },
new SimApiJobServerConfig { Queues = ["email"], WorkerNum = 2 }
];
});
// 使用
BackgroundJob.Enqueue(() => myService.DoWork());
BackgroundJob.Schedule(() => myService.DoWork(), TimeSpan.FromMinutes(5));
RecurringJob.AddOrUpdate("job-id", () => myService.DoWork(), Cron.Daily);
var id = BackgroundJob.Enqueue(() => Step1());
BackgroundJob.ContinueJobWith(id, () => Step2());
```
### 12.5 MQTT 通信(Synapse
```csharp
// Program.cs
options.EnableSynapse = true;
options.ConfigureSimApiSynapse(s =>
{
s.Websocket = "ws://mqtt:8083/mqtt"; // WebSocket 连接(不是 RabbitMQ
s.Username = "user";
s.Password = "pass";
s.SysName = "my-system";
s.AppName = "order-service";
s.AppId = "instance-001";
s.RpcTimeout = 3;
});
// DI 注入
public MyService(Synapse synapse) { }
// 发布事件
synapse.Event("order/created", new { OrderId = 1 });
// RPC 调用(同步,返回 SimApiBaseResponse<T>
var res = synapse.Rpc<UserDto>("user-service", "GetUserInfo", new { Id = 1 });
// RPC 方法内部抛错
synapse.RpcError(400, "参数错误");
synapse.RpcErrorWhen(id <= 0, 400, "ID 无效");
// 事件处理器(自动扫描注册)
public class OrderEventHandler
{
[SynapseEvent("order/+/status")] // 支持 + 和 # 通配符
public void OnOrderStatus(string eventName, OrderStatusDto data) { }
}
// RPC 服务(自动扫描注册)
public class UserRpcService
{
[SynapseRpc] // 注册为 "UserRpcService.GetUserInfo"
public UserDto GetUserInfo(GetUserRequest req) { return ...; }
[SynapseRpc("customName")]
public ResultDto DoSomething(RequestDto req, Dictionary<string, string> headers) { }
}
```
### 12.6 HTTP 客户端(SimApiHttpClient
```csharp
var client = new SimApiHttpClient(appId: "myapp", appKey: "secret")
{
Server = "https://api.example.com",
};
var r = client.SignQuery<T>("/api/user", body, queries); // 仅签名
var r = client.AesQuery<T>("/api/user", body); // 仅 AES 加密
var r = client.AesSignQuery<T>("/api/user", body, queries); // AES + 签名
```
### 12.7 工具类(SimApiUtil,全部静态)
```csharp
DateTime cst = SimApiUtil.CstNow; // UTC+8 当前时间
double ts = SimApiUtil.TimestampNow; // 秒级 Unix 时间戳
string md5 = SimApiUtil.Md5("src"); // 32位 MD5
string json = SimApiUtil.Json(obj); // camelCase,中文不转义
bool ok = SimApiUtil.CheckCell("13800138000"); // 手机号验证
```
---
## 十三、配置文件规范
`appsettings.json` 只保留框架默认值:
```json
{
"Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } },
"AllowedHosts": "*"
}
```
`appsettings.Development.json` 存放开发环境实际配置(不提交到 Git):
```json
{
"ConnectionStrings": {
"Default": "Host=...;Database=...;Username=...;Password=...",
"Redis": "host:port,defaultDatabase=N"
},
"Sms": { "Account": "...", "Password": "..." }
}
```
配置读取:
```csharp
builder.Configuration.GetConnectionString("Default")
config["Gateway:Key"]
config.GetSection("Sms").GetSection("Templates")["verify"]
```
---
## 十四、注释规范
- **公有 API / 方法**:写 XML 文档注释
- **私有方法**:逻辑简单可不写;复杂逻辑写行内注释说明**为什么**
- **不要写废话注释**
```csharp
/// <summary>
/// 根据邮箱查询用户,不存在返回 null。
/// </summary>
public Account? FindByEmail(string email)
=> db.Accounts.FirstOrDefault(x => x.Email == email);
// ❌ 废话注释
// 查询用户
var user = db.Accounts.Find(id);
// ✅ 有意义的注释
// EF Core 的 Find 会优先命中一级缓存
var user = db.Accounts.Find(id);
```
---
## 十五、禁止事项
以下模式在使用 SimApi 框架时**明确禁止**
| ❌ 禁止 | ✅ 正确做法 |
|---------|------------|
| 使用 HTTP 4xx/5xx 表达业务错误 | HTTP 200 + JSON `code` 字段 |
| `throw new Exception(message)` | `ErrorWhen` 系列或 `SimApiException` |
| 新建 Service / Repository 层(除非项目明确需要) | Controller 直接操作 DbContext |
| 使用 `ActionResult<T>` / `IActionResult` | 直接返回 Entity / `void` / `SimApiBaseResponse` |
| 在 Controller 方法上加鉴权 Attribute | 加在 Controller 类上 |
| 在 Entity 中配置导航属性或 EF Fluent API | 依赖 Convention 自动映射 |
| 在 DbContext 中写 `OnModelCreating`(除非需要全局过滤等) | 只定义 DbSet |
| 花括号块命名空间 | 文件范围命名空间 |
| 传统构造函数注入 | 主构造函数 |
| `new List<T>()` 初始化空集合 | `[]` 集合表达式 |
| `Count() > 0` 判断存在 | `Any()` |
| `ToList()` 再转数组 | 直接 `ToArray()` |
| `string.IsNullOrEmpty` 判断必填入参 | `required` 修饰符 + 模型验证 |
| 全局 `catch (Exception e) { log; return null; }` | 让异常冒泡,由 SimApiExceptionMiddleware 处理 |