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;
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) |
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 DomainExceptionValidation.When(nome.Length < 3, DomainExceptionValidation.When(string.IsNullOrEmpty(email), DomainExceptionValidation.When(email.Length < 3, DomainExceptionValidation.When(!email.Contains("@"), DomainExceptionValidation.When(string.IsNullOrEmpty(curso), DomainExceptionValidation.When(curso.Length < 5, DomainExceptionValidation.When(nota < 0, "Nota inválida"); DomainExceptionValidation.When(nota > 10, "Nota inválida"); Nome = nome; |
Nosso modelo de domínio embora não seja um domínio rico não é totalmente anêmico.
Aqui definimos o seguinte:
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 [Required(ErrorMessage = "Informe o nome")] [Required(ErrorMessage = "Informe o email")] [Required(ErrorMessage = "Informe o curso")] [Required(ErrorMessage = "Informe a nota")] |
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 |
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:
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 DbSet<Aluno> Alunos { get; set; } protected override void OnModelCreating(ModelBuilder builder) |
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 |
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 AlunoRepository(ApplicationDbContext alunoContext) public async Task<IEnumerable<Aluno>> GetAlunosAsync(CancellationToken cancellationToken = default) public async Task<Aluno> GetAlunoIdAsync(int alunoId, CancellationToken cancellationToken = default) public async Task<Aluno> InsertAsync(Aluno aluno) public async Task<Aluno> UpdateAsync(Aluno aluno) public async Task<Aluno> RemoveAsync(Aluno 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:
C# - Lendo e escrevendo em arquivos textos e binários
C# - Entendo o I/O na plataforma .NET
C# - Fluxo assíncrono ou async streams