ASP.NET
Core Web API - Clean Architecture - I
![]() |
Hoje vamos criar um projeto ASP.NET Core Web API aplicando o padrão de arquitetura Clean Architecture. |
A ideia por trás da Clean Architecture é criar um design que minimize as dependências externas e facilite a evolução do sistema ao longo do tempo. Ela enfatiza a separação de preocupações em várias camadas, cada uma com um propósito específico, e estabelece uma hierarquia de dependências que mantém o núcleo da aplicação independente de detalhes de implementação.
Vamos mostrar a implementação da Clean Architecture em um projeto ASP.NET Core Web API que vai gerenciar informações sobre usuários definidos pela entidade User que vai conter as propriedades Email e Name. (Estamos simplificando o modelo de domínio)
Para aplicar a Arquitetura Limpa, podemos dividir a aplicação em quatro camadas:
A estrutura do projeto criado é mostrado a seguir:
Podemos identificar neste projeto os seguintes componentes:
Camada de Domínio
A camada de domínio é
um componente central da Clean Architecture, representando a lógica e as
entidades de negócios de um aplicativo. Ele contém todas as regras de negócios e
conhecimento do aplicativo e deve ser independente de quaisquer detalhes ou
tecnologias específicas de implementação. A camada de domínio define as
entidades, objetos de valor, serviços e regras de negócios que compõem o núcleo
do aplicativo.
O objetivo da camada de domínio é encapsular o conhecimento do aplicativo de uma
forma que seja facilmente testável, reutilizável e independente de qualquer
infraestrutura ou tecnologia específica. Essa camada não deve depender de
componentes externos, como bancos de dados ou APIs, devendo apenas interagir com
eles por meio de abstrações. Manter a camada de domínio livre de problemas de
infraestrutura facilita a alteração ou substituição de componentes de
infraestrutura sem afetar a lógica de negócios do aplicativo.
A camada de domínio - Domain - é um projeto
Class Library localizado dentro da pasta Core.
e não tem referência à outra camada.
Vamos criar no projeto Domain a pasta Entities contendo a classe User e a pasta Common contendo a classe BaseEntity:
1- User
using
CleanArchitecture.Domain.Common; namespace CleanArchitecture.Domain.Entities;public sealed class User : BaseEntity{ public string? Email { get; private set; } public string? Name { get; private set; } } |
2- BaseEntity
namespace
CleanArchitecture.Domain.Common; public abstract class BaseEntity{ public Guid Id { get; set; } public DateTimeOffset DateCreated { get; set; } public DateTimeOffset? DateUpdated { get; set; } public DateTimeOffset? DateDeleted { get; set; } } |
A seguir vamos criar a pasta Interfaces na camada Domain e nesta pasta vamos criar as interfaces IUserRepository, IBaseRepository e IUnitOfWork.
A definição da interface IUserRepository deve estar na camada Domain pois o repositório é um conceito relacionado à persistência de dados e está mais ligado à estrutura das entidades do domínio do que às operações de aplicação em si.
using
CleanArchitecture.Domain.Entities; namespace CleanArchitecture.Domain.Interfaces;public interface IUserRepository : IBaseRepository<User>{ Task<User> GetByEmail(string email, CancellationToken cancellationToken); } |
A definição da interface IBaseRepository também deve estar na camada Domain pois essa interface é uma abstração genérica para operações de CRUD básicas e, portanto, está mais relacionada ao domínio do que às operações específicas de aplicação.
using
CleanArchitecture.Domain.Common; namespace CleanArchitecture.Domain.Interfaces;public interface IBaseRepository<T> where T : BaseEntity{ void Create(T entity); void Update(T entity); void Delete(T entity); Task<T> Get(Guid id, CancellationToken cancellationToken); Task<List<T>> GetAll(CancellationToken cancellationToken); } |
A definição da interface IUnitOfWork deve estar na camada Domain. O padrão Unit of Work é um conceito que está mais relacionado à coordenação das operações de persistência e transações, o que se encaixa bem na camada Domain.
namespace
CleanArchitecture.Domain.Interfaces; public interface IUnitOfWork{ Task Commit(CancellationToken cancellationToken); } |
Poderíamos também ter criado estas interfaces na camada Application dependendo dos requisitos e do cenário mas de maneira geral a definição das interfaces é feita no projeto Domain.
Camada de aplicação
A camada de aplicação é um componente da Clean Architecture que atua como uma
ponte entre a camada de domínio e as interfaces externas de uma aplicação, como
a camada de apresentação ou camada de acesso a dados. Esta camada coordena as
interações entre a camada de domínio e os componentes externos e transforma os
dados entre as diferentes camadas.
A camada de aplicativo contém serviços de aplicativo e classes que contêm a
lógica de negócios do aplicativo. Esses serviços interagem com a camada de
domínio para executar tarefas como criar ou atualizar entidades ou invocar
serviços de domínio. A camada de aplicação também atua como um intermediário
entre a camada de domínio e a camada de apresentação ou camada de acesso a
dados, traduzindo objetos de domínio em objetos de apresentação ou objetos de
acesso a dados e vice-versa.
A camada de aplicação não deve conter nenhum código específico de infraestrutura
e não deve depender de nenhuma tecnologia específica ou mecanismo de acesso a
dados. Em vez disso, ele deve usar abstrações e interfaces para interagir com
componentes externos, facilitando a alteração ou substituição sem afetar a
lógica de negócios principal do aplicativo.
A camada de aplicativo é um projeto Class Library localizado dentro da
pasta Core e deve referenciar a camada de domínio -Domain.
Este projeto vai conter os seguintes bibliotecas nuget:
Vamos criar neste projeto as seguintes pastas:
A estrutura é mostrada na figura abaixo:
Na pasta Services definimos o método de extensão ConfigureApplication que estende IServiceCollection :
using
CleanArchitecture.Application.Shared.Behavior; using FluentValidation; using MediatR; using Microsoft.Extensions.DependencyInjection; using System.Reflection; namespace CleanArchitecture.Application.Services;public static class ServiceExtensions{ public static void ConfigureApplication(this IServiceCollection services) { services.AddAutoMapper(Assembly.GetExecutingAssembly()); services.AddMediatR(Assembly.GetExecutingAssembly()); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); } } |
Esse método é usado para configurar diversos serviços e comportamentos relacionados à aplicação, incluindo o uso do MediatR, AutoMapper e FluentValidation. Vou explicar cada parte do código:
A pasta Shared contém as pastas Behavior e Exceptions. Nesta pasta definimos recursos comuns como helpers, exceptions e comportamentos, etc.
1- Behavior/ValidationBehavior
using
CleanArchitecture.Application.Shared.Exceptions; using FluentValidation; using MediatR; namespace CleanArchitecture.Application.Shared.Behavior;public sealed class ValidationBehavior<TRequest, TResponse> :IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IEnumerable<IValidator<TRequest>> _validators; public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) { _validators = validators; } public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { if (!_validators.Any()) return await next(); var context = new ValidationContext<TRequest>(request); var errors = _validators .Select(x => x.Validate(context)) .SelectMany(x => x.Errors) .Where(x => x != null) .Select(x => x.ErrorMessage) .Distinct() .ToArray(); if (errors.Any()) throw new BadRequestException(errors); return await next(); } } |
Essa classe é um comportamento (behavior) personalizado do MediatR, um padrão que permite a execução de lógica antes ou depois do processamento da solicitação por um manipulador. No caso, o ValidationBehavior é responsável por executar a validação dos dados de entrada de uma solicitação antes de passá-la para o manipulador apropriado.
Vamos entender o código:
2- Exceptions/BadRequestException
public
class
BadRequestException :
Exception { public BadRequestException(string message) : base(message) {} public BadRequestException(string[] errors) : base("Multiple errors occurred. See error details.") { Errors = errors; } public string[]? Errors { get; set; } } |
3- Exceptions/NotFoundException
public
class
NotFoundException : Exception { public NotFoundException(string message) : base(message) {} } |
Na pasta UseCases/Features/CreateUser temos a implementação dos casos de uso para criar usuário e obter todos os usuários usando a biblioteca MediatR para implementar um manipulador de solicitações (handler) que lida com a criação de um usuário.
A seguir o código definido nestas classes:
1- CreateUserHandler
using
AutoMapper; using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Interfaces; using MediatR; namespace CleanArchitecture.Application.UseCases.Features.CreateUser;public sealed class CreateUserHandler : IRequestHandler<CreateUserRequest, CreateUserResponse>{ private readonly IUnitOfWork _unitOfWork; private readonly IUserRepository _userRepository; private readonly IMapper _mapper; public CreateUserHandler(IUnitOfWork unitOfWork, IUserRepository userRepository, IMapper mapper) { _unitOfWork = unitOfWork; _userRepository = userRepository; _mapper = mapper; } public async Task<CreateUserResponse> Handle(CreateUserRequest request, CancellationToken cancellationToken) { var user = _mapper.Map<User>(request); _userRepository.Create(user); await _unitOfWork.Save(cancellationToken); return _mapper.Map<CreateUserResponse>(user); } } |
2- CreateUserMapper
using
AutoMapper; using CleanArchitecture.Domain.Entities; namespace CleanArchitecture.Application.UseCases.Features.CreateUser;public sealed class CreateUserMapper : Profile{ public CreateUserMapper() { CreateMap<CreateUserRequest, User>(); CreateMap<User, CreateUserResponse>(); } } |
3- CreateUserRequest
using
MediatR; namespace CleanArchitecture.Application.UseCases.Features.CreateUser;public sealed record CreateUserRequest(string Email, string Name) : IRequest<CreateUserResponse>; |
4- CreateUserResponse
namespace
CleanArchitecture.Application.UseCases.Features.CreateUser; public sealed record CreateUserResponse{ public Guid Id { get; set; } public string? Email { get; set; } public string? Name { get; set; } } |
5- CreateUserValidator
using
FluentValidation; namespace CleanArchitecture.Application.UseCases.Features.CreateUser;public sealed class CreateUserValidator : AbstractValidator<CreateUserRequest>{ public CreateUserValidator() { RuleFor(x => x.Email).NotEmpty().MaximumLength(50).EmailAddress(); RuleFor(x => x.Name).NotEmpty().MinimumLength(3).MaximumLength(50); } } |
Na pasta UseCases/Features/GetAllUser temos a implementação dos casos de uso que sa a biblioteca MediatR para implementar um manipulador que obtém todos os usuários (getAll) e mapeia esses usuários para uma lista de respostas específicas.
Abaixo temos o código usado para definir estas classes:
1- GetAllUserHandler
using
AutoMapper; using CleanArchitecture.Domain.Interfaces; using MediatR; namespace CleanArchitecture.Application.UseCases.Features.GetAllUser;public sealed class GetAllUserHandler : IRequestHandler<GetAllUserRequest, List<GetAllUserResponse>>{ private readonly IUserRepository _userRepository; private readonly IMapper _mapper; public GetAllUserHandler(IUserRepository userRepository, IMapper mapper) { _userRepository = userRepository; _mapper = mapper; } public async Task<List<GetAllUserResponse>> Handle(GetAllUserRequest request, CancellationToken cancellationToken) { var users = await _userRepository.GetAll(cancellationToken); return _mapper.Map<List<GetAllUserResponse>>(users); } } |
2- GetAllUserMapper
using
AutoMapper; using CleanArchitecture.Domain.Entities; namespace CleanArchitecture.Application.UseCases.Features.GetAllUser;public sealed class GetAllUserMapper : Profile{ public GetAllUserMapper() { CreateMap<User, GetAllUserResponse>(); } } |
3- GetAllUserRequest
using
MediatR; namespace CleanArchitecture.Application.UseCases.Features.GetAllUser;public sealed record GetAllUserRequest : IRequest<List<GetAllUserResponse>>; |
4- GetAllUserResponse
namespace
CleanArchitecture.Application.UseCases.Features.GetAllUser; public sealed record GetAllUserResponse{ public Guid Id { get; set; } public string? Email { get; set; } public string? Name { get; set; } } |
Na próxima parte do artigo vamos implementar os recursos da infraestrutura na pasta Infrastructure.
E estamos conversados...
"Não saia da vossa boca nenhuma palavra torpe, mas só a que for boa para
promover a edificação, para que dê graça aos que a ouvem."
Efésios 4:29
Referências:
C# - Tasks x Threads. Qual a diferença
DateTime - Macoratti.net
Null o que é isso ? - Macoratti.net
Formatação de data e hora para uma cultura ...
C# - Calculando a diferença entre duas datas
NET - Padrão de Projeto - Null Object Pattern
C# - Fundamentos : Definindo DateTime como Null ...
C# - Os tipos Nullable (Tipos Anuláveis)