.NET - Apresentando Hexagonal Architecture - II


  Neste artigo vou vou continuar apresentando os fundamentos da arquitetura Hexagonal e mostrar um exemplo prático de uso desta arquitetura na plataforma .NET.

Continuando o artigo anterior veremos agora um exemplo prático de implementação da arquitetura Hexagonal na plataforma .NET.

Apresentação do projeto

Vamos criar um pequeno projeto ASP.NET Core Web API com base nos conceitos da arquitetura hexagonal.  Esta API vai gerenciar usuários a partir da entidade User que contém as propriedades Id nome e email.

Estou usando o mesmo modelo de dominio que foi usado nas semanas anteriores na arquitetura cebola e arquitetura limpa e criando uma api para gerenciar usuários.

O projeto foi criado no VS 2022 preview usando o .NET 8.0 e contém uma solução HexagonalArch
e 5 projetos :

Nesta solução criamos a solution folder Adapters e dentro desta pasta criamos duas solution Folders

  1.  Primary representando os adaptadores primários ou de input -  Aqui criamos o projeto API que representa a nossa Web API;
     
  2. Secondary representando os adapatadores secundários ou output ou saida- Aqui criamos dois projetos Class Library :

Na pasta Core criamos os projetos do tipo Class Library :

1 - Application  -  Nesta camada é onde ficam as implementações, das regras de negócio, de acordo com a interface IUserService.  Na classe UserServiceManager registramos injetamo a Interface “IUserService” a que será usada como porta de entrada para a regra de negócio.

Recebemos, via construtor, todas as dependências com o mundo externo que necessitamos. No caso as operações com o banco de dados e email.

2 - Domain -  Nesta camada é onde ficam a declaração das nossas “Portas”/Interfaces (Adapters) de saída para comunicação com o meio externo, além das entidades que representam o nosso negócio e a “porta de entrada” para a regra de negócio especificamente. Aqui temos a definição das interfaces IEmailService, IUserRepository e IUserService.

A camada de domínio, que contém essas interfaces, não se preocupa com a implementação concreta das funcionalidades, mas sim com os contratos que garantem como essas funcionalidades podem ser usadas. As implementações concretas dessas interfaces ficarão nas camadas de adaptação (por exemplo, Infra.Data e Infra.Email).

Na pasta Primary esta o projeto que vai desencadear alguma ação : Neste caso, o projeto API, onde no controlador temos o acionamento da aplicação via método Action e uso da interface de serviço

Na pasta Secondary temos a o projeto que vai definir a saida  definindo as implementações do repositorio, do serviço de mail

Note que em cada projeto definimos as dependências com métodos de extensão que serão carregados na classe Program do projeto API

Na classe Program defini um método DataInit para incluir dois usuários quando da carga da aplicação
:

void DataInit(IApplicationBuilder app)
{
   using (var serviceScope = app.ApplicationServices.CreateScope())
   {
     var context = serviceScope.ServiceProvider.GetService<InMemoryContext>();
     context.Database.EnsureCreated();
      if (!context.Users.Any())
      {
         context.Users.AddRange(
          new User(Guid.NewGuid(), "Maria Silva", "mariasilva@email.com"),
          new User(Guid.NewGuid(), "Paulo Santos", "paulosantos@email.com")
      );
      context.SaveChanges();
    }
}

Criamos na pasta Services a classe UserServiceManager que implementa IUserService e define o acesso aos dados através do repositório UserRepository:

using Domain.Adapters;
using Domain.Entities;
using Domain.Ports;
namespace Application.Services;
public class UserServiceManager : IUserService
{
    private readonly IEmailService _emailAdapter;
    private readonly IUserRepository _userRepository;
    public UserServiceManager(IEmailService emailAdapter,
                              IUserRepository userRepository)
    {
        _emailAdapter = emailAdapter;
        _userRepository = userRepository;
    }
    public async Task<IEnumerable<User>> GetAllUsersAsync()
    {
        var users = await _userRepository.GetAll();
        return users;
    }
    public async Task<User> AddNewUserAsync(User user)
    {
        await _userRepository.Insert(user);
        _emailAdapter.SendEmail("macoratti@yahoo.com", 
                      "teste@email.com", "User was included with sucess...", "Added user");
        return user;
    }
    public async Task<User> UpdateUserAsync(User user)
    {
        var userUpdated = await _userRepository.Update(user);
        _emailAdapter.SendEmail("macoratti@yahoo.com", 
                       "teste@email.com", "User was updated with sucess...", "Updated user");
        return userUpdated;
    }
    public async Task<User> DeleteUserAsync(Guid id)
    {
        var userDeleted = await _userRepository.Delete(id);
        _emailAdapter.SendEmail("macoratti@yahoo.com",
                         "teste@email.com", "User was deleted with sucess...", "Deleted user");
        return userDeleted;
    }
}
O código da classe UserRepository é o seguinte:
using Domain.Adapters;
using Domain.Entities;
using Infra.Data.Context;
using Microsoft.EntityFrameworkCore;
namespace Infra.Data.Repositories;
public class UserRepository : IUserRepository
{
    private readonly InMemoryContext _context;
    public UserRepository(InMemoryContext context)
    {
        _context = context;
    }
    public async Task<IEnumerable<User>> GetAll()
    {
        return await _context.Users.ToListAsync();
    }
    public async Task<User> Insert(User user)
    {
        if (user is null)
        {
            throw new ArgumentNullException(nameof(user));
        }
        _context.Users.Add(user);
        await _context.SaveChangesAsync();
        return user;
    }
    public async Task<User> Update(User user)
    {
        _context.Users.Update(user);
        await _context.SaveChangesAsync();
        return user;
    }
    public async Task<User> Delete(Guid id)
    {
        var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == id);
        if (user is null)
        {
            throw new ArgumentNullException(nameof(user));
        }
        _context.Users.Remove(user);
        await _context.SaveChangesAsync();
        return user;
    }
}

E o controlador UsersController usa o serviço IUserService para realizar as operações:

using Domain.Entities;
using Domain.Ports;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;
    public UsersController(IUserService clubService) =>
        _userService = clubService;
    [HttpGet]
    public async Task<IActionResult> GetAllUsers()
    {
        var users = await _userService.GetAllUsersAsync();
        return Ok(users);
    }
    [HttpPost]
    public async Task<IActionResult> RegisterUser(User user)
    {
        await _userService.AddNewUserAsync(user);
        return Ok(user);
    }
    [HttpPut("{id:Guid}")]
    public async Task<IActionResult> UpdateUser(Guid id, User user)
    {
        if (id != user.Id)
        {
            return BadRequest();
        }
        await _userService.UpdateUserAsync(user);
        return Ok(user);
    }
    [HttpDelete("{id:Guid}")]
    public async Task<IActionResult> DeleteUser(Guid id)
    {
        var user = await _userService.DeleteUserAsync(id);
        return Ok(user);
    }
}

Pegue o código do projeto aqui:  https://github.com/macoratti/HexagonalArch

E estamos conversados...

"(Disse Jesus) Quem crê no Filho de Deus, em si mesmo tem o testemunho; quem a Deus não crê mentiroso o fez, porquanto não creu no testemunho que Deus de seu Filho deu"
1 João 5:10

Referências:


José Carlos Macoratti