其他
在 ASP.NET Core 中新增或修改 Web API…
为 ASP.NET Core Web API 添加/修改端点(控制器或 minimal API)、配置 OpenAPI/Swagger、编写 .http 测试文件、设置全局错误处理中间件时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 48/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 53/100 ×0.15
适用场景
在 ASP.NET Core 中新增或修通用
解决思路
- 先扫描项目确定 API 风格:搜索继承 ControllerBase 或带 [ApiController] 的类,搜索 Program.cs 或端点文件中的 app.MapGet、app.MapPost 等;已有 controllers 就继续用 controllers,已有 minimal APIs 就继续用 minimal APIs;两者都不存在(新项目)时默认用 minimal APIs,除非用户明确要求 controllers;不要在同一项目中混用两种风格。
- 为 API 输入输出定义专用类型,绝不直接在请求/响应体中暴露 EF Core 实体。
- 所有 DTO 使用 sealed record:record 保证不可变、提供基于值的相等性并生成简洁代码,sealed 防止意外继承并启用 JIT devirtualization(CA1852)。
- 按命名约定命名 DTO:创建输入用 Create{Entity}Request,更新输入用 Update{Entity}Request,单个输出用 {Entity}Response,列表输出用 {Entity}ListResponse。
- 为所有对外暴露的请求和响应类型添加 <summary> XML 文档注释,这些注释会自动进入生成的 OpenAPI 规范,无需额外元数据调用。
- DTO 中的日期时间属性一律使用 DateTimeOffset 而非 DateTime:它保留 UTC 偏移、避免时区转换歧义,并在 JSON 中序列化为带偏移的 ISO 8601。
- JSON 序列化默认保持现有行为:对已有 API,除非项目已使用或用户明确要求,不要引入更严格的序列化/反序列化设置(如大小写敏感匹配、严格数字处理),以免破坏现有客户端;仅对新项目或用户明确要求严格 JSON 处理时,才配置 ConfigureHttpJsonOptions,设置 NumberHandling = JsonNumberHandling.Strict、PropertyNameCaseInsensitive = false、AllowDuplicateProperties = false、DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull。
- 枚举属性默认序列化为字符串(除非用户明确要求整数序列化):字符串枚举可读性更好、值重排时更不易出错,并生成更好的 OpenAPI 文档;通过 JsonStringEnumConverter 配置。
- 响应 DTO 使用位置式 sealed record 实现简洁、不可变的输出;请求 DTO 使用带 init 属性的 sealed record,以便数据注解自然生效。
- Minimal API 的数据注解验证需显式注册:在 .NET 10+ 项目的 Program.cs 中添加 builder.Services.AddValidation(),它会接入端点过滤器,在处理器执行前验证带数据注解的参数,失败时返回 400 Bad Request 和验证问题详情响应。
- 不要用可变类({ get; set; })作为 DTO:可变 DTO 允许构造后被意外修改,并失去 record 提供的自文档化不可变性。
- Minimal API 端点按资源组织:每个资源一个静态类(如 ProductEndpoints、CategoryEndpoints),定义静态 Map<Resource>(this WebApplication app) 扩展方法,在其中调用 MapGet、MapPost、MapPut、MapDelete 等,并在 Program.cs 中按顺序调用各资源的 Map 方法。
- Minimal API 返回类型优先使用 TypedResults 而非 Results 工厂:TypedResults 在方法签名中嵌入响应类型信息,自动为 OpenAPI 生成器提供更丰富的元数据。
- 当处理器返回多种结果类型(如 Ok 或 NotFound)时,用显式 Results<T1, T2> 返回类型标注 lambda,以便在仍使用 TypedResults 的同时给编译器一个公共类型。
- 不要在裸三元表达式中使用 TypedResults.Ok(x) 和 TypedResults.NotFound() 而不加显式返回类型标注:Ok<T> 和 NotFound 没有编译器可推断的公共基类,会导致 CS1593 错误。
- 若处理器有大量条件分支(7 种以上结果类型),可回退使用 Results 工厂(Results.Ok()、Results.NotFound())返回 IResult,牺牲编译期 OpenAPI 推断以换取更简单的签名。
- 遵循 HTTP 状态码约定:GET 单个成功 200 OK、常见错误 404 Not Found;GET 列表成功 200 OK;POST 创建成功 201 Created 带 Location 头、常见错误 400 Bad Request 和 409 Conflict;PUT 全量更新成功 200 OK、常见错误 400 Bad Request 和 404 Not Found;PATCH 部分更新/动作成功 200 OK、常见错误 400 Bad Request 和 404 Not Found;DELETE 成功 204 No Content、常见错误 404 Not Found 和 409 Conflict。
- POST 201 响应必须返回指向新建资源的 Location 头:controllers 用 CreatedAtAction(nameof(GetById), new { id = ... }, response),minimal APIs 用 TypedResults.Created($"/api/products/{id}", response)。
- 每个端点签名都接受 CancellationToken,并将其转发到所有异步调用(服务方法、EF Core 查询、HttpClient 调用),以便客户端断开时服务器停止工作。
- 接入 OpenAPI:.NET 9+ 项目使用内置支持(builder.Services.AddOpenApi() + 开发环境中的 app.MapOpenApi()),无需额外包;不要向 .NET 9+ 项目添加任何 Swashbuckle.* NuGet 包(Swashbuckle.AspNetCore、Swashbuckle.AspNetCore.SwaggerUI、Swashbuckle.AspNetCore.SwaggerGen 等),因为 Swashbuckle 与 .NET 9+ 和 .NET 10 的 OpenAPI 类型存在已知兼容性问题;.NET 8 及更早项目可用 Swashbuckle;若项目已安装 Swashbuckle,除非用户要求移除,否则保留。
- 为端点添加描述性 OpenAPI 元数据:minimal APIs 链式调用 .WithName()、.WithSummary()、.WithDescription()、.Produces<T>()、.Produces(StatusCodes.Status404NotFound) 等。
- 配置枚举字符串序列化:minimal APIs 用 builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));controllers/MVC 用 builder.Services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));两者使用不同的选项类型,都要配置。
- 使用全局异常处理器,避免各端点写 try-catch,所有错误响应返回 RFC 7807 Problem Details;.NET 8+ 项目优先使用内置异常处理中间件:builder.Services.AddProblemDetails()、app.UseExceptionHandler()、app.UseStatusCodePages()。
- 如需自定义异常到状态码的映射(如 NotFoundException 返回 404),实现 IExceptionHandler:在 TryHandleAsync 中用 switch 映射异常到 (statusCode, title),未匹配时返回 false 交给默认处理器;返回 true 前先记录日志/遥测(返回 true 会抑制该异常的诊断中间件);设置 httpContext.Response.StatusCode 并用 WriteAsJsonAsync 写入 ProblemDetails(Status、Title、Detail、Instance);不要使用 exception.Message 作为 Detail,以免泄露敏感内部细节,改用安全的面向用户的消息。
- 注册自定义异常处理器:builder.Services.AddExceptionHandler<ApiExceptionHandler>()、builder.Services.AddProblemDetails()、app.UseExceptionHandler()。
- 异常处理器类始终放在 Middleware/ 文件夹中,不要放在项目根目录。
- 使用服务层:不要把数据存储直接注入 controllers 或端点处理器;创建服务接口和 sealed 实现类,由它负责数据访问逻辑以及实体与请求/响应类型之间的映射。
- 为每个服务定义接口(便于用 mock 做单元测试,遵循依赖倒置原则),并按接口注册:builder.Services.AddScoped<IProductService, ProductService>()。
- 实现端点后,在项目根目录创建 .http 文件,为每个新端点提供调用示例,作为活文档和快速手动测试工具;每个端点至少一个请求并带真实请求体,展示错误路径(如不存在的 ID),端口与 launchSettings.json 匹配。
- 构建并验证:运行 dotnet build 确认零错误零警告;启动应用确认 OpenAPI 文档可加载(默认 /openapi/v1.json);运行 .http 文件中的请求确认状态码正确。
- 按验证清单自检:状态码正确、POST 返回 201 带 Location、DELETE 返回 204、每个端点含 CancellationToken 并向下游转发、OpenAPI 文档包含所有新端点且有 summary/description、枚举以字符串出现、错误用 RFC 7807、不暴露领域实体、所有 DTO 有 <summary>、日期时间用 DateTimeOffset、.http 文件覆盖每个端点、dotnet build 零错误零警告、DTO 均为 sealed record、minimal API 用 TypedResults 加显式 Results<T1, T2>、每个服务有接口并注册 DI、异常处理器在 Middleware/ 文件夹。
适用边界 · 注意事项
- 通用 C# 编码风格或非 API 重构
- EF Core 数据建模或查询优化(改用 optimizing-ef-core-queries 技能)
- 前端、Razor 或 Blazor UI 改动
- gRPC 服务
- SignalR hub 或实时消息流程