Começando com .NET Core, com Arquitetura em Camadas
Antes de começar, DDD não é uma arquitetura. O DDD (Domain Driven Design) é uma modelagem de software cujo objetivo é facilitar a implementação de regras e processos complexos, onde visa a divisão de responsabilidades por camadas e é independente da tecnologia utilizada. Ou seja, o DDD é uma filosofia voltado para o domínio do negócio.
Levando em consideração este conceito, é proposto desenvolver uma nova arquitetura para construção de uma API (Interface de Programação de Aplicativos).
Entendendo a arquitetura utilizada
- Camada de aplicação: responsável pelo projeto principal, pois é onde será desenvolvido os controladores e serviços da API. Tem a função de receber todas as requisições e direcioná-las a algum serviço para executar uma determinada ação.
Possui referências das camadas Service e Domain. - Camada de domínio: responsável pela implementação de classes/modelos, as quais serão mapeadas para o banco de dados, além de obter as declarações de interfaces, constantes, DTOs (Data Transfer Object) e enums.
- Camada de serviço: seria o “coração” do projeto, pois é nela que é feita todas as regras de negócio e todas as validações, antes de persistir os dados no banco de dados.
Possui referências das camadas Domain, Infra.Data e Infra.CrossCutting. - Camada de infraestrutura: é dividida em duas sub-camadas
- Data: realiza a persistência com o banco de dados, utilizando, ou não, algum ORM.
- Cross-Cutting: uma camada a parte que não obedece a hierarquia de camada. Como o próprio nome diz, essa camada cruza toda a hierarquia. Contém as funcionalidades que pode ser utilizada em qualquer parte do código, como, por exemplo, validação de CPF/CNPJ, consumo de API externa e utilização de alguma segurança.
Possui referências da camada Domain.
Criando o projeto
O projeto em questão será um CRUD (Criar, Ler, Alterar e Deletar) simples o qual utiliza-se o banco de dados MySql e o ORM EntityFramework Core.
Primeiramente, cria-se uma solução vazia:
Após o a solução criada, cria-se as pastas referente a cada uma das camadas, considerando que a camada de infraestrutura possui duas sub-camadas (Data e CrossCutting).
Na camada de aplicação (1 — Application), gera-se um projeto do tipo ASP.Net Core Web Application, onde deve-se a opção Web API.
Nas camadas de domínio (2 — Domain), serviço (3 — Service) e infraestrutura (4 — Infra), forma-se com projetos do tipo Class Library (.Net Core).
A estrutura final da solução ficará da seguinte maneira:
Implementando as camadas
Camada Domain
Primeiramente, deve-se instalar os seguintes pacotes:
— FluentValidation.AspNetCore
Este pacote serve para realizar a validação das entidades. No caso, utilizar-se-á a declaração AbstractValidator na declaração da interface para os serviços.
Em seguida cria-se duas pastas, uma para declarar as entidades e outra para declarar as interfaces que serão utilizadas.
Dentro da pasta de entidades desenvolve-se uma classe chamada BaseEntity, a qual terá a propriedade Id. Esta classe será herdada por todas as outras entidades criadas, obrigando todos os modelos a possuírem um Id. E gera-se outra classe chamada User.
# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Domain/Entities/BaseEntity.cs
namespace Layer.Architecture.Domain.Entities
{
public abstract class BaseEntity
{
public virtual int Id { get; set; }
}
}# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Domain/Entities/User.cs
namespace Layer.Architecture.Domain.Entities
{
public class User : BaseEntity
{
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
}
Na pasta destinada as interfaces, desenvolve-se as mesmas referentes a implementação de repositórios e serviços.
Obs’.: Ambas as interfaces são genéricas, onde recebem um modelo (T) como parâmetro, identificando sobre qual entidade àquela interface irá atuar.
Obs’’.: Os métodos Post e Put da interface IService recebem como parâmetro a entidade para validação (V) do modelo referente (T).
# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Domain/Interfaces/IBaseRepository.cs
using Layer.Architecture.Domain.Entities;
using System.Collections.Generic;
namespace Layer.Architecture.Domain.Interfaces
{
public interface IBaseRepository<TEntity> where TEntity : BaseEntity
{
void Insert(TEntity obj);
void Update(TEntity obj);
void Delete(int id);
IList<TEntity> Select();
TEntity Select(int id);
}
}# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Domain/Interfaces/IBaseService.cs
using FluentValidation;
using Layer.Architecture.Domain.Entities;
using System.Collections.Generic;
namespace Layer.Architecture.Domain.Interfaces
{
public interface IBaseService<TEntity> where TEntity : BaseEntity
{
TEntity Add<TValidator>(TEntity obj) where TValidator : AbstractValidator<TEntity>;
void Delete(int id);
IList<TEntity> Get();
TEntity GetById(int id);
TEntity Update<TValidator>(TEntity obj) where TValidator : AbstractValidator<TEntity>;
}
}
Camada Infra.Data
Essa camada será responsável por conectar ao banco de dados, no caso será utilizado o MySql, e realizar as persistências.
Inicialmente instala-se os seguintes pacotes:
— Microsoft.EntityFrameworkCore.Design
— Microsoft.EntityFrameworkCore.Tools
— MySqlConnector
— Pomelo.EntityFrameworkCore.MySql
Estes pacotes servirão para poder utilizar o EF Core com o MySql, utilizando, também, o Migrations. Não será dado ênfase na configuração destas etapas pois não faz parte do contexto proposto.
Cria-se três pastas chamadas Context, Mapping e Repository.
- Context: ficará a classe de contexto, responsável por conectar no banco de dados e, também, por fazer o mapeamento das tabelas do banco de dados nas entidades.
# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Infra.Data/Context/MySqlContext.cs
using Layer.Architecture.Domain.Entities;
using Layer.Architecture.Infra.Data.Mapping;
using Microsoft.EntityFrameworkCore;
namespace Layer.Architecture.Infra.Data.Context
{
public class MySqlContext : DbContext
{
public MySqlContext(DbContextOptions<MySqlContext> options) : base(options)
{
}
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>(new UserMap().Configure);
}
}
}
- Mapping: ficará as classes referente ao mapeamento de cada entidade. Nela realiza-se algumas configurações referente a própria entidade, como, por exemplo, o nome da tabela que vai para o banco de dados, o nome das colunas e qual será a chave primária.
# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Infra.Data/Mapping/UserMap.cs
using Layer.Architecture.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Layer.Architecture.Infra.Data.Mapping
{
public class UserMap : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("User");
builder.HasKey(prop => prop.Id);
builder.Property(prop => prop.Name)
.HasConversion(prop => prop.ToString(), prop => prop)
.IsRequired()
.HasColumnName("Name")
.HasColumnType("varchar(100)");
builder.Property(prop => prop.Email)
.HasConversion(prop => prop.ToString(), prop => prop)
.IsRequired()
.HasColumnName("Email")
.HasColumnType("varchar(100)");
builder.Property(prop => prop.Password)
.HasConversion(prop => prop.ToString(), prop => prop)
.IsRequired()
.HasColumnName("Password")
.HasColumnType("varchar(100)");
}
}
}
- Repository: ficará as classes responsáveis por realizar o CRUD no banco de dados.
# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Infra.Data/Repository/BaseRepository.cs
using Layer.Architecture.Domain.Entities;
using Layer.Architecture.Domain.Interfaces;
using Layer.Architecture.Infra.Data.Context;
using System.Collections.Generic;
using System.Linq;
namespace Layer.Architecture.Infra.Data.Repository
{
public class BaseRepository<TEntity> : IBaseRepository<TEntity> where TEntity : BaseEntity
{
protected readonly MySqlContext _mySqlContext;
public BaseRepository(MySqlContext mySqlContext)
{
_mySqlContext = mySqlContext;
}
public void Insert(TEntity obj)
{
_mySqlContext.Set<TEntity>().Add(obj);
_mySqlContext.SaveChanges();
}
public void Update(TEntity obj)
{
_mySqlContext.Entry(obj).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
_mySqlContext.SaveChanges();
}
public void Delete(int id)
{
_mySqlContext.Set<TEntity>().Remove(Select(id));
_mySqlContext.SaveChanges();
}
public IList<TEntity> Select() =>
_mySqlContext.Set<TEntity>().ToList();
public TEntity Select(int id) =>
_mySqlContext.Set<TEntity>().Find(id);
}
}
Sobre a classe BaseRepository
A intenção é de ter uma única classe, genérica, para realizar o CRUD, onde pode-se passar uma entidade T para ela, e essa classe irá trabalhar em cima dessa entidade. Herda-se a interface IRepository, onde obriga-se a classe a implementar os métodos que definiu-se anteriormente na camada de domínio.
Camada Infra.CrossCutting
Essa camada não será implementada nesse projeto, mas pode-se desenvolver nela, por exemplo, a validação de CPF e o consumo uma API de terceiro, além de outras funcionalidades que considera-se ser utilitária.
Camada Service
Previamente instala-se o seguinte pacote:
— FluentValidation.ApsNetCore
É nesta camada que disponibiliza-se todas as regras de negócio e validações necessárias. O pacote instalado servirá como framework para efetuar validações de objetos referentes às entidades.
Forma-se 2 pastas, uma chamada de Services a qual ficará os serviços contendo as regras de negócio e uma chamada Validators, onde ficará as validações de entidades.
Na pasta Validators, cria-se uma classe chamada UsuarioValidator, a qual será utilizada para validar toda a entidade de usuário.
# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Service/Validators/UserValidator.cs
using FluentValidation;
using Layer.Architecture.Domain.Entities;
namespace Layer.Architecture.Service.Validators
{
public class UserValidator : AbstractValidator<User>
{
public UserValidator()
{
RuleFor(c => c.Name)
.NotEmpty().WithMessage("Please enter the name.")
.NotNull().WithMessage("Please enter the name.");
RuleFor(c => c.Email)
.NotEmpty().WithMessage("Please enter the email.")
.NotNull().WithMessage("Please enter the email.");
RuleFor(c => c.Password)
.NotEmpty().WithMessage("Please enter the password.")
.NotNull().WithMessage("Please enter the password.");
}
}
}
Na pasta Services, desenvolve-se uma classe chamada BaseService.
# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Service/Services/BaseService.cs
using FluentValidation;
using Layer.Architecture.Domain.Entities;
using Layer.Architecture.Domain.Interfaces;
using System;
using System.Collections.Generic;
namespace Layer.Architecture.Service.Services
{
public class BaseService<TEntity> : IBaseService<TEntity> where TEntity : BaseEntity
{
private readonly IBaseRepository<TEntity> _baseRepository;
public BaseService(IBaseRepository<TEntity> baseRepository)
{
_baseRepository = baseRepository;
}
public TEntity Add<TValidator>(TEntity obj) where TValidator : AbstractValidator<TEntity>
{
Validate(obj, Activator.CreateInstance<TValidator>());
_baseRepository.Insert(obj);
return obj;
}
public void Delete(int id) => _baseRepository.Delete(id);
public IList<TEntity> Get() => _baseRepository.Select();
public TEntity GetById(int id) => _baseRepository.Select(id);
public TEntity Update<TValidator>(TEntity obj) where TValidator : AbstractValidator<TEntity>
{
Validate(obj, Activator.CreateInstance<TValidator>());
_baseRepository.Update(obj);
return obj;
}
private void Validate(TEntity obj, AbstractValidator<TEntity> validator)
{
if (obj == null)
throw new Exception("Registros não detectados!");
validator.ValidateAndThrow(obj);
}
}
}
Sobre a classe BaseService
É uma classe genérica utilizada para centralizar o CRUD, onde passa-se uma entidade como parâmetro, a qual irá trabalhar os serviços em cima da mesma, igualmente feito com o repositório. Além do mais, nos métodos de inserção e alteração, passa-se a classe responsável por validar a entidade, assim será obrigado efetuar a validação da mesma, através do método privado Validate.
Camada Application
Esta camada é a “porta de entrada” do sistema, pois é nela que conterá os controladores e serviços para efetuar as chamadas na API.
Dentro da pasta Controllers, cria-se uma classe chamada UserController.
Para isso, clica-se com o botão direito na pasta, seleciona-se a opção Add e, por fim, a opção controller.
Opós feito o processo acima, seleciona-se a opção API Controller — Empty e atribui-se o nome de UserController ao controller que será criado.
Dentro dessa classe faz-se a seguinte implementação:
# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Application/Controllers/UserController.cs
using Layer.Architecture.Domain.Entities;
using Layer.Architecture.Domain.Interfaces;
using Layer.Architecture.Service.Validators;
using Microsoft.AspNetCore.Mvc;
using System;
namespace Layer.Architecture.Application.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
private IBaseService<User> _baseUserService;
public UserController(IBaseService<User> baseUserService)
{
_baseUserService = baseUserService;
}
[HttpPost]
public IActionResult Create([FromBody] User user)
{
if (user == null)
return NotFound();
return Execute(() => _baseUserService.Add<UserValidator>(user).Id);
}
[HttpPut]
public IActionResult Update([FromBody] User user)
{
if (user == null)
return NotFound();
return Execute(() => _baseUserService.Update<UserValidator>(user));
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
if (id == 0)
return NotFound();
Execute(() =>
{
_baseUserService.Delete(id);
return true;
});
return new NoContentResult();
}
[HttpGet]
public IActionResult Get()
{
return Execute(() => _baseUserService.Get());
}
[HttpGet("{id}")]
public IActionResult Get(int id)
{
if (id == 0)
return NotFound();
return Execute(() => _baseUserService.GetById(id));
}
private IActionResult Execute(Func<object> func)
{
try
{
var result = func();
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(ex);
}
}
}
}
É criado um controlador baseado nas GuideLines do RESTFull do .Net Core, onde tem-se uma inserção, alteração, remoção, recuperação de todos os registros e recuperação de um registro pelo Id.
É desenvolvido uma instância do serviço genérico e passa-se para o mesmo, como argumento, a classe de User, identificando que irá atuar com o serviço referente ao usuário. Nos métodos de Post e Put, introduz-se no método, como argumento, a classe de validação para que o objeto seja validado na camada de serviço.
Conclusão
O foco deste artigo é demonstrar que é possível fazer um projeto de pequeno porte utilizando os conceitos de DDD e criando uma nova arquitetura, além de utilizar várias classes genéricas para poupar trabalho no desenvolvimento, trazendo os conceitos de polimorfismo e encapsulamento.
O projeto completo pode ser conferido no seguinte link:
https://github.com/alexalvess/layer-architecture