ASP.NET Core Web API - Clean Architecture - III


 Hoje vamos criar um projeto ASP.NET Core Web API aplicando o padrão de arquitetura Clean Architecture.

Continuando o artigo anterior vamos implementar a camada de apresentação no projeto WebAPI da pasta Presentation.

Camada de Apresentação

A camada de apresentação é um componente da Clean Architecture responsável por enviar respostas e receber solicitações do usuário. É a camada mais externa de um aplicativo e é a camada que interage diretamente com o usuário final. É nesta camada onde residem os componentes responsáveis pela interação com o usuário, como interfaces gráficas, APIs, controladores web, etc. Essa camada deve ser minimalista e apenas traduzir as ações do usuário para chamadas nos casos de uso.

A camada de apresentação pode ser implementada usando várias tecnologias, como Web API, gRPC, Web Application, Blazor, React, Vue, etc .

A camada de apresentação não deve conter lógica de negócios ou conhecimento de domínio e deve apenas interagir com o restante do aplicativo por meio da camada de aplicativo. O objetivo dessa camada é apresentar a funcionalidade do aplicativo ao usuário e receber entrada do usuário sem estar vinculado a nenhum detalhe ou tecnologia de implementação específica.

A camada de apresentação contém o projeto WebAPI que fornece uma maneira de criar e expor serviços da Web RESTful. O projeto WebAPI deve fazer referência ao projeto Application e ao projeto Persistence.

Vamos iniciar apresentando o controlador UsersController da pasta Controllers:

using CleanArchitecture.Application.UseCases.Features.CreateUser;
using
CleanArchitecture.Application.UseCases.Features.GetAllUser;
using
MediatR;
using
Microsoft.AspNetCore.Mvc;

namespace CleanArchitecture.WebAPI.Controllers;

[Route("api/[controller]")]
[ApiController]

public
class UsersController : ControllerBase
{
 
private readonly IMediator _mediator;
 
public UsersController(IMediator mediator)
  {
    _mediator = mediator;
  }

  [HttpGet]
 
public async Task<ActionResult<List<GetAllUserResponse>>> GetAll(CancellationToken cancellationToken)
  {
   
var response = await _mediator.Send(new GetAllUserRequest(), cancellationToken);
   
return Ok(response);
  }

  [HttpPost]
 
public async Task<ActionResult<CreateUserResponse>> Create(CreateUserRequest request,
                                                    CancellationToken cancellationToken)
  {
    
var response = await _mediator.Send(request, cancellationToken);
    
return Ok(response);
   }
}
    

Neste controlador temos os atributos [Route] e [ApiController] que são usados para definir a rota base da API para esse controlador. [controller] é uma convenção que será substituída pelo nome do controlador, no caso, "UsersController". O atributo [ApiController] indica que o controlador deve ter comportamentos específicos para APIs, como validação automática de modelo e outras características.

A classe UsersController herda da classe ControllerBase, que é a classe base para controladores em ASP.NET Core. Ela fornece funcionalidades relacionadas à criação de APIs.

O construtor do controlador recebe uma instância de IMediator. Isso indica que a classe está usando o padrão Mediator para lidar com as operações de solicitação e resposta. O Mediator é uma parte do padrão CQRS (Command Query Responsibility Segregation) e é usado para separar as operações de leitura e escrita.

O método GetAll é um endpoint HTTP GET que recupera todos os usuários. Ele chama o Mediator para enviar uma solicitação GetAllUserRequest. Quando a resposta é recebida, ela é retornada como um resultado HTTP 200 OK.

O método Create é um endpoint HTTP POST para criar um novo usuário. Ele recebe uma solicitação CreateUserRequest. A solicitação é enviada para o Mediator, que processa a criação do usuário. Quando a resposta é recebida, ela é retornada como um resultado HTTP 200 OK.

A pasta Extensions contém métodos de extensão para IServiceCollection e IApplicatioinBuilder definidos nas classes estáticas : ApiBehaviorExtensions, CorsPolicyExtensions e ErrorHandlerExtensions :

1- ApiBehaviorExtensions

