ASP.NET Core - Implementando Onion Architecture com CQRS - II


 Hoje vamos continuar realizando a implementação da Onion Architecture em uma aplicação ASP .NET Core aplicando o CQRS.

Continuando a primeira parte do artigo vamos agora implementar a arquitetura Cebola em uma aplicação ASP .NET Core.

Como exemplo vamos criar uma aplicação ASP .NET Core para gerenciar informações de Alunos acessando um banco de dados SQL Server via EF Core onde vamos implementar o padrão CQRS.

Os recursos usados serão os seguintes:

Criando a Solução e os projetos no VS 2019

Vamos iniciar criando uma Blank Solution no VS 2019 com o nome OnionArch;

A seguir crie 3 pastas na solução usando a opção Add-> New Solution Folder;

  1. Core
  2. Infrastructure
  3. Presentation

Agora vamos criar os projetos em cada pasta iniciando com a pasta Core.

Clique com o botão direito na pasta Core e selecione Add -> New Project e selecione Class Library e informe o nome Domain e selecione o framework atual .NET 5.0 :

Este projeto terá as seguintes definições :

Agora vamos repetir este procedimento e criar na pasta Core o projeto Application com as mesmas propriedades e a seguir criar na pasta Infrastructure o projeto Persistence;

Finalmente na pasta Presentation vamos criar um projeto do tipo Web API.

Clique com o botão direito na pasta Presentation e selecione Add -> New Project e selecione ASP.NET Core Web API ;

Informe o nome ApiAlunos e defina as propriedades conforme a figura baixo e clique em Create;

Temos assim a nossa solução contendo os projetos criados usando a disposição recomendada pela Arquitetura Cebola:

Vamos agora definir os recursos em cada projeto da Solução seguindo as recomendações da arquitetura. Vamos iniciar com o projeto Domain da camada Core.

1- Domain

A camada Domain deve ser independente de todas as demais camadas e fatores externos e deve conter as regras do negócio e as entidades usadas para expressar o domínio.

Vamos criar neste projeto as pastas:

Na pasta Validation vamos criar uma classe DomainExceptionValidation  que herda de Exception que iremos usar para validar o domínio:

public class DomainExceptionValidation : Exception
{
        public DomainExceptionValidation(string error) : base(error)
        { }

        public static void When(bool hasError, string error)
        {
            if (hasError)
                throw new DomainExceptionValidation(error);
        }
}

Vamos criar na pasta Entities uma classe abstrata chamada Entity onde vamos definir a propriedade Id que será herdada pela classe Aluno.

public abstract class Entity
{
    public int Id { get; protected set; }
}

Para não complicar muito o exemplo vamos criar uma classe Aluno na pasta Entities e definir apenas 4 propriedades: Nome, Email, Curso e Nota.

Esta classe vai herdar da classe Entity e assim receberá o Id desta classe e iremos realizar a validação desta classe usando a classe DomainExceptionValidation. 

using Domain.Validation;

namespace Domain.Entities
{
    public sealed class Aluno : Entity
    {
        public string Nome { get; private set; }
        public string Email { get; private set; }
        public string Curso { get; private set; }
        public int Nota { get; private set; }


        public Aluno(int id, string nome, string email, string curso , int nota)
        {
            DomainExceptionValidation.When(id < 0, "Id inválido.");
            Id = id;
            ValidarDomain(nome, email, curso, nota);
        }

        public void Atualizar(string nome, string email, string curso, int nota)
        {
            ValidarDomain(nome, email, curso, nota);
        }

        private void ValidarDomain(string nome, string email, string curso, int nota)
        {
            DomainExceptionValidation.When(string.IsNullOrEmpty(nome),
                "Nome inválido");

            DomainExceptionValidation.When(nome.Length < 3,
                "Nome inválido");

            DomainExceptionValidation.When(string.IsNullOrEmpty(email),
                "Email inválido");

            DomainExceptionValidation.When(email.Length < 3,
                "Email inválido");

            DomainExceptionValidation.When(!email.Contains("@"),
                "Formato de email inválido");

            DomainExceptionValidation.When(string.IsNullOrEmpty(curso),
                "Curso inválido");

            DomainExceptionValidation.When(curso.Length < 5,
                "Nome do curso inválido");

            DomainExceptionValidation.When(nota < 0, "Nota inválida");

            DomainExceptionValidation.When(nota > 10, "Nota inválida");

            Nome = nome;
            Email = email;
            Curso = curso;
            Nota = nota;

        }
    }
}

