Attribute routing maps an HTTP request to a controller action. Those attributes decorate the controllers and the actions to create the complete routes. We already used some of those attributes. Nonetheless, let’s visit those attributes:
namespace MVC.API.Controllers.Empty;
[Route(“empty/[controller]”)]
[ApiController]
public class CustomersController : ControllerBase
{
[HttpGet]
public Task<IEnumerable<Customer>> GetAllAsync(
ICustomerRepository customerRepository)
=> throw new NotImplementedException();
[HttpGet(“{id}”)]
public Task<ActionResult<Customer>> GetOneAsync(
int id, ICustomerRepository customerRepository)
=> throw new NotImplementedException();
[HttpPost]
public Task<ActionResult> PostAsync(
[FromBody] Customer value, ICustomerRepository customerRepository)
=> throw new NotImplementedException();
[HttpPut(“{id}”)]
public Task<ActionResult<Customer>> PutAsync(
int id, [FromBody] Customer value,
ICustomerRepository customerRepository)
=> throw new NotImplementedException();
[HttpDelete(“{id}”)]
public Task<ActionResult<Customer>> DeleteAsync(
int id, ICustomerRepository customerRepository)
=> throw new NotImplementedException();
}
The Route attributes and Http[Method] attributes define what a user should query to reach a specific resource. The Route attribute allows us to define a routing pattern that applies to all HTTP methods under the decorated controller. The Http[Method] attributes determine the HTTP method used to reach that action method. They also offer the possibility to set an optional and additive route pattern to handle more complex routes, including specifying route parameters. Those attributes are beneficial in crafting concise and clear URLs while keeping the routing system close to the controller. All routes must be unique.Based on the code, [Route(“empty/[controller]”)] means that the actions of this controller are reachable through empty/customers (MVC ignores the Controller suffix). Then, the other attributes tell ASP.NET to map specific requests to specific methods:
Routing Attribute | HTTP Method | URL |
HttpGet | GET | empty/customers |
HttpGet(“{id}”) | GET | empty/customers/{id} |
HttpPost | POST | empty/customers |
HttpPut(“{id}”) | PUT | empty/customers/{id} |
HttpDelete(“{id}”) | DELETE | empty/customers/{id} |
Table 6.3: routing attributes of the example controller and their final URL
As we can see from the preceding table, we can even use the same attribute for multiple actions as long as the URL is unique. In this case, the id parameter is the GET discriminator.Next, we can use the FromBody attribute to tell the model binder to use the HTTP request body to get the value of that parameter. There are many of those attributes; here’s a list:
Attribute | Description |
FromBody | Binds the JSON body of the request to the parameter’s type. |
FromForm | Binds the form value that matches the name of the parameter. |
FromHeader | Binds the HTTP header value that matches the name of the parameter. |
FromQuery | Binds the query string value that matches the name of the parameter. |
FromRoute | Binds the route value that matches the name of the parameter. |
FromServices | Inject the service from the ASP.NET Core dependency container. |
Table 6.4: MVC binding sources
ASP.NET Core MVC does many implicit binding, so you don’t always need to decorate all parameters with an attribute. For example, .NET injects the services we needed in the code samples, and we never used the FromServices attribute. Same with the FromRoute attribute.
Now, if we look back at CustomersController, the route map looks like the following (I excluded non-route-related code to improve readability):
URL | Action/Method |
GET empty/customers | GetAllAsync() |
GET empty/customers/{id} | GetOneAsync(int id) |
POST empty/customers | PostAsync([FromBody] Customer value) |
PUT empty/customers/{id} | PutAsync(int id, [FromBody] Customer value) |
DELETE empty/customers/{id} | DeleteAsync(int id) |
Table 6.5: the map between the URLs and their respective action methods
When designing a REST API, the URL leading to our endpoints should be clear and concise, making it easy for consumers to discover and learn. Hierarchically grouping our resources by responsibility (concern) and creating a cohesive URL space help achieve that goal. Consumers (a.k.a. other developers) should understand the logic behind the endpoints easily. Think about your endpoints as if you were the consumer of the REST API. I would even extend that suggestion to any API; always consider the consumers of your code to create the best possible APIs.
Leave a Reply