Uma arquitetura, em .Net Core, baseada nos princípios do DDD

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

  1. 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.
  2. 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.
  3. 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.
  4. 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:

Criando uma projeto vazio

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).

Adicionando pastas
Modelo final, após criação das pastas

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.

Criando um projeto do tipo Asp.Net Core Web Application
Selecionando o tipo 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).

Criando projeto do tipo Class Library (.Net Core)

A estrutura final da solução ficará da seguinte maneira:

Estrutura final do projeto

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.

Estrutura da camada de Domain

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.

Estrtura da camada Infra.Data
  • 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.

Estrutura da camada Service

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.

Criando um 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.

Selecionando um tipo de controller

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

Referências

Bachelor in Computer Science, MBA in Software Architecture and .NET Developer.

Bachelor in Computer Science, MBA in Software Architecture and .NET Developer.