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


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

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