Archives 06/11/2022

Attribute routing – Model-View-Controller

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 AttributeHTTP MethodURL
HttpGetGETempty/customers
HttpGet(“{id}”)GETempty/customers/{id}
HttpPostPOSTempty/customers
HttpPut(“{id}”)PUTempty/customers/{id}
HttpDelete(“{id}”)DELETEempty/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:

AttributeDescription
FromBodyBinds the JSON body of the request to the parameter’s type.
FromFormBinds the form value that matches the name of the parameter.
FromHeaderBinds the HTTP header value that matches the name of the parameter.
FromQueryBinds the query string value that matches the name of the parameter.
FromRouteBinds the route value that matches the name of the parameter.
FromServicesInject 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):

URLAction/Method
GET empty/customersGetAllAsync()
GET empty/customers/{id}GetOneAsync(int id)
POST empty/customersPostAsync([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.