Skip to content

Latest commit

 

History

History
828 lines (629 loc) · 24.2 KB

File metadata and controls

828 lines (629 loc) · 24.2 KB

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 字段

{ "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 命名空间

使用文件范围命名空间,不用花括号块:

// ✅
namespace MyApp.Controllers;

// ❌
namespace MyApp.Controllers { }

3.2 主构造函数(依赖注入)

使用主构造函数注入依赖,不写传统构造函数:

// ✅
public class OrderController(DataContext db) : SimApiBaseController
{
}

// ❌
public class OrderController : SimApiBaseController
{
    private readonly DataContext _db;
    public OrderController(DataContext db) { _db = db; }
}

3.3 集合表达式

优先 [],避免冗余的 new

// ✅
string[] tags = [];
string[] roles = ["admin", "manager"];

// ❌
var tags = new string[] { };
var roles = new string[] { "admin", "manager" };

3.4 字符串

优先字符串插值,不用 string.Format

// ✅
var msg = $"用户 {user.Name} 不存在";

// ❌
var msg = string.Format("用户 {0} 不存在", user.Name);

3.5 Null 处理

var key = app?.Key;                         // 安全访问
var name = user?.Name ?? "匿名";            // 空合并
config ??= new Dictionary<string, string>(); // 空合并赋值

四、命名规范

类型 规则 示例
类、接口、枚举 PascalCase AdminControllerResPermission
方法名 PascalCase UserListApplicationEdit
属性名 PascalCase AccountIdLicenseTotal
私有字段 _camelCase(如有) _logger
局部变量、参数 camelCase var uservar appId
常量 PascalCase MaxRetryCountDefaultRole
路由路径 全小写 + 连字符 /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 类):

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 常用配置

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,依赖通过主构造函数注入:

[SimApiAuth]
public class DeviceController(DataContext db) : SimApiBaseController
{
}

7.2 路由规则

默认全部使用 POST(除非在 SupportedMethod 中显式添加其他方法):

// 方法上写完整路径
[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 接口分组与文档

[ApiExplorerSettings(GroupName = "platform")]
[SimApiDoc("设备", "获取设备详情")]
[HttpPost("device/detail")]
public Device DeviceDetail(...)

7.5 方法返回值

场景 返回类型
写操作(新增/修改/删除) void(框架自动返回 {"code":200}
单条查询 直接返回 Entity(如 AccountDevice
列表查询 Entity[](数组,不用 List<T>
分页查询 PageResponse<Entity[]>
有状态响应 SimApiBaseResponse
复杂组合响应 对应 DTO

不使用 ActionResult<T>IActionResult(除非使用 [AesBody] 等框架 Attribute)。

7.6 方法参数

// 普通请求体
[FromBody] DeviceDto.SerialAddRequest request

// AES 加密请求体
[AesBody(KeyProvider = typeof(AesBodyProvider))] BotDto.BotChatRequest request

// 查询字符串(直接写,不加 Attribute)
string appId

7.7 错误处理

使用基类的 ErrorWhen 系列方法,不手动 throw、不返回错误码

ErrorWhenNull(entity);                           // null 则报错(默认 404)
ErrorWhenNull(entity, 404, "用户不存在");          // 自定义状态码和消息
ErrorWhen(condition, 400, "已经共享过了");          // condition 为 true 则报错
ErrorWhenFalse(condition, 403, "你无权操作");      // condition 为 false 则报错

在 Controller 外部(如 Helper 中)需要抛出异常时,使用 SimApiException

throw new SimApiException(404, "App不存在");
throw new SimApiException(403, "没有权限修改对应资源");

7.8 当前登录信息

// 通过基类属性获取(需 EnableSimApiAuth)
var userId = LoginInfo?.Id;
var userRole = LoginInfo?.Type;  // string[]

// 权限检查示例
ErrorWhen(!LoginInfo.Type.Contains("admin"), 403, "需要管理员权限");

7.9 私有辅助方法

Controller 内部可复用的逻辑提取为 private 方法,不独立成 Service:

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。使用嵌套容器类:

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 UserEditRequestDeviceSerialAddRequest
响应 DTO [动作]Response GenerateResponseTokenResponse
数据载体 [含义]Data GenerateDataAgentItem

引用时用全限定名:AdminDto.UserEditRequestPlatformDto.GenerateResponse

8.3 属性规则

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 分页请求基类(含 PageCount
SimApiBaseResponse 通用状态响应(可传 code + message)
SimApiBaseResponse<T> 带数据的响应
PageResponse<T> 分页响应(含 TotalPageCountList

九、Entity 规范

9.1 基类

所有实体继承 SimApiBaseModel(自动提供 IdCreatedAtUpdatedAt):

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 映射:

// 跳过 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,如 AccountIdAppIdServiceId
  • 不配置导航属性,不配置 EF Fluent API,依赖 Convention 自动映射

9.4 DataContext

只定义 DbSet,不做任何 Fluent API 配置:

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 查询风格

// 列表查询(排序 + 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,提供解密密钥:

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)"}

静态工具类(无需注入):

string cipher = SimApiAesUtil.Encrypt("明文", "任意长度密钥");
string plain  = SimApiAesUtil.Decrypt(cipher, "任意长度密钥");

11.2 API 签名验证

实现 SimApiSignProviderBase

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)

// 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)

// 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)

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)

// 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)

// 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)

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,全部静态)

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 只保留框架默认值:

{
  "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } },
  "AllowedHosts": "*"
}

appsettings.Development.json 存放开发环境实际配置(不提交到 Git):

{
  "ConnectionStrings": {
    "Default": "Host=...;Database=...;Username=...;Password=...",
    "Redis": "host:port,defaultDatabase=N"
  },
  "Sms": { "Account": "...", "Password": "..." }
}

配置读取:

builder.Configuration.GetConnectionString("Default")
config["Gateway:Key"]
config.GetSection("Sms").GetSection("Templates")["verify"]

十四、注释规范

  • 公有 API / 方法:写 XML 文档注释
  • 私有方法:逻辑简单可不写;复杂逻辑写行内注释说明为什么
  • 不要写废话注释
/// <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 处理