在.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
相关产品推荐
相关产品推荐