public static class ApiBehaviorExtensions
{
  
public static void ConfigureApiBehavior(this IServiceCollection services)
   {
     services.Configure<ApiBehaviorOptions>(options => {
                  options.SuppressModelStateInvalidFilter =
true;
       });
   }
}

Esse código define um método de extensão para configurar o comportamento da API em uma aplicação ASP.NET Core.

O método de extensão ConfigureApiBehavior é um método de extensão que adiciona uma configuração específica ao comportamento da API na coleção de serviços (IServiceCollection). Ele aceita um parâmetro services, que é a instância da coleção de serviços onde a configuração será adicionada.

No código do método,  a configuração do comportamento da API é definida usando a classe ApiBehaviorOptions que é configurada por meio do método Configure. O objetivo dessa configuração é suprimir o filtro que lida com os erros de validação de modelo (ModelState) na API. Isso significa que, se um modelo inválido for enviado em uma solicitação, os erros de validação do modelo não serão tratados automaticamente pelo filtro. Essa ação significa que a ASP.NET Core não tratará automaticamente os erros de validação do modelo, e você precisará lidar manualmente com a validação e os erros.

2- CorsPolicyExtensions

public static class CorsPolicyExtensions
{
 
public static void ConfigureCorsPolicy(this IServiceCollection services)
  {
    services.AddCors(opt =>
    {
       opt.AddDefaultPolicy(builder => builder
                   .AllowAnyOrigin()
                   .AllowAnyMethod()
                   .AllowAnyHeader());
       });
    }
}

Esse código define um método de extensão para configurar políticas CORS (Cross-Origin Resource Sharing) em uma aplicação ASP.NET Core.

Esse é um método de extensão que adiciona uma configuração de política CORS à coleção de serviços (IServiceCollection). Ele aceita um parâmetro services, que é a instância da coleção de serviços onde a configuração será adicionada.

Nesse bloco, a configuração da política CORS é definida. A classe CorsOptions é configurada por meio do método AddCors. Isso define uma política CORS padrão que permite qualquer origem (AllowAnyOrigin), qualquer método HTTP (AllowAnyMethod) e qualquer cabeçalho (AllowAnyHeader).

3- ErrorHandlerExtensions

using CleanArchitecture.Application.Shared.Exceptions;
using
Microsoft.AspNetCore.Diagnostics;
using
System.Net;
using
System.Text.Json;

namespace CleanArchitecture.WebAPI.Extensions;

public static class ErrorHandlerExtensions
{
 
public static void UseErrorHandler(this IApplicationBuilder app)
  {
    app.UseExceptionHandler(appError =>
    {
          appError.Run(
async context =>
          {
            
var contextFeature = context.Features.Get<IExceptionHandlerFeature>();

            
if (contextFeature == null) return;

             context.Response.Headers.Add(
"Access-Control-Allow-Origin", "*");

             context.Response.ContentType =
"application/json";

             context.Response.StatusCode = contextFeature.Error
switch
             {
                BadRequestException => (
int)HttpStatusCode.BadRequest,
                OperationCanceledException => (
int)HttpStatusCode.ServiceUnavailable,
                NotFoundException => (
int)HttpStatusCode.NotFound,
               
_ => (int)HttpStatusCode.InternalServerError
             };

             var errorResponse = new
             {
                  statusCode = context.Response.StatusCode,
                  message = contextFeature.Error.GetBaseException().Message
             };

             await context.Response.WriteAsync(JsonSerializer.Serialize(errorResponse));
           });
     });
   }
}


Temos aqui a definição de uma série de extensões para lidar com erros e exceções em uma aplicação ASP.NET Core.

Esse código está importando os namespaces necessários para a implementação das extensões, incluindo namespaces relacionados a exceções, manipulação de erros, comunicação HTTP e serialização JSON.

Esse é um método de extensão que adiciona um manipulador de erro global à pipeline de solicitação. Ele aceita um parâmetro app, que é a instância da classe IApplicationBuilder na qual o manipulador de erro será configurado.

No código temos que a configuração do manipulador de erro global é definida usando o método UseExceptionHandler. Dentro do manipulador, o contexto da solicitação é acessado por meio da variável context.

