Não é segredo que sou fã de carteirinha de .NET Core para desenvolvimento de APIs. É um framework que permite o desenvolvimento de aplicações poderosas, performantes e que, no geral, facilita e encoraja o uso de boas práticas.
Mo entanto, tem um pormenor que sempre me deixa desconfortável: controllers!
Na maior parte dos projetos, os controllers acabam por ser uma coleção de métodos sem qualquer relação direta, a não ser o relacionamento a uma entidade de negócio. Normalmente basta olhar para a quantidade de dependências injetadas no controller para levantar a suspeita que o S de SOLID foi para o espaço.
Para uma API do tipo:
Não é raro ter um controller com um construtor parecido com:
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
...
public ProductsController(IProductsCrudServices crudService, IProductsSalesReporting salesReporting, IProductsInventoryReports inventoryReports)
{
_crudService = crudService;
_salesReporting = salesReporting;
_inventoryReports = inventoryReports;
}
...
}
Podemos com alguma facilidade imaginar um cenário em que o número de dependências cresce exponencialmente.
Até agora, a minha maneira preferida de resolver o problema é separar as funcionalidades em vários controllers, cada um dependente de um único serviço. Para controlar a geração da definição Open API do Swagger, uso o pacote de anotações do wagger.
dotnet add package Swashbuckle.AspNetCore.Annotations
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Api", Version = "v1" });
c.EnableAnnotations();
});
[ApiController]
[Route("products")]
public class ProductsCrudController : ControllerBase
{
private readonly IProductsCrudServices _crudService;
public ProductsCrudController(IProductsCrudServices crudService) => _crudService = crudService;
[HttpGet]
[SwaggerOperation(Tags = new[] { "products" })]
public async Task<IActionResult> ListAll() { ... }
[HttpPost]
[SwaggerOperation(Tags = new[] { "products" })]
public async Task<IActionResult> Post(Product product) { ... }
}
É uma abordagem que funciona mas que depende muito do cuidado com que é implementado. Nada impede que um desenvolvedor desavisado viole a regra.
Outra abordagem que descobri recentemente é utilizando o pacote APIEndpoints desenvolvido pelo Steve ‘Ardalis’ Smith.
Trata-se de uma biblioteca que facilita a criação de API pela definição de endpoints. A sua utilização possibilita a criação de classes super focadas, que realmente aderem ao SRP (Single Responsability Principle).
dotnet add package Ardalis.ApiEndpoints
Para criar um novo endpoint, basta herdar de BaseEndpoint ou BaseAsyncEndpoint e fazer override de Handle
public class TopSalesReportEndpoint : BaseAsyncEndpoint.WithoutRequest.WithResponse<SalesReportResponse>
{
private readonly IProductsSalesReporting _service;
public TopSalesReportEndpoint(IProductsSalesReporting service) => _service = service;
[HttpGet("products/reports/topsales")]
[SwaggerOperation(Tags = new[] { "products" })]
public override Task<ActionResult<SalesReportResponse>> HandleAsync(CancellationToken cancellationToken = default)
{
...
}
}
Como anteriormente, usamos as anotações do Swagger para gerar a documentação Open API.
O que mais me agrada nesta solução é a possibilidade de standardizar a criação de APIs, sem a possibilidade de inflar a quantidade de dependências, ficando com uma série de classes pequenas e fáceis de testar.