Nosso modelo de domínio embora não seja um domínio rico não é totalmente anêmico.

Aqui definimos o seguinte:

  1. A entidade Aluno é selada;
  2. A propriedades Set são privadas;
  3. Temos um construtor para criar Alunos;
  4. Temos um método Atualizar para atualizar Aluno;
  5. Estamos validando o domínio aplicando regras simples de validação de forma a termos um domínio válido;

Na pasta Interfaces vamos criar a interface IAlunoRepository :

 public interface IAlunoRepository
 {
        Task<IEnumerable<Aluno>> GetAlunosAsync(CancellationToken cancellationToken = default);
        Task<Aluno> GetAlunoIdAsync(int alunoId, CancellationToken cancellationToken = default);
        Task<Aluno> UpdateAsync(Aluno aluno);
        Task<Aluno> InsertAsync(Aluno aluno);
        Task<Aluno> RemoveAsync(Aluno aluno);
 }

Temos a definição de um contrato com métodos assíncronos que serão implementados no Repositório.

Mas afinal porque temos a definição de uma interface do repositório na camada de domínio ?

Isso ocorre porque os casos de uso no domínio não estão usando a implementação real do repositório que fica na camada de dados. Em vez disso, está apenas usando uma abstração/interface na Camada Domain que atua como um contrato para qualquer repositório que deseja fornecer os dados.

Observe que estamos definindo o argumento CancelamentoToken como um valor opcional e atribuindo a ele o valor padrão. Com essa abordagem, se não fornecermos um valor de CancellationToken, um CancellationToken.None será fornecido para nós. Fazendo isso, podemos garantir que nossas chamadas assíncronas que usam o CancelToken sempre funcionarão.

Ao final teremos a implementação da camada Domain :

2- Application

Esta camada também pode ser identificada como a camada de serviços, o nome não é o mais importante.

O que é importante é que esta camada fica logo acima da camada de Domínio, o que significa que tem uma referência à camada de Domínio.  Nesta camada vamos criar as seguintes pastas:

Neste projeto iremos definir os DTOs , o CQRS, o Mapeamento do DTO e criar a a interface de serviço e sua implementação.

Assim teremos que incluir uma referência ao projeto Domain neste projeto e também vamos incluir neste projeto as seguintes referências:

Vamos iniciar criando o arquivo AlunoDto na pasta DTOs :

using System;
using System.ComponentModel.DataAnnotations;

namespace Application.DTOs
{
    public class AlunoDto
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "Informe o nome")]
        [MinLength(3)]
        [MaxLength(100)]
        public string Nome { get;  set; }

        [Required(ErrorMessage = "Informe o email")]
        [MinLength(3)]
        [MaxLength(100)]
        public string Email { get;  set; }

        [Required(ErrorMessage = "Informe o curso")]
        [MinLength(3)]
        [MaxLength(100)]
        public string Curso { get;  set; }

        [Required(ErrorMessage = "Informe a nota")]
        [Range(1, 10)]
        public int Nota { get;  set; }
    }
}

Observe que aqui podemos definir os Data Annotations para validar os dados no input do usuário.

A seguir na pasta Mappings vamos criar o Mapeamento definindo a classe DomainToDtoMappingProfile usando os recursos do AutoMapper:

using Application.DTOs;
using AutoMapper;
using Domain.Entities;
namespace Application.Mappings
{
    public class DomainToDtoMappingProfile : Profile
    {
        public DomainToDtoMappingProfile()
        {
            CreateMap<Aluno, AlunoDto>().ReverseMap();
        }
    }
}

A seguir vamos criar a interface IAlunoService na pasta Interfaces para definir o serviço para gerenciar os alunos:

