There are several ways in .NET 5 Web Api to bind request data into a model. Attributes such as [FromBody] or [FromRoute] can be used for this.
[HttpPost("{id}/rooms/{roomId}")]
public ActionResult<FromRouteDto> AddRoom([FromRoute] FromRouteDto fromRouteDto)
{
// the route parameter will be mapped to the dto properties
return Ok(fromRouteDto)
}
public class FromRouteDto
{
public Guid Id { get; set; }
public Guid RoomId { get; set; }
}
Example for [FromRoute] model binding
[HttpPost("login")]
public ActionResult<FromBodyDto> Login([FromBody] FromBodyDto fromBodyDto)
{
// The request content ({ "username": "myusername", "password":
"mypassword" }) will be mapped to the dto
return Ok(fromBodyDto);
}
public class FromBodyDto
{
public string Username { get; set; }
public string Password { get; set; ]
}
Example for [FromBody] model binding
This works fine and is self-explaining. But there are cases where you will want both: A combination of route- and body-binding.
This can for example be the case with a PUT endpoint which will have the id in the route and additional properties in the request body.
Example:
Route: “api/users/{id}” → “api/users/1”
Body: “{ “name”: “John Doe”, “favoriteDish”: “Pizza” }”
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
public string FavoriteDish { get; set; }
}
Expected Model
Of course you could do something like this:
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpPut("{id}")]
public ActionResult<UserDto> Put ([FromRoute] int id, [FromBody] UserDto userDto)
{
userDto.Id=id;
return Ok(userDto);
}
}
This will definitely work, but it has several disadvantages:
You will have to map the route parameter manually
If you use pre-controller model validation (e. g. with FluentValidation), the id will not be validated because it is empty at this point.
The next idea could be to define the source of every single property in the model like this:
public class UserDto
{
[FromRoute]
public int Id { get; set; }
[FromBody]
public string Name { get; set; }
[FromBody]
public string FavoriteDish { get; set; }
}
Unfortunately, this will not work in .NET 5. After some research, I finally found the following working solution:
First you have to add the following line in Startup.cs → ConfigureServices:
services.Configure<ApiBehaviorOptions>(options=>options.SuppressInferBindingSourcesForParameters=true);
Gets or sets a value that determines if model binding sources are inferred for action parameters on controllers annotated with Microsoft.AspNetCore.Mvc.ApiControllerAttribute is suppressed. When enabled, the following sources are inferred: Parameters that appear as route values, are assumed to be bound from the path. Parameters of type Microsoft.AspNetCore.Http.IFormFile and Microsoft.AspNetCore.Http.IFormFileCollection are assumed to be bound from form. Parameters that are complex are assumed to be bound from the body. All other parameters are assumed to be bound from the query.
The next step is to change the model and endpoint implementation a bit like this:
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
[HttpPut("{Id}")]
public ActionResult<UserDto> Put(UserDtouserDto)
{
return Ok(userDto);
}
}
public class UserDto
{
[FromRoute]
public int Id { get; set; }
[FromBody]
public UserBodyDto Body { get; set; }
}
public class UserBodyDto
{
public string Name { get; set; }
public string FavoriteDish { get; set; }
}
See the approach in action:
Passing the id via route and the name and favorite dish properties in the body…
… will be mapped automatically into one combined model
You can find the repository with this example here.
If you have multiple cases where you need this behaviour, consider using a base model class, e. g. like this:
public abstract class BaseModifyDto<T>
{
[FromRoute]
public Guid Id { get; set; }
[FromBody]
public T Body { get; set; }
}
T is the type of the body model
Source: Medium : Mi Ra
The Tech Platform
Comments