Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Custom ModelBinder not fired when used with FromBody on a sub property #4553

Closed
sebastieno opened this issue Apr 28, 2016 · 14 comments
Closed

Comments

@sebastieno
Copy link

I have a model that looks like this :

public class RegisteredCreationModel
{
    [Required]
    public string Name { get; set; }
    [ModelBinder(BinderType = typeof(ExtendedValuesModelBinder))]
    public Dictionary<string, string> ExtendedValues { get; set; }
}

In my controller, I add the FromBody attribut to bind the parameter model from the body :

[Route("{eventId:Guid}/registered")]
[HttpPost]
public async Task<IActionResult> CreateRegistered(Guid eventId, [FromBody]RegisteredCreationModel model)

The problem I have is that my custom model binder is not called. If I remove the FromBody attribut, my model binder is called, but each property is empty (bindingContext.ValueProvider). If I move the ModelBinder(BinderType = typeof(ExtendedValuesModelBinder)) attribut to the RegisteredCreationModel class, and leave the FromBody attribut on my action, properties are defined and my model binder is called.

In my case, I really need to set the custom model binder on a property of my model. Did I do something wrong ? If it's an issue, what's the workaround ? Thanks for your help.

@dougbu
Copy link
Member

dougbu commented Apr 28, 2016

[FromBody] ends model binding's involvement and the world centres on the chosen formatter from that point on. Suggest a model more like

public class RegisteredCreationModel
{
    [Required]
    [FromBody]
    public string Name { get; set; }

    [ModelBinder(BinderType = typeof(ExtendedValuesModelBinder))]
    public Dictionary<string, string> ExtendedValues { get; set; }
}

@sebastieno
Copy link
Author

Thx for your reply.
I tried your model but it doesn't work. My model binder is called but bindingContext.ValueProvider is empty (so I can't retrieve any data). The field Name is also null.

@dougbu
Copy link
Member

dougbu commented Apr 28, 2016

Can't see what's going on without more information. Please upload a small repro project (preferably a GitHub repo) and provide a link here.

@sebastieno
Copy link
Author

I just create a repo here : https://github.com/sebastieno/aspnetcore-issue4553

You can post data { 'name': 'sébastien ollivier', 'company':'infinite square'} to http://localhost:5000 and http://localhost:5000/2. They use the same model but the first url has the FromBody attribut, but not the second.

The call to the first url does not trigger my model binder. The call to the second url trigger the model binder but with empty data bindingContext.ValueProvider.

Tell me if you need more informations. Thx.

@dougbu
Copy link
Member

dougbu commented Apr 30, 2016

I wasn't clear, sorry. The original reported issue and the sample repo are working as expected: Model binding isn't involved once the formatter takes over. So ExtendedValuesModelBinder will never be called for a property of a type that is bound [FromBody].

My request for more information was about

I tried your model but it doesn't work. My model binder is called but bindingContext.ValueProvider is empty (so I can't retrieve any data). The field Name is also null.

@sebastieno
Copy link
Author

I add an action to the sample : https://github.com/sebastieno/aspnetcore-issue4553

You can post data { 'name': 'sébastien ollivier', 'company':'infinite square'} to http://localhost:5000/3. The custom model binder will be trigger (and will correctly bind the company value, contrary as I said before) but the from body attribut on the Name property will do nothing (the property stay empty).

@dougbu
Copy link
Member

dougbu commented May 2, 2016

company may be provided in the query string e.g. [http://localhost:5000/3?company=infinite square](http://localhost:5000/3?company=infinite square), as a route value e.g. http://localhost:5000/3/infinite%20square/), or in a form-URL-encoded body. The last option prevents use of the body for anything else, including [FromBody]. Also make sure the content type is application/json or text/json when submitting JSON and using [FromBody].

@sebastieno
Copy link
Author

Indeed, company may be provided by query string or route values, but in my case I want to use the request body.
Actually, I use form-URL-encoded body like you said. So I don't have to use the FromBodyattribute and it works. I will change this to call manually my custom model binder (I want to post JSON).

Do you consider this behavior as an issue ? Do you think that this will be fixed in the RC2 ?

Thx for your time and your help.

@dougbu
Copy link
Member

dougbu commented May 2, 2016

@sebastieno I am having trouble understanding your last response. You said both that you want to post JSON and that you're using a form-URL-encoded body. The two can't both be true.

More generally what should be "fixed" here? The system has a few relevant features but nothing that's behaving unexpectedly in your scenario:

  1. The default value providers get data from a form-URL-encoded body, route values, and the query string.
  2. Input formatters are entirely responsible for creating the parameter or property value associated with [FromBody].
  3. Input formatters are configured to handle only a limited set of content types.
  4. No default input formatter can handle a form-URL-encoded body.

One option may be to write a custom value provider factory that creates a value provider for a JSON request body -- turning that body into name / value pairs. Don't use [FromBody] at all.

@sebastieno
Copy link
Author

Ok. I will try to explain better.

My need is to post JSON data to an action. In that data, I have static fields, like the name property, and dynamic fields, like the company property. Those properties are dynamic because they depend on a client configuration. To handle those dynamic properties, I want to create a model binder (which will rehydrate the property ExtendedValues of my model and update the ModelState). This scenario doesn't work (because of the FromBody attribute).

Actually I post data as form-URL-encoded, but it's a workaround.

I can use another example. Imagine I have a decimal property. How can I do to be able to bind values 3,5 and 3.5 ? In aspnet 4, I would create a model binder and initialize it like ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());. Is that possible in aspnet core ?

@dougbu
Copy link
Member

dougbu commented May 3, 2016

The scenario you're describing wouldn't work in ASP.NET 4 unless the body was form-URL-encoded. The problem here is attempting to mix in JSON and input formatters. Stick w/ a form-URL-encoded body and add a custom value provider or model binder where needed.

@sebastieno
Copy link
Author

sebastieno commented May 4, 2016

I take the time to create my own JsonInputFormatter. I just duplicate the classic JsonInputFormatter and change how the deserialization is done https://github.com/aspnet/mvc/blob/master/src/Microsoft.AspNet.Mvc.Formatters.Json/JsonInputFormatter.cs#L117. Here my implementation :

try
{
    var jsonModel = await jsonReader.ReadToEndAsync();
    model = jsonSerializer.Deserialize(new StringReader(jsonModel), type);

    var extendedValuesModel = model as IExtendedValuesModel;
    if (extendedValuesModel != null)
    {

Do you think is the right place ?

@dougbu
Copy link
Member

dougbu commented May 4, 2016

Since IExtendedValuesModel properties within your model will not be special-cased, I'm a bit surprised this is sufficient. But if this customization gives you the behaviour you want, it seems fine.

@sebastieno
Copy link
Author

Ok. Thanks a lot for your time and your help :)

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

No branches or pull requests

2 participants