Map entire body to model property (rest of the properties bind from route)
Misiu opened this issue · 1 comments
I have a REST API that needs to support a dynamic model,
I have this endpoint:
[HttpPost("{entity}")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[AllowAnonymous]
public async Task<object> CreateSingleRecord2([ModelBinder(typeof(CreateSingleRecordBinder))] CreateSingleRecord model)
{
//process model;
}
and this model:
public class CreateSingleRecord : ICommand<object>
{
[FromRoute(Name ="entity")]
public string Entity { get; init; }
[FromBody]
public IDictionary<string, object> Record { get; init; }
}
I'm doing this request:
curl --location --request POST 'https://localhost:7299/api/data/cars' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data-raw '{
"model": 1,
"name": "Micra",
"id":"a47d52de-fcd1-48e7-8656-7edb84dc78bd",
"is_created": true,
"date":"2022-09-23",
"datetime":"2022-09-23 13:10"
}'
Right now I'm using this binder:
public class CreateSingleRecordBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
if (!bindingContext.ActionContext.RouteData.Values.ContainsKey("entity"))
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
var entityName = bindingContext.ActionContext.RouteData.Values["entity"]?.ToString();
if (string.IsNullOrWhiteSpace(entityName))
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
using var reader = new StreamReader(bindingContext.HttpContext.Request.Body);
var body = await reader.ReadToEndAsync();
var data = System.Text.Json.JsonSerializer.Deserialize<IDictionary<string, object>>(body);
if (data == null || !data.Any())
{
//return failed result
bindingContext.Result = ModelBindingResult.Failed();
return;
}
var model = new CreateSingleRecord
{
Entity = entityName,
Record = data
};
bindingContext.Result = ModelBindingResult.Success(model);
}
}
I'd like to avoid writing a custom binder because the same request binds to:
[HttpPost("{entity}")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[AllowAnonymous]
public async Task<object> CreateSingleRecord([FromRoute] string entity, [FromBody] IDictionary<string, object> model)
{
//process model
}
Sadly when I try to use FromHybrid
I'm getting this error:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-5975e1c9239eebbea90add81d3698958-d4ad813d42423c4a-00",
"errors": {
"Record": [
"The Record field is required."
]
}
}
How can I use Your binder to bind entire body of request to Property of a model.
Can I use FromBody or do I have to create another binder, for example FromEntireBody
?
I've created a question on Stack Overflow: https://stackoverflow.com/questions/73827565/custom-model-binder-with-idictionarystring-object/73854657?noredirect=1#73854657 and this currently works:
[HttpPost("{entity}")]
public async Task<object> CreateSingleRecord([FromRoute] CreateSingleRecord model)
{
//process
}
Notice [FromRoute]
attribute.
Ideally, I'd like to add more properties to my models, two from headers and one from claims, so my question and my problem are still actual - how to make this work with your package.
As a reference please take a look at this model:
public class CreateSingleRecord
{
[HybridBindProperty(Source.Claim, "UserId")]
public int UserId { get; set; }
[HybridBindProperty(Source.Header, "ServerName")]
public string Name { get; set; }
[HybridBindProperty(Source.Route)]
public string Entity { get; set; }
[HybridBindProperty(Source.Body)]
public IDictionary<string, object> Record { get; set; }
}