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.

amedina
  • 2,838
  • 3
  • 19
  • 40
  • 1
    Interesting, maybe you've just found a bug. Your code looks correctly (https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-2.2). Might be better to create an issue in MVC repo. You can also try this with newer versions just to be sure. – Konrad Apr 13 '19 at 20:15
  • 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? – Kirk Larkin Apr 13 '19 at 20:31
  • @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. – amedina Apr 13 '19 at 20:35
  • 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). – Kirk Larkin Apr 13 '19 at 20:37
  • @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. – amedina Apr 13 '19 at 20:41
  • Does your controller have the `[ApiController]` attribute? – Kirk Larkin Apr 13 '19 at 20:57
  • @KirkLarkin Yes, `[Authorize]`, `[ApiController]` and `[Route("api/[controller]")]`. – amedina Apr 13 '19 at 20:59
  • 2
    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. – Kirk Larkin Apr 14 '19 at 13:38
  • @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. – amedina Apr 15 '19 at 07:06

1 Answers1

2

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

Edward
  • 28,296
  • 11
  • 76
  • 121
  • 1
    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". – amedina Apr 15 '19 at 07:01
  • 1
    @amedina 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. – Edward Apr 15 '19 at 07:06
  • @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. – amedina Apr 15 '19 at 07:45