You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在同一模型中结合FromBody与FromRoute实现模型绑定

问题描述

我想在ASP.NET Core API中让AddStateContactDTO模型的StateId属性通过FromRoute绑定路由参数state_id,其余属性通过FromBody接收JSON数据。但按此实现后,POST JSON请求时出现400验证错误;改成FromForm提交表单数据时,模型绑定却能正常完成。想知道这是框架Bug,还是本身就不支持同一模型同时用FromBody和FromRoute?


初始API代码

[HttpPost("{state_id}/StateContact")]
[Authorize]
[OktaAllowedRoles(["arms-dmcp-user", "arms-oact-user", "arms-approver"])]
[EnableRateLimiting("api")]
public async Task<IActionResult> AddStateContact([FromBody] AddStateContactDTO addStateContactDTO)
{
    try
    {
        return Ok(addStateContactDTO);
    }
    catch(Exception ex)
    {
        return Ok(ex);
    } 
}

初始模型代码

public class AddStateContactDTO 
{
    [FromRoute(Name = "state_id")]
    public required int StateId {get; set;}

    [ModelBinder(Name = "first_name")]
    public string? FirstName {get; set;}

    [ModelBinder(Name = "last_name")]
    public string? LastName {get; set;}

    [ModelBinder(Name = "email_address")]
    [Required]
    [EmailAddress(ErrorMessage = "The Email address submitted is not a valid e-mail address.")]
    public required string EmailAddress {get; set;}
}

请求代码

const submitAddNewStateContact = () => {
if(addStateContactValid.value) {
  const json = {
    first_name : newStateContactFirstNameModel.value,
    last_name : newStateContactLastNameModel.value,
    email_address : newStateContactEmailAddressModel.value
  }

  api.post('/states/'+stateModel.value!.state_id+'/StateContact', json, config).then(response => {})
}
}

错误信息

{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
    "$": [
        "The JSON value could not be converted to System.String. Path: $ | LineNumber: 0 | BytePositionInLine: 1.",
        "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0.",
        "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0."
    ],
    "email_address": [
        "The EmailAddress field is required."
    ]
},
"traceId": "00-bcbe8b4460713f9266f5d6bbc15b940a-1746bfdbf5ba5213-00"
}

解答

这不是框架Bug,是ASP.NET Core模型绑定的既定行为:

  1. 当你在参数上标记[FromBody]时,框架会尝试将整个请求体(JSON)反序列化为该模型对象,此时模型内部的[FromRoute]属性标记会被忽略——因为[FromBody]优先级更高,框架不会再从路由提取数据填充模型。
  2. 改用[FromForm]时能正常工作,是因为表单绑定支持从多个来源(路由、表单字段、查询参数等)组合填充模型,这是表单绑定的设计特性。

正确实现方案

有两种常用的解决方式:

方式1:拆分参数,显式指定绑定来源

把路由参数单独作为方法参数,和DTO分开,逻辑更清晰:

[HttpPost("{state_id}/StateContact")]
public async Task<IActionResult> AddStateContact([FromRoute(Name = "state_id")] int stateId, [FromBody] AddStateContactBodyDTO bodyDto)
{
    // 手动合并参数到业务模型
    var addStateContactDto = new AddStateContactDTO
    {
        StateId = stateId,
        FirstName = bodyDto.FirstName,
        LastName = bodyDto.LastName,
        EmailAddress = bodyDto.EmailAddress
    };

    try
    {
        return Ok(addStateContactDto);
    }
    catch(Exception ex)
    {
        return Ok(ex);
    } 
}

// 新增只包含请求体字段的DTO
public class AddStateContactBodyDTO
{
    [ModelBinder(Name = "first_name")]
    public string? FirstName {get; set;}

    [ModelBinder(Name = "last_name")]
    public string? LastName {get; set;}

    [ModelBinder(Name = "email_address")]
    [Required]
    [EmailAddress(ErrorMessage = "The Email address submitted is not a valid e-mail address.")]
    public required string EmailAddress {get; set;}
}

方式2:自定义模型绑定器(适合复杂场景)

如果不想拆分DTO,可以自定义模型绑定器,让它同时从路由和请求体提取数据填充模型。不过这种方式相对复杂,仅在有特殊需求时推荐:

public class CombinedModelBinder : IModelBinder
{
    private readonly BodyModelBinder _bodyBinder;
    private readonly ComplexObjectModelBinder _routeBinder;

    public CombinedModelBinder(BodyModelBinder bodyBinder, ComplexObjectModelBinder routeBinder)
    {
        _bodyBinder = bodyBinder;
        _routeBinder = routeBinder;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // 先从路由绑定数据
        await _routeBinder.BindModelAsync(bindingContext);
        if (!bindingContext.Result.IsModelSet)
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        // 再从请求体绑定数据,覆盖冲突字段(可选)
        var bodyBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            bindingContext.ModelMetadata,
            bindingContext.BindingInfo,
            bindingContext.ModelName);

        await _bodyBinder.BindModelAsync(bodyBindingContext);
        if (bodyBindingContext.Result.IsModelSet)
        {
            // 合并两个模型的属性
            var routeModel = bindingContext.Result.Model;
            var bodyModel = bodyBindingContext.Result.Model;
            foreach (var prop in bindingContext.ModelMetadata.Properties)
            {
                var bodyValue = prop.PropertyGetter.GetValue(bodyModel);
                if (bodyValue != null)
                {
                    prop.PropertySetter.SetValue(routeModel, bodyValue);
                }
            }
        }
    }
}

// 在DTO上标记使用自定义绑定器
[ModelBinder(BinderType = typeof(CombinedModelBinder))]
public class AddStateContactDTO 
{
    [FromRoute(Name = "state_id")]
    public required int StateId {get; set;}

    [ModelBinder(Name = "first_name")]
    public string? FirstName {get; set;}

    [ModelBinder(Name = "last_name")]
    public string? LastName {get; set;}

    [ModelBinder(Name = "email_address")]
    [Required]
    [EmailAddress(ErrorMessage = "The Email address submitted is not a valid e-mail address.")]
    public required string EmailAddress {get; set;}
}

// 控制器方法去掉[FromBody]
public async Task<IActionResult> AddStateContact(AddStateContactDTO addStateContactDTO)
{
    // ... 逻辑不变
}

错误原因解析

错误信息里的JSON解析错误,本质是框架尝试把请求体的JSON反序列化为包含StateId的AddStateContactDTO,但请求体里没有StateId字段,而StateId是required属性,加上模型上的[FromRoute]标记干扰了JSON反序列化逻辑,最终触发双重错误:JSON解析失败 + 必填字段缺失。


内容的提问来源于stack exchange,提问作者Qiuzman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 12:45:04