Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Misiu opened this issue Sep 27, 2022 · 1 comment
Open

Comments

@Misiu
Copy link
Contributor

Misiu commented Sep 27, 2022

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?

@Misiu
Copy link
Contributor Author

Misiu commented Sep 27, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant