Search code examples
asp.net-coreasp.net-core-webapiasp.net-core-5.0

Not able to receive values from Route and Body in a POST call


On an Asp.Net Core 5.0 project I have the ApiController route and Model:

[HttpPost("users/{userId:int}/confirm-email")]
public async Task<IActionResult> ConfirmEmail(Model model) { }

public class Model { 
  [FromRoute] public Int32? UserId { get; set; }
  [FromQuery] public String Token { get; set; } 
}

Result: I got what I expected ...

UserId = 10
Token = "ABC"

I tried receive the token in the Body and not in the query parameter:

public class Model { 
  [FromRoute] public Int32? UserId { get; set; }
  [FromBody] public String Token { get; set; } 
}

In this case I got the error:

"errors": {
  "$": [
    "The JSON value could not be converted to System.String. Path: $ | LineNumber: 0 | BytePositionInLine: 1."
  ]
}

I used Insomnia.Rest to call the API and the body is:

{
  "token": "ABC"
}

Then I tried to add [FromBody] to Controller action:

[HttpPost("users/{userId:int}/confirm-email")]
public async Task<IActionResult> ConfirmEmail([FromBody]Model model) { }

public class Model { 
  [FromRoute] public Int32? UserId { get; set; }
  [FromBody] public String Token { get; set; } 
}

Result: I got the token from body but now userId is null.

UserId = null
Token = "ABC" 

I have been trying a few variations but I can't figure out what is happening.

I also tried to call the API from Angular and the result is the same.

Any idea what am I missing?


Solution

  • By default, model binding gets data in the form of key-value pairs from the following sources in an HTTP request:

    • Form fields
    • The request body (For controllers that have the [ApiController] attribute.)
    • Route data
    • Query string parameters
    • Uploaded files

    For each target parameter or property, the sources are scanned in the order indicated in the preceding list.

    You can refer the following screenshot:

    enter image description here

    We can see that, the [FromRoute] and the [FromForm] attribute which apply to the property is not working. It uses the default inference rules to bind model.

    Then, when the [FromBody] attribute is applied to a complex type parameter (add the [FromBody] attribute to Controller action), any binding source attributes applied to its properties are ignored. So, the UserId is null. More detail information, see [FromBody] attribute and Binding source parameter inference.

    So, in this scenario, to access the value from route and request body, the easiest way is adding a parameter in the ConfirmEmail method, code as below:

        [HttpPost("users/{userId:int}/confirm-email")]
        public async Task<IActionResult> ConfirmEmail(Model model, [FromRoute]int userid)
        {
            //set the userid
            model.userId = userid;
    
            return Ok("success");
        }
    

    The result as below:

    enter image description here

    Besides, you can also try to create a Custom model binder to bind the model.

    Here are some relate articles, you can refer them:

    Custom Model Binding in ASP.NET Core

    Custom Model Binding In ASP.NET Core MVC