Skip to content

Map entire body to model property (rest of the properties bind from route) #64

Description

@Misiu

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions