Skip to content

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

Alex Alves edited this page Mar 8, 2021 · 2 revisions

Antes de começar, vale ressaltar que 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 arquitetura para construção de uma API (Interface de Programação de Aplicativos).

Entendendo a arquitetura utilizada

architecture

  1. Application: Porta de entrada, responsável por receber as requisições e direcioná-las para camadas mais internas
  2. Domain: Um dos responsáveis pelo Core do projeto, contendo classes, enums e interfaces que poderão ser utilizadas para compor as regras de negócio
  3. Service: Um dos responsáveis pelo Core do projeto, onde é utilizado o que há na camada Domínio para realizar, de fato, as regras de negócio
  4. Infra: Camada para comunicação externa
    1. Infra.Data: Responsável pela comunicação com banco de dados, realizando operações
    2. Infra.CrossCutting: Responsável por registrar as dependências do projeto (IoC / DI), além de poder conter códigos reutilizáveis e comums para todo o projeto

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:

blank_solution

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

add_folders

folders

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.

asp_net_core_app

net_core_webapi

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

class_lib

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

layers

Implementação

Antes de colocar a mão na massa, vamos adicionar alguns pacotes na nossa solução:

  • AutoMapper v10.1.1: Application | Domain
  • FluentValidation.AspNetCore v9.5.1: Domain | Service
  • Microsoft.EntityFrameworkCore.Design v3.1.5: Application | Infra.Data
  • Microsoft.EntityFrameworkCore.Tools v3.1.5: Application | Infra.Data
  • MySqlConnector v0.69.4: Application | Infra.Data
  • Pomelo.EntityFrameworkCore.MySql: Application | Infra.Data

Camada Domain

Vamos criar duas pastas, uma para declarar as entidades e outra para as interfaces.

domain_layer

  • Pasta Entidades: vamos criar a classe BaseEntity, onde conterá propriedades base, no caso será apenas o ID, que servirão para demais classes do nosso domínio.
# 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; }
    }
}
  • Pasta Entidades: vamos criar, também, a classe User, a qual será nossa classe de domínio e herdará de BaseEntity para compor com as propriedades base.
# 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; }
    }
}

Já nas pasta de interfaces, vamos declarar interfaces genéricas, as quais receberão um Generics para identificar a classe de domínio que está sendo tratada no momento, onde serão referente ao repositório de acesso a base de dados e ao serviço que conterá algumas regras de negócio.

Na pasta destinada as interfaces, desenvolve-se as mesmas referentes a implementação de repositórios e serviços.

Na interface IBaseRepository, é necessário informar apenas a entidade de domínio (TEntity).

# 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);
    }
}

Já na interface IBaseService, além de informarmos a classe de domínio (TEntity) alguns métodos (Add, Update e Get) receberão mais alguns Generics, sendo:

  • TInputModel: classe modelo responsável por receber os dados de entrada da API
  • TOutputModel: classe modelo responsável por retornar os dados de uma requisição
  • TValidator: classe que contém as validações necessárias para o nosso domínio específico
# 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
    {
        TOutputModel Add<TInputModel, TOutputModel, TValidator>(TInputModel inputModel)
            where TValidator : AbstractValidator<TEntity>
            where TInputModel : class
            where TOutputModel : class;

        void Delete(int id);

        IEnumerable<TOutputModel> Get<TOutputModel>() where TOutputModel : class;

        TOutputModel GetById<TOutputModel>(int id) where TOutputModel : class;

        TOutputModel Update<TInputModel, TOutputModel, TValidator>(TInputModel inputModel)
            where TValidator : AbstractValidator<TEntity>
            where TInputModel : class
            where TOutputModel : class;
    }
}

Camada Infra.Data

Responsável pela conexão e operação com o banco de dados, no caso MySql

Nesta camada vamos criar três pastas chamadas Context, Mapping e Repository.

data_layer

  • 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: Responsável pelo mapeamento, customizado, de entidade para o banco de dados. Podendo especificar o nome da tabela, campos, tipo de cada campo, etc.
# 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: Responsável por realizar operações no banco de dados (CRUD).
# 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 ideia inicial é trabalhar com uma classe genérica para realizar as operações no banco de dados. Nesse caso, passamos um Generics para ela (TEntity) para que esse repositório trabalhe com base nele.

Camada Infra.CrossCutting

Não abordaremos esta camada neste momento, mas resumidamente ela seria responsável por conter todos os registros de dependências do projeto (IOC / DI) e algum código em comum que pode ser utilizado em qualquer parte do projeto.

Camada Service

É nesta camada que disponibilizaremos todas as regras de negócio e validações necessárias.

Assim, vamos criar 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 das nossas entidades de domínio.

service_layer

Na pasta Validators, cria-se uma classe chamada UserValidator:

# 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.");
        }
    }
}

Nesta classe conterá as validações necessárias referente a nossa entidade de domínio User. Para isso, utilizamos os recursos do pacote FluentValidation, deixando o nosso código um pouco mais elegante.

E na pasta Services, vamos criar 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 Neste exemplo, utilizamos uma classe, também, genérica, para executar as nossas regras de negócio. Como falado anteriormente, sobre a interface IBaseService, essa classe irá receber alguns Generics referente ao recebimento de dados (via objeto) da request, retorno de algum dado (via objeto) via response e validação da entidade de domínio.

Camada Application

A porta de entrada da nossa aplicação, nesta camada encontraremos controladores e serviços para efetuar as chamadas em nossa API.

Sendo assim, dentro da pasta Controllers, vamos criar 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.

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

api_empty

E vamos ter a seguinte implementação:

# https://github.com/alexalvess/layer-architecture/blob/main/Layer.Architecture.Application/Controllers/UserController.cs

using Layer.Architecture.Application.Models;
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] CreateUserModel user)
        {
            if (user == null)
                return NotFound();

            return Execute(() => _baseUserService.Add<CreateUserModel, UserModel, UserValidator>(user));
        }

        [HttpPut]
        public IActionResult Update([FromBody] UpdateUserModel user)
        {
            if (user == null)
                return NotFound();

            return Execute(() => _baseUserService.Update<UpdateUserModel, UserModel, 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<UserModel>());
        }

        [HttpGet("{id}")]
        public IActionResult Get(int id)
        {
            if (id == 0)
                return NotFound();

            return Execute(() => _baseUserService.GetById<UserModel>(id));
        }

        private IActionResult Execute(Func<object> func)
        {
            try
            {
                var result = func();

                return Ok(result);
            }
            catch (Exception ex)
            {
                return BadRequest(ex);
            }
        }
    }
}

Esta controller, criamos seguindo alguns princípios do RESTfull. Além disso, solicitamos a utilização de algumas dependências, que a o caso do IBaseService.

Conclusão

Podemos notar que é possível construir um projeto, para aplicações de pequeno porte, sem ter grandes complexidade. E, utilizando alguns princípios macros DDD como um guia, buscamos focar sempre no domínio da aplicação.

Além disso, vale ressaltar que a maneira como foi utilizado os Repositório e Serviços, foi apenas para simplificar o desenvolvimento e mostrar que algumas coisas podem ser feitas de maneira genérica e, tais classes, em projetos corporativos/sérios, poderiam ser utilizadas como realmente classes extensoras sempre de classes/abstrações específicas.

O projeto completo pode ser conferido no seguinte link: https://github.com/alexalvess/layer-architecture

Referências: