ASP.NET核心MVC混合路由/ FromBody模型绑定和validation

我正在使用ASP.NET Core 1.1 MVC来构建JSON API。 给出以下模型和动作方法:

public class TestModel { public int Id { get; set; } [Range(100, 999)] public int RootId { get; set; } [Required, MaxLength(200)] public string Name { get; set; } public string Description { get; set; } } [HttpPost("/test/{rootId}/echo/{id}")] public IActionResult TestEcho([FromBody] TestModel data) { return Json(new { data.Id, data.RootId, data.Name, data.Description, Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors) }); } 

我的action方法参数上的[FromBody]导致模型从发布到端点的JSON有效负载绑定,但是它也阻止了IdRootId属性通过路由参数绑定。

我可以将其分解为单独的模型,一个绑定路由,一个绑定到正文,或者我也可以强制任何客户端将idrootId作为有效负载的一部分发送,但这两个解决方案似乎都比我希望并且不允许我将validation逻辑保留在一个地方。 有没有办法让这种情况在模型可以正确绑定的地方工作,我可以将我的模型和validation逻辑保持在一起?

您可以删除输入上的[FromBody]装饰器,让MVC绑定映射属性:

 [HttpPost("/test/{rootId}/echo/{id}")] public IActionResult TestEcho(TestModel data) { return Json(new { data.Id, data.RootId, data.Name, data.Description, Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors) }); } 

更多信息: ASP.NET Core MVC中的模型绑定

UPDATE

测试

在此处输入图像描述

在此处输入图像描述

更新2

@heavyd,你是对的,JSON数据需要[FromBody]属性来绑定你的模型。 所以我上面所说的将用于表单数据,但不用于JSON数据。

或者,您可以创建一个自定义模型绑定器,用于绑定URL中的IdRootId属性,同时绑定请求主体中的其余属性。

 public class TestModelBinder : IModelBinder { private BodyModelBinder defaultBinder; public TestModelBinder(IList formatters, IHttpRequestStreamReaderFactory readerFactory) // : base(formatters, readerFactory) { defaultBinder = new BodyModelBinder(formatters, readerFactory); } public async Task BindModelAsync(ModelBindingContext bindingContext) { // callinng the default body binder await defaultBinder.BindModelAsync(bindingContext); if (bindingContext.Result.IsModelSet) { var data = bindingContext.Result.Model as TestModel; if (data != null) { var value = bindingContext.ValueProvider.GetValue("Id").FirstValue; int intValue = 0; if (int.TryParse(value, out intValue)) { // Override the Id property data.Id = intValue; } value = bindingContext.ValueProvider.GetValue("RootId").FirstValue; if (int.TryParse(value, out intValue)) { // Override the RootId property data.RootId = intValue; } bindingContext.Result = ModelBindingResult.Success(data); } } } } 

创建活页夹提供程序:

 public class TestModelBinderProvider : IModelBinderProvider { private readonly IList formatters; private readonly IHttpRequestStreamReaderFactory readerFactory; public TestModelBinderProvider(IList formatters, IHttpRequestStreamReaderFactory readerFactory) { this.formatters = formatters; this.readerFactory = readerFactory; } public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context.Metadata.ModelType == typeof(TestModel)) return new TestModelBinder(formatters, readerFactory); return null; } } 

并告诉MVC使用它:

 services.AddMvc() .AddMvcOptions(options => { IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService(); options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory)); }); 

然后你的控制器有:

 [HttpPost("/test/{rootId}/echo/{id}")] public IActionResult TestEcho(TestModel data) {...} 

测试

在此处输入图像描述 在此处输入图像描述

您可以向JSON添加IdRootId ,但它们将被忽略,因为我们在模型绑定器中覆盖它们。

更新3

以上允许您使用数据模型注释来validationIdRootId 。 但我认为可能会混淆其他开发人员,他们会查看您的API代码。 我建议只是简化API签名以接受与[FromBody]一起使用的不同模型,并将来自uri的其他两个属性分开。

 [HttpPost("/test/{rootId}/echo/{id}")] public IActionResult TestEcho(int id, int rootId, [FromBody]TestModelNameAndAddress testModelNameAndAddress) 

你可以为你的所有输入找到一个validation器,例如:

 // This would return a list of tuples of property and error message. var errors = validator.Validate(id, rootId, testModelNameAndAddress); if (errors.Count() > 0) { foreach (var error in errors) { ModelState.AddModelError(error.Property, error.Message); } } 

