.NET Core Web API如何将JSON请求体属性映射为带验证的动态Action方法参数
这个需求太合理了——谁也不想为几十个相似的JSON结构维护一堆重复的模型类!下面我给你一个完整的解决方案,让你不用创建任何模型类,直接在Action的参数上绑定JSON请求体里的属性,还能正常使用[Required]、[Range]这类数据注解验证规则。
核心思路
默认情况下,.NET Core的Action简单类型参数(比如int?、bool)会从URL的Query或Route中取值,而JSON请求体需要用[FromBody]绑定,但[FromBody]只能绑定到一个参数上(因为请求体只能读取一次)。我们需要实现一个自定义模型绑定器,让多个参数可以从同一个JSON请求体中提取对应属性,同时保留数据注解的验证能力。
实现步骤
1. 创建自定义绑定属性
首先定义一个标记属性,用来标识哪些参数需要从JSON请求体中绑定:
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] public class FromJsonBodyAttribute : Attribute, IBindingSourceMetadata { public BindingSource BindingSource => JsonBodyBindingSource.Instance; } // 自定义绑定源,用于区分其他绑定方式 public class JsonBodyBindingSource : BindingSource { public static readonly JsonBodyBindingSource Instance = new(); private JsonBodyBindingSource() : base("JsonBody", "JSON请求体绑定源", true, true) { } public override bool CanAcceptDataFrom(BindingSource bindingSource) { return bindingSource == BodyBindingSource; } }
2. 实现模型绑定器
这个绑定器会读取一次请求体并解析为JObject,然后为每个标记了[FromJsonBody]的参数提取对应属性值:
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; // 需要安装Newtonsoft.Json包,或者用System.Text.Json的JsonDocument public class JsonBodyModelBinder : IModelBinder { private readonly IOptions<MvcNewtonsoftJsonOptions> _jsonOptions; private readonly ILogger<JsonBodyModelBinder> _logger; public JsonBodyModelBinder(IOptions<MvcNewtonsoftJsonOptions> jsonOptions, ILogger<JsonBodyModelBinder> logger) { _jsonOptions = jsonOptions; _logger = logger; } public async Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); var httpContext = bindingContext.HttpContext; JObject payload; // 检查请求体是否已经被读取过,避免重复读取 if (!httpContext.Items.TryGetValue("JsonBodyPayload", out var payloadObj)) { using var reader = new StreamReader(httpContext.Request.Body); var jsonString = await reader.ReadToEndAsync(); if (string.IsNullOrWhiteSpace(jsonString)) { bindingContext.Result = ModelBindingResult.Failed(); return; } payload = JObject.Parse(jsonString); httpContext.Items["JsonBodyPayload"] = payload; } else { payload = (JObject)payloadObj; } var parameterName = bindingContext.FieldName; var token = payload.GetValue(parameterName, StringComparison.OrdinalIgnoreCase); // 处理属性不存在或为null的情况 if (token == null || token.Type == JTokenType.Null) { if (IsNullableType(bindingContext.ModelType)) { bindingContext.Result = ModelBindingResult.Success(null); return; } bindingContext.ModelState.AddModelError(bindingContext.ModelName, $"字段 {parameterName} 是必填项"); bindingContext.Result = ModelBindingResult.Failed(); return; } // 尝试转换为参数类型 try { var value = token.ToObject(bindingContext.ModelType, _jsonOptions.Value.SerializerSettings); bindingContext.Result = ModelBindingResult.Success(value); } catch (Exception ex) { _logger.LogError(ex, "转换JSON属性 {Param} 到类型 {Type} 失败", parameterName, bindingContext.ModelType.Name); bindingContext.ModelState.AddModelError(bindingContext.ModelName, $"字段 {parameterName} 的值不是有效的 {bindingContext.ModelType.Name} 类型"); bindingContext.Result = ModelBindingResult.Failed(); } } // 判断是否是可空值类型 private bool IsNullableType(Type type) { return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } }
注意:如果你用的是.NET 6+默认的
System.Text.Json,可以把JObject替换成JsonDocument来解析,核心逻辑是一样的。
3. 注册模型绑定器提供器
我们需要告诉.NET Core什么时候使用这个绑定器,所以创建一个提供器:
public class JsonBodyModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); // 如果参数标记了FromJsonBodyAttribute,就使用我们的绑定器 if (context.ParameterInfo.GetCustomAttribute<FromJsonBodyAttribute>() != null) { return new BinderTypeModelBinder(typeof(JsonBodyModelBinder)); } return null; } }
然后在Program.cs(.NET 6+)或者Startup.cs中注册这个提供器:
// .NET 6+ Program.cs var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers() .AddNewtonsoftJson(); // 如果用Newtonsoft.Json的话 // 把自定义绑定器提供器加到最前面,确保优先使用 builder.Services.AddControllers(options => { options.ModelBinderProviders.Insert(0, new JsonBodyModelBinderProvider()); }); var app = builder.Build(); // ... 其他中间件配置 app.Run();
4. 在Action中使用
现在你就可以像最开始想的那样,直接在Action参数上标记验证规则了:
[HttpPut("/some-route")] public IActionResult SomeAction( [FromJsonBody, Required, Range(0, 100)] int? foo, [FromJsonBody] byte? bar) { if (!ModelState.IsValid) { return BadRequest(ModelState); } // 你的业务逻辑 return Ok(new { ReceivedFoo = foo, ReceivedBar = bar }); } [HttpPut("/some-other-route")] public IActionResult SomeOtherAction( [FromJsonBody] int? foo, [FromJsonBody, Required] bool baz) { if (!ModelState.IsValid) { return BadRequest(ModelState); } // 你的业务逻辑 return Ok(new { ReceivedFoo = foo, ReceivedBaz = baz }); }
注意事项
- 请求体只会被读取一次,解析后的
JObject会存在HttpContext.Items中,避免重复IO操作。 - 属性匹配是大小写不敏感的,兼容第三方可能的大小写差异。
- 数据注解的验证会自动生效,
ModelState.IsValid会正确反映验证结果。 - 对于可空类型,如果JSON中没有对应属性或值为
null,会绑定为null;非可空类型如果缺少属性会触发必填验证(如果加了[Required])。
替代方案(简化版)
如果你的验证逻辑比较简单,也可以直接用dynamic绑定整个请求体,然后手动验证:
[HttpPut("/simple-route")] public IActionResult SimpleAction([FromBody] dynamic payload) { int? foo = payload.foo; bool? baz = payload.baz; // 手动验证 if (foo == null || foo < 0 || foo > 100) { ModelState.AddModelError("foo", "Foo必须是0-100之间的整数且为必填项"); } if (baz == null) { ModelState.AddModelError("baz", "Baz是必填项"); } if (!ModelState.IsValid) { return BadRequest(ModelState); } // 业务逻辑 return Ok(); }
不过这种方法需要手动写验证代码,不如自定义绑定器的方案优雅,适合简单场景。
内容的提问来源于stack exchange,提问作者user1969903