using Application.DTOs;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Application.Interfaces
{
    public interface IAlunoService
    {
        Task<IEnumerable<AlunoDto>> GetAlunosAsync(CancellationToken cancellationToken = default);
        Task<AlunoDto> GetAlunoIdAsync(int alunoId, CancellationToken cancellationToken = default);
        Task CreateAsync(AlunoDto alunoDto, CancellationToken cancellationToken = default);
        Task UpdateAsync(int alunoId, AlunoDto alunoDto, CancellationToken cancellationToken = default);
        Task DeleteAsync(int alunoId, CancellationToken cancellationToken = default);
    }
}

Aqui já estamos usando o DTO AlunoDto nas definições do serviço.

Agora para poder implementar o CQRS e o Serviço para o Cliente precisamos criar o contexto e o repositório na camada Infrastructure.

2- Infrastructure

Nesta camada teremos as implementações e as referências ao Entity Framework Core e aqui iremos armazenar o Migrations.

Vamos criar nesta pasta os seguintes projetos:

  1. Persistence
  2. CrossCutting

No projeto Persistence vamos criar as seguintes pastas:

Neste projeto vamos incluir uma referência ao projeto Domain e as referências aos seguintes pacotes:

Na pasta Context vamos criar a classe de Contexto ApplicationDbContext que herda de DbContext:

using Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace Persistence.Context
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        { }

        public DbSet<Aluno> Alunos { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
        }
    }
}

A seguir na pasta EntitiesConfiguration vamos criar a classe AlunoConfiguration onde vamos usar a Fluent API para configurar as propriedades da entidade Aluno para o EF Core:

using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Persistence.EntitiesConfiguration
{
    public class AlunoConfiguration : IEntityTypeConfiguration<Aluno>
    {
        public void Configure(EntityTypeBuilder<Aluno> builder)
        {
            builder.HasKey(t => t.Id);
            builder.Property(p => p.Nome).HasMaxLength(100).IsRequired();
            builder.Property(p => p.Email).HasMaxLength(200).IsRequired();
            builder.Property(p => p.Curso).HasMaxLength(200).IsRequired();
            builder.Property(p => p.Nota).IsRequired();
        }
    }
}

Agora na pasta Repositories vamos implementar o nosso repositório criando a classe AlunoRepository que vai herdar da classe IAlunoRepository definida na camada Domain:

using Domain.Entities;
using Domain.Interfaces;
using Microsoft.EntityFrameworkCore;
using Persistence.Context;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Persistence.Repositories
{
    public class AlunoRepository : IAlunoRepository
    {
       
private ApplicationDbContext _alunoContext;

        public AlunoRepository(ApplicationDbContext alunoContext)
        {
            _alunoContext = alunoContext;
        }

        public async Task<IEnumerable<Aluno>> GetAlunosAsync(CancellationToken cancellationToken = default)
        {
            return await _alunoContext.Alunos.ToListAsync();
        }

        public async Task<Aluno> GetAlunoIdAsync(int alunoId, CancellationToken cancellationToken = default)
        {
            return await _alunoContext.Alunos.SingleOrDefaultAsync(a => a.Id == alunoId);
        }

        public async Task<Aluno> InsertAsync(Aluno aluno)
        {
            _alunoContext.Add(aluno);
            await _alunoContext.SaveChangesAsync();
            return aluno;
        }

        public async Task<Aluno> UpdateAsync(Aluno aluno)
        {
            _alunoContext.Update(aluno);
            await _alunoContext.SaveChangesAsync();
            return aluno;
        }

        public async Task<Aluno> RemoveAsync(Aluno aluno)
        {
            _alunoContext.Remove(aluno);
            await _alunoContext.SaveChangesAsync();
            return aluno;  
        }
    }
}

Temos aqui o repositório que iremos usar para implementar os comandos e consultas no CQRS.

Na próxima parte do artigo iremos implementar o CQRS criando as consultas e os comandos usando o MediatR.

"Cheguemos, pois, com confiança ao trono da graça, para que possamos alcançar misericórdia e achar graça, a fim de sermos ajudados em tempo oportuno."
Hebreus 4:16

Referências:


José Carlos Macoratti