在研究之后,我想出了一个创建新模型绑定器+绑定源+属性的解决方案,它结合了BodyModelBinder和ComplexTypeModelBinder的function。 它首先使用BodyModelBinder从body读取,然后ComplexModelBinder填充其他字段。 代码在这里:

 public class BodyAndRouteBindingSource : BindingSource { public static readonly BindingSource BodyAndRoute = new BodyAndRouteBindingSource( "BodyAndRoute", "BodyAndRoute", true, true ); public BodyAndRouteBindingSource(string id, string displayName, bool isGreedy, bool isFromRequest) : base(id, displayName, isGreedy, isFromRequest) { } public override bool CanAcceptDataFrom(BindingSource bindingSource) { return bindingSource == Body || bindingSource == this; } } 

 [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class FromBodyAndRouteAttribute : Attribute, IBindingSourceMetadata { public BindingSource BindingSource => BodyAndRouteBindingSource.BodyAndRoute; } 

 public class BodyAndRouteModelBinder : IModelBinder { private readonly IModelBinder _bodyBinder; private readonly IModelBinder _complexBinder; public BodyAndRouteModelBinder(IModelBinder bodyBinder, IModelBinder complexBinder) { _bodyBinder = bodyBinder; _complexBinder = complexBinder; } public async Task BindModelAsync(ModelBindingContext bindingContext) { await _bodyBinder.BindModelAsync(bindingContext); if (bindingContext.Result.IsModelSet) { bindingContext.Model = bindingContext.Result.Model; } await _complexBinder.BindModelAsync(bindingContext); } } 

 public class BodyAndRouteModelBinderProvider : IModelBinderProvider { private BodyModelBinderProvider _bodyModelBinderProvider; private ComplexTypeModelBinderProvider _complexTypeModelBinderProvider; public BodyAndRouteModelBinderProvider(BodyModelBinderProvider bodyModelBinderProvider, ComplexTypeModelBinderProvider complexTypeModelBinderProvider) { _bodyModelBinderProvider = bodyModelBinderProvider; _complexTypeModelBinderProvider = complexTypeModelBinderProvider; } public IModelBinder GetBinder(ModelBinderProviderContext context) { var bodyBinder = _bodyModelBinderProvider.GetBinder(context); var complexBinder = _complexTypeModelBinderProvider.GetBinder(context); if (context.BindingInfo.BindingSource != null && context.BindingInfo.BindingSource.CanAcceptDataFrom(BodyAndRouteBindingSource.BodyAndRoute)) { return new BodyAndRouteModelBinder(bodyBinder, complexBinder); } else { return null; } } } 

 public static class BodyAndRouteModelBinderProviderSetup { public static void InsertBodyAndRouteBinding(this IList providers) { var bodyProvider = providers.Single(provider => provider.GetType() == typeof(BodyModelBinderProvider)) as BodyModelBinderProvider; var complexProvider = providers.Single(provider => provider.GetType() == typeof(ComplexTypeModelBinderProvider)) as ComplexTypeModelBinderProvider; var bodyAndRouteProvider = new BodyAndRouteModelBinderProvider(bodyProvider, complexProvider); providers.Insert(0, bodyAndRouteProvider); } } 
  1. 安装包HybridModelBinding

  2. 添加到Statrup:

     services.AddMvc( opt => { var readerFactory = services.BuildServiceProvider() .GetRequiredService(); opt.ModelBinderProviders.Insert(0, new DefaultHybridModelBinderProvider(opt.InputFormatters, readerFactory)); }); 
  3. 模型:

     public class Person { public int Age { get; set; } public string Name { get; set; } } 
  4. 控制器:

     [HttpPost] [Route("people/{id}")] public IActionResult Post([FromHybrid]Person model) { } 
  5. 请求:

     curl -X POST -H "Accept: application/json" -H "Content-Type:application/json" -d '{ "id": 999, "name": "Bill Boga", "favoriteColor": "Blue" }' "https://localhost/people/123/addresses/456?name=William%20Boga" 
  6. 结果:

     { "Id": 123, "Name": "William Boga", "FavoriteColor": "Blue" } 
  7. 还有其他高级function。

我没有尝试过这个例子,但它应该像asp.net核心支持模型这样绑定。

你可以像这样创建模型。

 public class TestModel { [FromRoute] public int Id { get; set; } [FromRoute] [Range(100, 999)] public int RootId { get; set; } [FromBody] [Required, MaxLength(200)] public string Name { get; set; } [FromBody] public string Description { get; set; } } 

更新1:如果流不可重绕,则上述function不起作用。 主要是在你发布json数据的情况下。

自定义模型绑定器是解决方案,但如果您仍然不想创建那个并且只想管理模型,那么您可以创建两个模型。

 public class TestModel { [FromRoute] public int Id { get; set; } [FromRoute] [Range(100, 999)] public int RootId { get; set; } [FromBody] public ChildModel OtherData { get; set; } } public class ChildModel { [Required, MaxLength(200)] public string Name { get; set; } public string Description { get; set; } } 

注意:这与application / json绑定完美配合,因为它的工作方式与其他内容类型不同。