ASP.NET Core MVC Mixed Route / FromBody Binding & Validation Model

I am using ASP.NET Core 1.1 MVC to create the JSON API. Given the following model and method of action:

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) }); } 

[FromBody] in my action method parameter causes the model to be bound from the JSON payload that is sent to the endpoint, however it also prevents the Id and RootId properties from being bound to the route parameters.

I could break it down into separate models, one associated with the route and one from the body, or I could also force any clients to send Id and RootId as part of the payload, but both of these solutions seem to complicate the situation more than I would like , and they don’t let me maintain the validation logic in one place. Is there a way to make this situation work when the model can be bound properly and I can combine my model and validation logic?

+5
source share
4 answers

You can remove the [FromBody] decorator at your input and bind the MVC binding to 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) }); } 

Additional Information: Model Binding in ASP.NET Core MVC

UPDATE

Testing

enter image description here

enter image description here

UPDATE 2

@heavyd, you are correct that JSON data requires the [FromBody] attribute to bind your model. So, what I said above will work with form data, but not JSON data.

Alternatively, you can create a custom mediation device that binds the Id and RootId to a URL, while it binds the rest of the properties to 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

enter image description here enter image description here

You can add Id and RootId to your JSON, but they will be ignored as we overwrite them in our connecting device.

UPDATE 3

The above allows you to use data model annotations to validate Id and RootId . But I think this may confuse other developers who will look at your API code. I would suggest simply simplifying the signature of the API to accept another model for 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 can just confirm the validator for all your input, for example:

 // 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); } } 
+4
source

After researching, I came up with a solution to create a new connecting source + source attribute + attribute, which combines the functionality of BodyModelBinder and ComplexTypeModelBinder. First, it uses BodyModelBinder to read from the body, and then ComplexModelBinder fills in other fields. The code is here:

 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<IModelBinderProvider> 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); } } 
+6
source

I have not tried this for your example, but it should work as an asp.net support model binding in this way.

You can create such a model.

 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; } } 

Update 1: Above will not work if the thread is not rewound. Mostly in your case when you post json data.

Custom Binder is a solution, but if you still do not want to create it and just want to control it using a model, you can create two models.

 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; } } 

Note. This works fine with application / json bindings, since it works a bit differently than another type of content.

+2
source
  • Install-Package HybridModelBinding

  • Add to Statrup:

     services.AddMvc( opt => { var readerFactory = services.BuildServiceProvider() .GetRequiredService<IHttpRequestStreamReaderFactory>(); opt.ModelBinderProviders.Insert(0, new DefaultHybridModelBinderProvider(opt.InputFormatters, readerFactory)); }); 
  • Model:

     public class Person { public int Age { get; set; } public string Name { get; set; } } 
  • controller:

     [HttpPost] [Route("people/{id}")] public IActionResult Post([FromHybrid]Person model) { } 
  • Request:

     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" 
  • Result:

     { "Id": 123, "Name": "William Boga", "FavoriteColor": "Blue" } 
  • There are other additional features.

0
source

Source: https://habr.com/ru/post/1270556/


All Articles