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

在.NET Core 8中使用FromBody结合自定义模型绑定遇ValueProvider为空问题

解决ASP.NET Core中[FromBody]自定义模型绑定器ValueProvider返回null的问题

问题场景

X是可通过API执行若干操作的实体,所有操作共用一个通用控制器端点,输入模型需继承自ExecuteActionInputModel。为实现多态模型绑定,自定义了ActionInputModelBinder,但使用[FromBody]时,modelBindingContext.ValueProvider.GetValue()始终返回null,改用[FromForm]则正常运行。

相关代码如下:

控制器端点

[HttpPost("{id}/execute-action")]
public async Task<IActionResult> 
    ExecuteAction([FromRoute] Guid id,
                  [FromBody]
                  [ModelBinder(BinderType = typeof(ActionInputModelBinder))] 
                  ExecuteActionInputModel model)
{
    return await _service.ExecuteActionAsync(id, model);
}

基类输入模型

public class ExecuteActionInputModel
{
    [Required]
    public required MyActionEnum ActionName { get; set; }
}

自定义模型绑定器

public class ActionInputModelBinder : IModelBinder
{
    private readonly IDictionary<Type, (ModelMetadata, IModelBinder)> _propertyBinders;

    internal ActionInputModelBinder(IDictionary<Type, (ModelMetadata, IModelBinder)> propertyBinders)
    {
        _propertyBinders = propertyBinders;
    }

    public async Task BindModelAsync(ModelBindingContext modelBindingContext)
    {
        var actionNameValue = modelBindingContext.ValueProvider
                                                 .GetValue(nameof(ExecuteActionInputModel.ActionName)).FirstValue;

        if (actionNameValue == null)
        {
            modelBindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var actionName = Enum.Parse<RequestActionEnum>(actionNameValue);
    }
}

原因分析

[FromBody]依赖**格式化程序(如JSON/XML解析器)**处理请求体内容,而ValueProvider仅负责读取表单数据、查询字符串、路由参数这类键值对类型的输入。当使用[FromBody]时,请求体流已被格式化程序读取解析,不会再填充到ValueProvider中,因此直接从ValueProvider取值会返回null。

解决方案

方案1:修改模型绑定器,直接读取请求体解析

通过重新读取请求体原始流,解析出ActionName字段后,再绑定对应具体模型:

public async Task BindModelAsync(ModelBindingContext modelBindingContext)
{
    // 允许重新读取请求体流
    modelBindingContext.HttpContext.Request.EnableBuffering();
    using var reader = new StreamReader(modelBindingContext.HttpContext.Request.Body, leaveOpen: true);
    var requestBody = await reader.ReadToEndAsync();
    modelBindingContext.HttpContext.Request.Body.Position = 0; // 重置流位置,避免后续流程无法读取

    // 解析ActionName(以JSON为例)
    var jsonDoc = JsonDocument.Parse(requestBody);
    if (!jsonDoc.RootElement.TryGetProperty(nameof(ExecuteActionInputModel.ActionName), out var actionNameElement))
    {
        modelBindingContext.Result = ModelBindingResult.Failed();
        return;
    }

    if (!Enum.TryParse<MyActionEnum>(actionNameElement.GetString(), out var actionName))
    {
        modelBindingContext.ModelState.TryAddModelError(nameof(ExecuteActionInputModel.ActionName), "无效的操作类型");
        modelBindingContext.Result = ModelBindingResult.Failed();
        return;
    }

    // 根据ActionName获取对应具体模型类型
    var targetType = GetTargetModelType(actionName);
    if (targetType == null)
    {
        modelBindingContext.ModelState.TryAddModelError(nameof(ExecuteActionInputModel.ActionName), "不支持的操作类型");
        modelBindingContext.Result = ModelBindingResult.Failed();
        return;
    }

    // 使用内置绑定器绑定具体模型
    var (metadata, binder) = _propertyBinders[targetType];
    var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
        modelBindingContext.ActionContext,
        modelBindingContext.ValueProvider,
        metadata,
        bindingInfo: null,
        modelName: modelBindingContext.ModelName);

    await binder.BindModelAsync(newBindingContext);
    modelBindingContext.Result = newBindingContext.Result.IsModelSet 
        ? newBindingContext.Result 
        : ModelBindingResult.Failed();
}

// 自定义枚举到模型类型的映射逻辑
private Type GetTargetModelType(MyActionEnum actionName)
{
    return actionName switch
    {
        MyActionEnum.DoTask => typeof(DoTaskInputModel),
        MyActionEnum.UploadFile => typeof(UploadFileInputModel),
        _ => null
    };
}

方案2:实现自定义InputFormatter(更推荐)

[FromBody]原生依赖InputFormatter,自定义格式化器是处理请求体多态绑定的标准方式:

1. 创建自定义格式化器

public class ActionInputFormatter : JsonInputFormatter
{
    public ActionInputFormatter(JsonOptions options) : base(options.JsonSerializerOptions)
    {
        SupportedMediaTypes.Add("application/json");
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
    {
        var request = context.HttpContext.Request;
        request.EnableBuffering();
        using var reader = new StreamReader(request.Body, leaveOpen: true);
        var bodyContent = await reader.ReadToEndAsync();
        request.Body.Position = 0;

        var jsonDoc = JsonDocument.Parse(bodyContent);
        if (!jsonDoc.RootElement.TryGetProperty(nameof(ExecuteActionInputModel.ActionName), out var actionNameElement))
        {
            return await InputFormatterResult.FailureAsync();
        }

        if (!Enum.TryParse<MyActionEnum>(actionNameElement.GetString(), out var actionName))
        {
            context.ModelState.TryAddModelError(nameof(ExecuteActionInputModel.ActionName), "无效的操作类型");
            return await InputFormatterResult.FailureAsync(context.ModelState);
        }

        var targetType = GetTargetModelType(actionName);
        if (targetType == null)
        {
            context.ModelState.TryAddModelError(nameof(ExecuteActionInputModel.ActionName), "不支持的操作类型");
            return await InputFormatterResult.FailureAsync(context.ModelState);
        }

        var model = await JsonSerializer.DeserializeAsync(request.Body, targetType, JsonSerializerOptions);
        return await InputFormatterResult.SuccessAsync(model);
    }

    private Type GetTargetModelType(MyActionEnum actionName)
    {
        return actionName switch
        {
            MyActionEnum.DoTask => typeof(DoTaskInputModel),
            MyActionEnum.UploadFile => typeof(UploadFileInputModel),
            _ => null
        };
    }
}

2. 注册格式化器

在Program.cs中添加注册逻辑:

builder.Services.AddControllers(options =>
{
    // 将自定义格式化器插入到内置JSON格式化器之前
    options.InputFormatters.Insert(0, new ActionInputFormatter(builder.Services.GetRequiredService<IOptions<JsonOptions>>().Value));
});

3. 简化控制器端点

无需再指定自定义模型绑定器,直接使用[FromBody]:

[HttpPost("{id}/execute-action")]
public async Task<IActionResult> ExecuteAction([FromRoute] Guid id, [FromBody] ExecuteActionInputModel model)
{
    return await _service.ExecuteActionAsync(id, model);
}

关键注意事项

  • 必须调用EnableBuffering()允许重新读取请求体流,默认请求体流仅能读取一次
  • 若使用Newtonsoft.Json,将上述System.Text.Json相关代码替换为Newtonsoft的API即可
  • 自定义InputFormatter是处理[FromBody]多态绑定的官方推荐方案,适配性更强

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 10:35:56