排坑日记:ASP.NET Core 中 "Required field is not provided" 验证错误全记录

问题出现

在开发一个基于 ASP.NET Core 10.0 的 Web API 项目时,遇到了一个令人困惑的验证错误。

库存扣减接口的原始代码如下:

csharp 复制代码
[HttpPost("{drugId}/decrement/{quantity}")]
public async Task<string?> DecInventoryAsync(
    [FromRoute] string drugId,
    [FromRoute] int quantity)
{
    var inventory = await _inventoryService.DeductInventoryAsync(drugId, quantity);
    return inventory;
}

通过 Swagger UI 调用接口:

bash 复制代码
POST /api/inventories/drug001/decrement/10

返回的却是 400 错误:

json 复制代码
{
    "errors": {
        "quantity": ["Required field is not provided."]
    }
}

这就很奇怪了 ------URL 路径里明明写着 decrement/10quantity 的值 10 就在那里,怎么说没提供?


问题排查

既然路由拼写和参数绑定看起来都没问题,那就一步步缩小范围。

第一步:排除 [FromRoute] 显式标记的影响

假设[FromRoute] 显式标注与 [ApiController] 的自动推断产生了冲突。

验证 :去掉 [FromRoute],让框架自动推断绑定来源:

csharp 复制代码
[HttpPost("{drugId}/decrement/{quantity}")]
public async Task<string?> DecInventoryAsync(string drugId, int quantity)

结果 :依然报错 "Required field is not provided."假设不成立 ,问题不在 [FromRoute]

第二步:排查参数类型的影响

假设 :非可空值类型 int[ApiController] 隐式标记为 [Required],导致验证管道的误判。

验证 :将 int quantity 改为可空值类型 int?

csharp 复制代码
public async Task<string?> DecInventoryAsync(
    [FromRoute] string drugId,
    [FromRoute] int? quantity)

结果 :错误消失了!→ 假设成立

关键发现 :问题出在 int 这个非可空值类型 上。[ApiController] 对非可空值类型会自动添加 [Required] 验证,而在多参数 [FromRoute] 绑定场景下,验证管道无法正确识别该参数已被路由提供,于是报了误判错误。

第三步:确认根因

翻阅官方文档,[ApiController] 的隐式规则总结如下:

  1. 对非可空值类型自动添加 [Required] 验证
  2. 对所有参数进行自动模型验证
  3. 当存在多个 [FromRoute] 参数时,原生 AddOpenApi() 生成的 OpenAPI schema 和模型验证管道之间可能存在判断不一致

根因确诊[ApiController] 自动验证 + int 非可空类型 + 多 [FromRoute] 参数 + AddOpenApi() 四者叠加,触发了验证管道的兼容性边界 bug。


可选修复方案

确定了根因,接下来评估修复方案。核心思路是打破触发条件中的任意一环

方案 做法 打破的条件 评估
① 可空类型 intint? 非可空类型 → 不触发隐式 [Required] 改动最小,但需手动校验,且路由参数用 int? 语义不自然
② 去掉 [FromRoute] 不显式标注绑定来源 去掉显式标记 已验证无效,不可采用
[AsParameters] 用 Record 封装多参数 让验证管道正确识别绑定来源 有效,但需额外定义类型,且路由风格未改善
[BindRequired] 用显式绑定替代隐式 [Required] 替换隐式 Required 治标不治本,未来可能再踩坑
请求体 DTO 参数从路由移至 Body 彻底消除多 [FromRoute] 场景 RESTful 规范,验证管道成熟稳定,错误提示友好

选择方案 ⑤ 的理由:

  • 从根源上消除了"多 [FromRoute] 参数"这个触发条件
  • POST 请求将业务参数放在 Body 中更符合 RESTful 设计规范
  • [FromBody] 的验证管道非常成熟,不会再出现此类边界问题
  • 可以配合 [Required][Range] 等特性提供友好的中文错误提示

最终修复:使用 InventoryDecrementDto 请求体

