简体   繁体   中英

Custom model binder with IDictionary<string, object>

I have a .NET 6 REST API with a method that has two parameters:

public async Task<object> CreateSingleEntity([FromRoute] string entity, [FromBody] IDictionary<string, object> model)
{
    //process data
}

This works well when I do 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": "Ford",
    "id":"a47d52de-fcd1-48e7-8656-7edb84dc78bd",
    "is_created": true,
    "date":"2022-09-23",
    "datetime":"2022-09-23 13:10"
}'

But because I'm using MediatR I'd like to use a model instead.

public class CreateSingleRecord : ICommand<object>
{
    [FromRoute(Name ="entity")]
    public string Entity { get; init; }

    [FromBody]
    public IDictionary<string, object> Record { get; init; }
}

sadly every time I try to replace my previous method with:

public async Task<object> CreateSingleEntity([FromHybrid] CreateSingleRecord model)
{
    //process data
}

I'm getting errors:

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-0b9809f4e2a656dd8b0255940ce84db7-49b9b11c21ce132a-00", "errors": { "Record": [ "The Record field is required." ] } }

I've tried using [FromHybrid] model binder but sadly it isn't working with dictionary types.

The endpoint must handle dynamic objects because the whole system is very dynamic, so I can't bind to predefined models.

I think the only way is to create a model binder, but I have no clue how to deserialize the entire body as a dictionary and assign it to my model's property.

There is another way. If your problem is really just that you want to get the values of the dictionary, then just send your request like this:

curl --location --request POST 'https://localhost:7299/api/data/cars' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data-raw '{ "Record": {
    "model": 1,
    "name": "Ford",
    "id":"a47d52de-fcd1-48e7-8656-7edb84dc78bd",
    "is_created": true,
    "date":"2022-09-23",
    "datetime":"2022-09-23 13:10"
}}'

If you want to get rid of the Record property, then you probably cannot avoid writing a custom binder. Something like this.

[ModelBinder(BinderType = typeof(CreateSingleRecordBnder))]
public class CreateSingleRecord : ICommand<object>
{
    public string Entity { get; init; }

    public IDictionary<string, object> Record { get; init; }
}

Model binder

public class CreateSingleRecordBnder : IModelBinder
{
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var bodyStream = bindingContext.HttpContext.Request.Body;
        using var streamReader = new StreamReader(bodyStream);
        var body = await streamReader.ReadToEndAsync();

        var data = System.Text.Json.JsonSerializer.Deserialize<IDictionary<string, object>>(body);

        var model = new CreateSingleRecord
        {
            Entity = bindingContext.HttpContext.Request.RouteValues["entity"].ToString(),
            Record = data
        };

        bindingContext.Result = ModelBindingResult.Success(model);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM