Pedro Roque

Pedro Roque

Software Architect

API Endpoints - a (better?) alternative to the traditional controller

April 27, 2021
.NET
api
rest

It's no secret that I'm a big fan of .NET Core for API development. It's a framework that allows the development of powerful, high-performance applications and facilitates and encourages the use of good practices.

However, there is one detail that always makes me uncomfortable: controllers!

In most projects, controllers are a collection of methods without any direct relationship except for the relationship to a business entity. Usually, just looking at the number of dependencies injected into the controller is enough to raise suspicion that the S in SOLID has been lost.

For an API like:

Open API

It's not uncommon to have a controller with a constructor like:

1[ApiController]
2[Route("[controller]")]
3public class ProductsController : ControllerBase
4{
5...        
6    public ProductsController(IProductsCrudServices crudService, IProductsSalesReporting salesReporting, IProductsInventoryReports inventoryReports)
7    {
8        _crudService = crudService;
9        _salesReporting = salesReporting;
10        _inventoryReports = inventoryReports;
11    }
12...
13}

We can easily imagine a scenario where the number of dependencies grows exponentially.

So far, my preferred way to solve this problem is to separate the functionalities into several controllers, each dependent on a single service. I use the Swagger Annotations package to control the generation of the Open API definition.

1dotnet add package Swashbuckle.AspNetCore.Annotations

And annotate the controller methods:

1services.AddSwaggerGen(c =>
2{
3    c.SwaggerDoc("v1", new OpenApiInfo { Title = "Api", Version = "v1" });
4    c.EnableAnnotations();
5});
1[ApiController]
2[Route("products")]
3public class ProductsCrudController : ControllerBase
4{
5    private readonly IProductsCrudServices _crudService;
6    public ProductsCrudController(IProductsCrudServices crudService) => _crudService = crudService;
7
8    [HttpGet]
9    [SwaggerOperation(Tags = new[] { "products" })]
10    public async Task<IActionResult> ListAll() { ... }
11
12    [HttpPost]
13    [SwaggerOperation(Tags = new[] { "products" })]
14    public async Task<IActionResult> Post(Product product) { ... }
15}

It's an approach that works but depends heavily on the care with which it is implemented. Nothing prevents an unaware developer from violating the rule.

Another approach I recently discovered is using the APIEndpoints package developed by Steve 'Ardalis' Smith.

It's a library that facilitates API creation by defining endpoints. Its use creates highly focused classes that adhere to the SRP (Single Responsibility Principle).

1dotnet add package Ardalis.ApiEndpoints

To create a new endpoint, just inherit from BaseEndpoint or BaseAsyncEndpoint and override Handle.

1public class TopSalesReportEndpoint : BaseAsyncEndpoint.WithoutRequest.WithResponse<SalesReportResponse>
2{
3    private readonly IProductsSalesReporting _service;
4    public TopSalesReportEndpoint(IProductsSalesReporting service) => _service = service;
5
6    [HttpGet("products/reports/topsales")]
7    [SwaggerOperation(Tags = new[] { "products" })]
8    public override Task<ActionResult<SalesReportResponse>> HandleAsync(CancellationToken cancellationToken = default)
9    {
10        ...
11    }
12}

As before, we use Swagger annotations to generate the Open API documentation.

What I like most about this solution is the possibility to standardize the creation of APIs without inflating the number of dependencies, resulting in a series of small, easy-to-test classes.