O context.Features.Get<IExceptionHandlerFeature>() obtém informações sobre a exceção que ocorreu durante o processamento da solicitação.

Depois disso, são feitas as seguintes ações:

  • São adicionados cabeçalhos CORS (Access-Control-Allow-Origin: *) para permitir que qualquer origem acesse o recurso de erro.
  • O tipo de conteúdo da resposta é definido como JSON.
  • O código de status da resposta é definido com base no tipo de exceção que ocorreu.
  • Um objeto de resposta de erro é construído, contendo o código de status e a mensagem da exceção.

Finalmente, a resposta é serializada em JSON e enviada de volta ao cliente.

No arquivo appsettings.json temos a definição da string de conexão para acessar o banco SQLite:

{
 
"ConnectionStrings": {
         
"Sqlite": "Data Source=users.db"
},
"Logging": {
  
"LogLevel": {
    
"Default": "Information",
    
"Microsoft.AspNetCore": "Warning"
   }
  },
"AllowedHosts": "*"
}

Na classe Program temos o registro dos serviços e a configuração do pipeline do request:

using CleanArchitecture.Persistence.Context;
using
CleanArchitecture.WebAPI.Extensions;
using
CleanArchitecture.Application.Services;
using
CleanArchitecture.Persistence;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.ConfigurePersistenceApp(builder.Configuration);
builder.Services.ConfigureApplicationApp();
builder.Services.ConfigureApiBehavior();
builder.Services.ConfigureCorsPolicy();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

var serviceScope = app.Services.CreateScope();
var
dataContext = serviceScope.ServiceProvider.GetService<AppDbContext>();
dataContext?.Database.EnsureCreated();

app.UseSwagger();
app.UseSwaggerUI();
app.UseErrorHandler();
app.UseCors();
app.MapControllers();

app.Run();

Explicando o código acima temos que :

  • ConfigurePersistenceApp e ConfigureApplicationApp configuram a persistência e os serviços da aplicação. Provavelmente, esses métodos de extensão estão configurando injeção de dependência para serviços relacionados à persistência de dados e à lógica da aplicação.
     
  • ConfigureApiBehavior e ConfigureCorsPolicy configuram o comportamento da API e as políticas CORS (Cross-Origin Resource Sharing), respectivamente. Esses métodos de extensão provavelmente estão configurando comportamentos globais da API e as configurações de CORS
     
  • AddControllers adiciona os serviços relacionados a controladores à coleção de serviços.
  • AddEndpointsApiExplorer adiciona a exploração de endpoints da API, que é usado para gerar a documentação.
  • AddSwaggerGen adiciona os serviços para a geração de documentação do Swagger.

A seguir  é criado um escopo de serviço para a aplicação (serviceScope), e em seguida, o contexto do banco de dados (AppDbContext) é obtido do provedor de serviços. É verificado se o contexto existe e, se existir, o método EnsureCreated é chamado para garantir que o banco de dados seja criado se não existir.

  • UseSwagger e UseSwaggerUI configuram o middleware do Swagger, permitindo que você visualize e interaja com a documentação da API.
  • UseErrorHandler configura o middleware para manipular erros e exceções, conforme definido na extensão ErrorHandlerExtensions.
  • UseCors configura o middleware para lidar com as políticas CORS.
  • MapControllers mapeia os endpoints dos controladores.
  • Run inicia a aplicação.

Executando o projeto teremos a apresentação dos dois endpoints criados na interface do Swagger:

Após criar alguns usuários usando o endpoint POST /api/Users podemos obter os dados usando o método GET /api/Users:

Com isso temos uma implementação da Clean Architecture em um projeto Web API de forma objetiva e simples.

Em outro artigo vamos implementar os testes de unidade.

Pegue o código aqui :   CleanArchitecture.zip

"Ouve tu então nos céus, assento da tua habitação, e perdoa, e age, e dá a cada um conforme a todos os seus caminhos, e segundo vires o seu coração, porque só tu conheces o coração de todos os filhos dos homens."
1 Reis 8:39

Referências:


José Carlos Macoratti