问题描述
我正在使用 ASP.NET Core 1.1 MVC 来构建 JSON API.给定以下模型和动作方法:
公共类TestModel{公共 int ID { 获取;放;}[范围(100, 999)]公共 int RootId { 获取;放;}[必需,最大长度(200)]公共字符串名称 { 获取;放;}公共字符串描述 { 获取;放;}}[HttpPost("/test/{rootId}/echo/{id}")]公共 IActionResult TestEcho([FromBody] TestModel 数据){返回Json(新{数据.Id,数据.RootId,数据名称,数据.描述,错误 = ModelState.IsValid ?null : ModelState.SelectMany(x => x.Value.Errors)});}
我的操作方法参数上的 [FromBody]
导致模型从发布到端点的 JSON 有效负载绑定,但它也阻止了 Id
和 RootId
属性被路由参数绑定.
我可以将其分解为单独的模型,一个从路由绑定,一个从正文绑定,或者我也可以强制任何客户端发送 id
&rootId
作为有效负载的一部分,但是这两种解决方案似乎都比我想要的更复杂,并且不允许我将验证逻辑保留在一个地方.有什么方法可以让这种情况正常工作,模型可以正确绑定并且我可以保留我的模型和一起验证逻辑?
您可以删除输入中的 [FromBody]
装饰器,并让 MVC 绑定映射属性:
[HttpPost("/test/{rootId}/echo/{id}")]公共 IActionResult TestEcho(TestModel 数据){返回Json(新{数据.Id,数据.RootId,数据名称,数据.描述,错误 = ModelState.IsValid ?null : ModelState.SelectMany(x => x.Value.Errors)});}
更多信息:
更新 2
@heavyd,你说得对,JSON 数据需要 [FromBody]
属性来绑定你的模型.所以我上面所说的将适用于表单数据,但不适用于 JSON 数据.
作为替代方案,您可以创建一个自定义模型绑定器,该绑定器绑定来自 url 的 Id
和 RootId
属性,同时绑定来自请求正文的其余属性.
公共类 TestModelBinder : IModelBinder{私有 BodyModelBinder defaultBinder;public TestModelBinder(IList formatters, IHttpRequestStreamReaderFactory readerFactory)//: base(formatters, readerFactory){defaultBinder = new BodyModelBinder(formatters, readerFactory);}公共异步任务 BindModelAsync(ModelBindingContext bindingContext){//调用默认的body binder等待 defaultBinder.BindModelAsync(bindingContext);if (bindingContext.Result.IsModelSet){var data = bindingContext.Result.Model as TestModel;如果(数据!= null){var value = bindingContext.ValueProvider.GetValue("Id").FirstValue;int intValue = 0;if (int.TryParse(value, out intValue)){//覆盖 Id 属性数据.Id = intValue;}value = bindingContext.ValueProvider.GetValue("RootId").FirstValue;if (int.TryParse(value, out intValue)){//覆盖 RootId 属性data.RootId = intValue;}bindingContext.Result = ModelBindingResult.Success(data);}}}}
创建一个活页夹提供者:
公共类 TestModelBinderProvider : IModelBinderProvider{私有只读 IList格式化程序;私有只读 IHttpRequestStreamReaderFactory readerFactory;公共 TestModelBinderProvider(IList 格式化程序,IHttpRequestStreamReaderFactory readerFactory){this.formatters = 格式化程序;this.readerFactory = readerFactory;}公共 IModelBinder GetBinder(ModelBinderProviderContext 上下文){if (context.Metadata.ModelType == typeof(TestModel))return new TestModelBinder(formatters, readerFactory);返回空值;}}
并告诉 MVC 使用它:
services.AddMvc().AddMvcOptions(options =>{IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService();options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory));});
那么你的控制器有:
[HttpPost("/test/{rootId}/echo/{id}")]公共 IActionResult TestEcho(TestModel 数据){...}
测试
您可以将 Id
和 RootId
添加到您的 JSON 中,但它们将被忽略,因为我们正在模型绑定器中覆盖它们.
更新 3
以上允许您使用数据模型注释来验证 Id
和 RootId
.但我认为这可能会使其他查看您的 API 代码的开发人员感到困惑.我建议简化 API 签名以接受与 [FromBody]
一起使用的不同模型,并将来自 uri 的其他两个属性分开.
[HttpPost("/test/{rootId}/echo/{id}")]公共 IActionResult TestEcho(int id,int rootId,[FromBody]TestModelNameAndAddress testModelNameAndAddress)
您可以为所有输入编写一个验证器,例如:
//这将返回属性和错误消息的元组列表.var errors = validator.Validate(id, rootId, testModelNameAndAddress);if (errors.Count() > 0){foreach(错误中的var错误){ModelState.AddModelError(error.Property, error.Message);}}
I am using ASP.NET Core 1.1 MVC to build an JSON API. Given the following model and action method:
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)
});
}
The [FromBody]
on my action method parameter is causing the model to be bound from the JSON payload that is posted to the endpoint, however it also prevents the Id
and RootId
properties from being bound via the route parameters.
I could break this up into to separate models, one bound from the route and one from the body or I could also force any clients to send the id
& rootId
as part of the payload, but both of those solutions seem to complicate things more than I'd like and don't allow me to keep the validation logic in a single place. Is there any way to get this situation working where the model can be bound properly and I can keep my model & validation logic together?
You can remove the [FromBody]
decorator on your input and let MVC binding map the properties:
[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)
});
}
More info: Model binding in ASP.NET Core MVC
UPDATE
Testing
UPDATE 2
@heavyd, you are right in that JSON data requires [FromBody]
attribute to bind your model. So what I said above will work on form data but not with JSON data.
As alternative, you can create a custom model binder that binds the Id
and RootId
properties from the url, whilst it binds the rest of the properties from the request body.
public class TestModelBinder : IModelBinder
{
private BodyModelBinder defaultBinder;
public TestModelBinder(IList<IInputFormatter> 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);
}
}
}
}
Create a binder provider:
public class TestModelBinderProvider : IModelBinderProvider
{
private readonly IList<IInputFormatter> formatters;
private readonly IHttpRequestStreamReaderFactory readerFactory;
public TestModelBinderProvider(IList<IInputFormatter> 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;
}
}
And tell MVC to use it:
services.AddMvc()
.AddMvcOptions(options =>
{
IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory));
});
Then your controller has:
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{...}
Testing
You can add an Id
and RootId
to your JSON but they will be ignored as we are overwriting them in our model binder.
UPDATE 3
The above allows you to use your data model annotations for validating Id
and RootId
. But I think it may confuse other developers who would look at your API code. I would suggest to just simplify the API signature to accept a different model to use with [FromBody]
and separate the other two properties that come from the uri.
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(int id, int rootId, [FromBody]TestModelNameAndAddress testModelNameAndAddress)
And you could just write a validator for all your input, like:
// 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);
}
}
这篇关于ASP.NET Core MVC 混合路由/FromBody 模型绑定&验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!