新增 DTO 对象

csharp 复制代码
// Models/Dto/InventoryDecrementDto.cs
using System.ComponentModel.DataAnnotations;

namespace MyWebApp.Models.Dto;

public class InventoryDecrementDto
{
    [Required(ErrorMessage = "药品ID不能为空")]
    public string DrugId { get; set; }

    [Required(ErrorMessage = "库存数量不能为空")]
    [Range(1, int.MaxValue, ErrorMessage = "库存数量必须大于0")]
    public int Quantity { get; set; }
}

修改控制器

csharp 复制代码
// 修改前
[HttpPost("{drugId}/decrement/{quantity}")]
public async Task<string?> DecInventoryAsync(
    [FromRoute] string drugId,
    [FromRoute] int quantity)

// 修改后
[HttpPost("decrement")]
public async Task<string?> DecInventoryAsync(InventoryDecrementDto decrementDto)
{
    var inventory = await _inventoryService.DeductInventoryAsync(
        decrementDto.DrugId,
        decrementDto.Quantity);
    return inventory;
}

新的接口签名

bash 复制代码
POST /api/inventories/decrement
Content-Type: application/json

{
    "drugId": "drug001",
    "quantity": 10
}

修复效果对比

对比维度 修复前 修复后
路由风格 POST /{drugId}/decrement/{quantity} POST /decrement + JSON Body
参数绑定 [FromRoute] × 2(触发 bug) [FromBody](默认,稳定)
验证行为 误判 "Required field is not provided." 正常校验,中文错误提示
不传 drugId 误判为 quantity 缺失 返回 "药品ID不能为空"
不传 quantity 误判为 quantity 缺失 返回 "库存数量不能为空"
quantity 为 0 误判为 quantity 缺失 返回 "库存数量必须大于0"

问题根因

scss 复制代码
[ApiController] 自动验证
       +
非可空值类型 int → 隐式添加 [Required]
       +
多个 [FromRoute] 参数
       +
.NET 原生 AddOpenApi() 的 schema 生成逻辑
       ↓
验证管道无法正确识别路由参数的绑定来源
       ↓
"Required field is not provided."(误报)

核心矛盾:[ApiController]int 自动加了 [Required],但 AddOpenApi() 在处理多 [FromRoute] 参数时,生成的 OpenAPI schema 与验证管道的判断逻辑不一致,导致验证管道认为 quantity 没有被提供。


经验总结

  1. "Required field is not provided" 不一定是真的没传参数------遇到这个错误,先检查是不是非可空值类型 + 多来源绑定的组合触发了框架的边界 bug。
  2. 排查这类问题最快的切入点是改类型 :把 int 改成 int? 试一下,如果错误消失,问题就在非可空类型的隐式 [Required] 上。
  3. POST 请求的参数放 Body 是最稳妥的做法[FromBody] 的模型绑定和验证管道是 ASP.NET Core 中最成熟、踩坑最少的路径,能避免大量路由参数绑定的边界问题。
  4. DTO 模式一举两得:既解决了验证问题,又让 API 设计更规范,还方便了参数校验和 OpenAPI 文档生成。
相关推荐
用户8356290780512 小时前
使用 Python 自动化 PowerPoint 形状布局与格式设置
后端·python
Oneslide3 小时前
sudo免密权限配置不生效
后端
站大爷IP3 小时前
为什么Python不用var或let声明变量?
后端
赴星半途3 小时前
NestJS实战-创建AuthService
后端
北冥有鱼3 小时前
mqtt 测试
前端·后端
代码丰3 小时前
使用 TtlExecutors 解决线程池中的 ThreadLocal 上下文丢失问题
后端
阿祖zu3 小时前
别再优化 RAG 了,适配 Agent 的 LLM Wiki 知识库理念
前端·后端·aigc
昵称为空C4 小时前
手撸一个动态 SQL 执行引擎:不重启服务,在线增删改查任意数据库
spring boot·后端