Custom model binder not firing for a single property in ASP.NET Core 2
Asked Answered
U

1

6

I already have tried this, but I don't think it's my case. This doesn't work neither.

I'm using ASP.NET Core 2 Web API. I just created a dummy model binder (what it does doesn't matter for now):

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

        var modelName = bindingContext.ModelName;

        return Task.CompletedTask;
    }
}

Now, I have a model. This one:

public class UserRegistrationInfo
{
    public string Email { get; set; }

    [ModelBinder(BinderType = typeof(SanitizeModelBinder))]
    public string Password { get; set; }
}

And an action method:

[AllowAnonymous]
[HttpPost("register")]
public async Task<IActionResult> RegisterAsync([FromBody] UserRegistrationInfo registrationInfo)
{
    var validationResult = validateEmailPassword(registrationInfo.Email, registrationInfo.Password);
    if (validationResult != null) 
    {
        return validationResult;
    }

    var user = await _authenticationService.RegisterAsync(registrationInfo.Email, registrationInfo.Password);

    if (user == null)
    {
        return StatusCode(StatusCodes.Status500InternalServerError, "Couldn't save the user.");
    }
    else
    {
        return Ok(user);
    }
}

If I make a post request from the client, my custom model binder isn't fired and the execution continues in the action method.

Things I have tried:

Applying the ModelBinder attribute to the whole model object:

[ModelBinder(BinderType = typeof(SanitizeModelBinder))]
public class UserRegistrationInfo
{
    public string Email { get; set; }
    public string Password { get; set; }
}

This works, but for the whole object, and I don't want that. I want the default model binder to do its job and then, apply my custom model binder to certain properties only.

I read here that it's the FromBody 's fault, so I removed it from the action method. It doesn't work neither.

I tried to change the attribute ModelBinder for BindProperty here:

public class UserRegistrationInfo
{
    public string Email { get; set; }

    [BindProperty(BinderType = typeof(SanitizeModelBinder))]
    public string Password { get; set; }
}

But it doesn't work.

It's quite disapointing that something that should be simple is turning to be quite cumbersome, and the information scattered on several blogs and github issues isn't helping at all. So please, if you can help me, it would be really appreciated.

Undress answered 13/4, 2019 at 19:52 Comment(9)
Interesting, maybe you've just found a bug. Your code looks correctly (learn.microsoft.com/en-us/aspnet/core/mvc/advanced/…). Might be better to create an issue in MVC repo. You can also try this with newer versions just to be sure.Chalco
What happens when you remove [FromBody]? By doing this, you're really switching from a JSON body to an application/x-www-form-urlencoded body so are you also changing the format of the body when you remove this?Cradle
@KirkLarkin No, I just removed the [FromBody] and set a breakpoint inside the action method. The action method gets executed, but not the custom model binder.Undress
So when you did that, did Email and Password have a value? I'm wondering if that change actually just means nothing gets bound at all as you're sending JSON where application/x-www-form-urlencoded is expected (assuming you are working with JSON).Cradle
@KirkLarkin I just tried what you have said. It's an Angular app, and I'm sending JSON to the server as payload. If I remove the [FromBody] and I inspect the registrationInfo parameter, it's filled.Undress
Does your controller have the [ApiController] attribute?Cradle
@KirkLarkin Yes, [Authorize], [ApiController] and [Route("api/[controller]")].Undress
That confirms you need a custom JsonConverter and not a custom IModelBinder. You haven't really removed [FromBody] when you say you have, because [ApiController] infers it.Cradle
@KirkLarkin Thanks for your help. Interesting, I didn't know [ApiController] would infer it. I'm going to investigate the JsonConverter approach, but in any case, I think this should be a job for a custom model binder.Undress
C
3

For ModelBinder, you need to use application/x-www-form-urlencoded at client side and [FromForm] at server side.

For ApiController, its default binding is JsonConverter.

Follow steps below:

  1. Change action

    [AllowAnonymous]
    [HttpPost("register")]
    public async Task<IActionResult> RegisterAsync([FromForm]UserRegistrationInfo registrationInfo)
    {
        return Ok(registrationInfo);
    }
    
  2. Angular

    post(url: string, model: any): Observable <any> {
        let formData: FormData = new FormData(); 
        formData.append('id', model.id); 
        formData.append('applicationName', model.applicationName); 
        return this._http.post(url, formData)
            .map((response: Response) => {
                return response;
            }).catch(this.handleError); 
    }
    

For using json with custom binding, you could custom formatters, and refer Custom formatters in ASP.NET Core Web API

Chemotherapy answered 15/4, 2019 at 1:24 Comment(3)
I upvoted your answer because it shows an alternative way of solving the issue (I appreciate it), but my idea is using the same API from a mobile app later, and I don't know if it would be very appropiate to make a mobile app send URL encoded data. Sending the payload as JSON seems more "standard".Undress
@Undress for formdata, it send the data in the request body instead of url encoded data. FormData and Json are two options to bind the data. Both are standard. If you prefer json, you need to custom jsoninput formatter.Chemotherapy
@TaoZhuo I know it sends the data inside the body as URL encoded form data, I meant that. I'm going to try the custom JSON input formatter approach. Please, mention in your answer the possibility of using a custom JSON formatter so other people can know about this and I mark it as the accepted answer.Undress

© 2022 - 2024 — McMap. All rights reserved.