Hoje vamos criar um projeto ASP .NET Core Web API usando conceitos do Domain Driven Design mais conhecido como DDD. |
Antes de mais nada vou apresentar o Domain Driven Design ou Design orientado ao domínio (DDD).
O DDD é uma filosofia de desenvolvimento de softwares complexos usando boas práticas na qual a estrutura e a linguagem do seu código (nomes de classe, métodos, variáveis, etc.) devem estar focados no modelo de domínio ou negócio.
Encare o DDD como uma prescrição de metodologia e processo para o desenvolvimento de sistemas complexos cujo foco é mapear atividades, tarefas, eventos e dados dentro de um domínio de problema nos artefatos de tecnologia de um domínio de solução.
Assim o DDD não é arquitetura, não é tecnologica, não é framework e não é linguagem.
Disclaimer
E qual é o objetivo deste artigo ?
Será criar uma aplicação ASP .NET Core onde vamos usar muitos conceitos usados pela abordagem DDD para termos uma arquitetura limpa e desacoplada. Só isso.
Assim eu não vou criar um projeto ASP .NET Core usando a arquitetura DDD porque isso não existe. OK ?
Vamos apenas usar as boas práticas associadas aos princípios SOLID, KISS e YAGNI e definir uma arquitetura onde vamos aplicar alguns dos conceitos do DDD.
Vamos iniciar definindo o nosso domínio que será o gerenciamento de informações de Contatos onde teremos o nome e o email do contato e onde vamos querer consultar, exibir, criar e atualizar contatos. Só isso.
Assim vamos usar um CRUD básico como pretexto para mostrar como podemos aplicar alguns dos conceitos DDD e obter um software mais robusto.
A seguir vou me basear em uma arquitetura genérica para criar a aplicação e que será definida da seguinte forma:
Procurei uma imagem que fosse uma representação mais fiel dessa abordagem que iremos fazer e achei a imagem abaixo:
Na figura
abaixo vemos que :
1- A camada da infraestrutura
conhece a camada de aplicação; Nota: A nossa camada de aplicação vai apenas realizar a injeção de dependência dos serviços definidos |
Abaixo temos uma visão mostrando o papel de cada 'camada' em nosso projeto. Note que o Domínio é o core, e tudo gira ao seu redor, e, além disso cabe destacar as seguintes vantagens apresentada pela aplicação destes conceitos em nosso projeto:
|
Nota: Você vai encontrar nomes e número de camadas diferentes mas o que importa é a aplicação dos conceitos DDD de forma correta. ( A figura acima evoca a arquitetura Onion)
Dessa forma em nossa implementação criaremos uma solução e 4 projetos :
O projeto principal é a camada de domínio. Todas as demais camadas usarão e dependerão da camada principal e esta não depende de nenhuma outra camada. O importante é que cada projeto terá a sua responsabilidade.
O projeto Contatos.Application vai apenas reunir as dependências aos projetos Domain e Infra e realizar a injeção de dependência dos serviços e inicializar a aplicação.
Uma decisão que não diz respeito ao DDD é a ferramenta que iremos usar para criar o projeto. Podemos usar o Visual Studio Community no ambiente Windows ou usar a ferramenta de linha de comando e o VS Code no Windows, Linux ou Mac.
Neste artigo iremos usar a ferramenta de linha de comando NET CLI e o VS Code, assim você pode criar o projeto no Windows, Linux ou Mac.
Assim vamos ao que interessa...
Recursos usados:
Criando o projeto e a solução usando a NET CLI e o VS Code
Vamos criar uma pasta onde a nossa solução e os nossos projetos serão criados.
Por questão de organização eu vou criar uma pasta projetos : mkdir c:\projetos
Dentro desta pasta vou criar a pasta WebDDD onde vou criar a nossa solução e os projetos: mkdir WebDDD
Dentro desta pasta vamos criar a pasta src onde estarão os fontes da nossa aplicação : mkdir src
Assim o caminho da nossa solução será c:\projetos\WebDDD e os fontes estarão em c:\projetos\WebDDD\src
Vamos abrir o Visual Studio Code a partir da pasta WebDDD digitando: code .
A seguir vamos abrir um terminal no VS Code de forma a podermos executar os comandos no terminal e visualizar o projeto.
Na janela do terminal vamos nos posicionar na pasta src para criar os projetos:
Vamos criar o primeiro projeto que será o projeto de domínio digitando:
dotnet new classlib --name Contatos.Domain
Aqui estamos criando um projeto do tipo Class Library chamado Contatos.Domain na pasta src.
E vemos abaixo o resultado da execução do comando onde temos o projeto criado.
Note que é executado um comando donet restore para restaurar as dependências do projeto.
Temos assim um projeto criado e agora vamos criar um arquivo de solução ou .sln para gerenciar os projetos que iremos criar. Assim através da solução poderemos referenciar e relacionar os demais projetos.
Vamos voltar para a pasta WebDDD e partir desta pasta executar o comando: dotnet new sln --name Contatos
Este comando vai criar o arquivo Contatos.sln na pasta WebDDD.
Agora vamos incluir o projeto Contatos.Domain na solução que foi criada.
Para isso estando na pasta
WebDDD digite o comando:
dotnet sln add
src/Contatos.Domain/Contatos.Domain.csproj
Isso incluir o projeto Contatos.Domain na solução e agora podemos executar um dotnet restore na pasta WebDDD.
A seguir vamos voltar para a pasta src, e criar os demais projetos usando os comandos abaixo:
E a seguir, voltando para pasta WebDDD, vamos incluir todos esses projetos na solução usando os comandos:
Ao final teremos a solução e todos os projetos criados e incluídos na solução:
Implementando o projeto Contatos.Domain
Nesta camada vamos criar as entidades do nosso dominio que será representado pela classe : Contato visto que nossa aplicação via gerenciar contatos.
Vamos entrar na pasta Contatos.Domain e criar uma pasta chamada Models onde vamos criar a classe que representa o nosso modelo de domínio.
Aqui vamos criar uma classe abstrata base BaseEntity onde vamos definir a propriedade Id :
1- BaseEntity
public class BaseEntity { public int Id { get; private set; } } |
A seguir vamos criar a classe Contato que herda da clase BaseEntity:
2- Contato
using System;
namespace Contatos.Domain.Models
{
public class Contato : BaseEntity
{
public Contato(string nome, string email)
{
ValidaCategoria(nome, email);
Nome = nome;
Email = email;
}
public string Nome { get; private set; }
public string Email{ get; private set; }
public void Update(string nome, string email)
{
ValidaCategoria(nome, email);
}
private void ValidaCategoria(string nome,string email)
{
if(string.IsNullOrEmpty(nome))
throw new InvalidOperationException("O nome é inválido");
if(string.IsNullOrEmpty(email))
throw new InvalidOperationException("O email é inválido");
}
}
}
|
Observe que nossa classe de domínio somente permite receber os valores para nome e email via construtor e realiza uma validação possuindo assim uma lógica bem simples mas não é mais uma classe POCO anêmica.
Criamos também o método Update que iremos usar quando formos atualizar os dados da entidade.
Vamos agora criar nesta pasta Models a classe ContatoService:
using Contatos.Domain.Interfaces;
namespace Contatos.Domain.Models
{
public class ContatoService
{
private readonly IRepository<Contato> _contatoRepository;
public ContatoService(IRepository<Contato> contatoRepository)
{
_contatoRepository = contatoRepository;
}
public void Save(int id, string nome, string email)
{
var contato = _contatoRepository.GetById(id);
if(contato == null)
{
contato = new Contato(nome, email);
_contatoRepository.Save(contato);
}
else
contato.Update(nome, email);
}
}
}
|
A classe de serviço utiliza o repositório que iremos criar a seguir para poder realizar a persistência na inclusão e atualização dos dados.
Vamos criar a pasta Interfaces e definir nesta pasta da camada Domain as interfaces para o repositório e para a unit of work.
Crie na pasta Interfaces a interface IRepository:
using System.Collections.Generic;
namespace Contatos.Domain.Interfaces
{
public interface IRepository<TEntity> where TEntity : class
{
TEntity GetById(int id);
IEnumerable<TEntity> GetAll();
void Save(TEntity entity);
}
}
|
Agora crie a interface IUnitOfWork:
using System.Threading.Tasks;
namespace Contatos.Domain.Interfaces
{
public interface IUnitOfWork
{
Task Commit();
}
}
|
Esta interface vai ser usada para dar o commit ou seja o SaveChanges para efetivar a persistência.
Essas interfaces serão implementadas no projeto Contatos.Infra.
O que vale a pena destacar é que o projeto Contatos.Domain não deve conter nenhuma dependência e seu arquivo de projeto deve estar apenas com a dependência do .Net Standard:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
|
Implementando o projeto Contatos.Infra
Neste projeto vamos criar a classe de contexto e implementar as interfaces definidas no projeto Contatos.Domain. Será aqui que iremos aplicar o Migrations.
Vamos criar as pastas Context e Repositories neste projeto e a seguir vamos incluir referências aos pacotes:
Para isso digite o comando:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 3.1.4
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 3.1.4
Precisamos também incluir uma referência ao projeto Contatos.Domain neste projeto. Para isso posicione-se na pasta do src e digite o comando :
dotnet add .\contatos.infra\contatos.infra.csproj reference .\contatos.domain\contatos.domain.csproj
Na pasta Context crie o arquivo AppDbContext com a definição do contexto:
using Contatos.Domain.Models;
using Microsoft.EntityFrameworkCore;
namespace Contatos.Infra.Context
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<Contato> Contatos { get; set; }
}
}
|
Na pasta Repositories vamos criar as classes concretas que implementam as interfaces criadas no projeto Contatos.Domain:
1- Repository
using System.Collections.Generic;
using System.Linq;
using Contatos.Domain.Interfaces;
using Contatos.Domain.Models;
using Contatos.Infra.Context;
namespace Contatos.Infra.Repositories
{
public class Repository<TEntity> : IRepository<TEntity> where TEntity : BaseEntity
{
protected readonly AppDbContext _context;
public Repository(AppDbContext context)
{
_context = context;
}
public virtual TEntity GetById(int id)
{
var query = _context.Set<TEntity>().Where(e => e.Id == id);
if(query.Any())
return query.FirstOrDefault();
return null;
}
public virtual IEnumerable<TEntity> GetAll()
{
var query = _context.Set<TEntity>();
if(query.Any())
return query.ToList();
return new List<TEntity>();
}
public virtual void Save(TEntity entity)
{
_context.Set<TEntity>().Add(entity);
}
}
}
|
2- ContatoRepository
using
System.Collections.Generic; using System.Linq; using Contatos.Domain.Models; using Contatos.Infra.Context;
namespace
Contatos.Infra.Repositories
public override Contato
GetById(int id)
if(query.Any())
return null;
public override IEnumerable<Contato>
GetAll()
return query.Any() ? query.ToList() : new
List<Contato>(); |
3- UnitOfWork
using
System.Threading.Tasks; using Contatos.Domain.Interfaces; using Contatos.Infra.Context;
namespace
Contatos.Infra.Repositories
public UnitOfWork(AppDbContext
context)
public async Task Commit() |
Implementando o projeto Contatos.Web
Neste projeto vamos criar a aplicação ASP .NET Core Web API onde vamos definir o controlador ContatosController, o arquivo DTO, a string de conexão no arquivo appsettings e no arquivo Startup vamos criar a classe de inicialização da nossa aplicação.
Vamos iniciar incluindo referências aos projetos Contatos.Domain e Contatos.Application a este projeto. Para isso posicione-se na pasta src e digite os comandos:
dotnet add .\contatos.web\contatos.web.csproj reference .\Contatos.Domain\Contatos.Domain.csproj
dotnet add .\contatos.web\contatos.web.csproj reference .\Contatos.Application\Contatos.Application.csproj
A seguir vamos criar a pastas DTOs neste projeto e nesta pasta criar a classe ContatoDTO:
using System.ComponentModel.DataAnnotations;
namespace Contatos.Web.DTOs
{
public class ContatoDTO
{
public int Id { get; set; }
[Required]
public string Nome { get; private set; }
[Required]
public string Email{ get; private set; }
}
}
|
No arquivo appsettings.json vamos definir a string de conexão:
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=Macoratti;Initial Catalog=ContatosDDD;Integrated Security=True"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
|
No arquivo Startup, no método ConfigureServices vamos criar o método que chama a classe que vai inicializar a nossa aplicação:
public void ConfigureServices(IServiceCollection services)
{
Initializer.Configure(services, Configuration.GetConnectionString("DefaultConnection"));
services.AddControllers();
}
|
Aqui estamos passando a instância de ServiceCollection e a string de conexão para o método estático Configure da classes Initiliazer que iremos criar no projeto Contatos.Application.
Para concluir vamos criar na pasta Controllers o controlador ContatosController onde vamos implementar apenas a obtenção de todos os contatos e do contato pelo seu Id, visto que nosso objetivo não é criar uma API completa mas mostrar como aplicar os conceitos DDD para ter um projeto com uma arquitetura mais limpa.
using System.Collections.Generic;
using System.Linq;
using Contatos.Domain.Interfaces;
using Contatos.Domain.Models;
using Contatos.Web.DTOs;
using Microsoft.AspNetCore.Mvc;
namespace Contatos.Web.Controllers
{
public class ContatosController : Controller
{
private readonly ContatoService _contatoService;
private readonly IRepository<Contato> _contatoRepository;
public ContatosController(ContatoService contatoService,
IRepository<Contato> contatoRepository)
{
_contatoService = contatoService;
_contatoRepository = contatoRepository;
}
[HttpGet]
public IEnumerable<Contato> GetContatos()
{
var contatos = _contatoRepository.GetAll();
return View(viewsModels);
}
[HttpGet("{id}")]
public ActionResult<Contato> GetContato(int id)
{
var contato = _contatoRepository.GetById(id);
if (contato == null)
{
return NotFound(new { message = $"Contato de id={id} não encontrado" });
}
return contato;
}
}
}
|
Neste codigo injetamos os serviços no construtor e definimos os métodos HttpGet para retornar os contatos e um contato pelo Id.
Observe que nosso projeto Web API somente possui referências aos projetos Contatos.Domain e Contatos.Application e não temos referência ao Entity Framework Core.
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\Contatos.application\Contatos.application.csproj" />
<ProjectReference Include="..\Contatos.domain\Contatos.domain.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
</Project>
|
Implementando o projeto Contatos.Application
Neste projeto vamos criar uma pasta DI e nesta pasta criar a classe Initializer onde vamos usar a instância de ServiceCollection para realizar a injeção de dependência dos serviços e também definir o provedor do banco de dados e a string de conexão.
Precisamos incluir neste projeto referências aos projetos Contatos.Infra e Contatos.Domain e para isso vamos nos posicionar na pasta src e digitar os comandos:
dotnet add .\Contatos.Application\Contatos.Applicatin.csproj reference .\Contatos.Domain\Contatos.Domain.csproj
dotnet add .\Contatos.Application\Contatos.Applicatin.csproj reference .\Contatos.Infra\Contatos.Infra.csproj
A seguir vamos criar na pasta DI a clase Initializer:
using Contatos.Domain.Interfaces;
using Contatos.Domain.Models;
using Contatos.Infra.Context;
using Contatos.Infra.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Contatos.Application.DI {
public class Initializer
{
public static void Configure (IServiceCollection services, string conection)
{
services.AddDbContext<AppDbContext> (options => options.UseSqlServer (conection));
services.AddScoped (typeof (IRepository<Contato>), typeof (ContatoRepository)); services.AddScoped (typeof (IRepository<>), typeof (Repository<>)); services.AddScoped (typeof (ContatoService)); services.AddScoped (typeof (IUnitOfWork), typeof (UnitOfWork)); } } } |
E estamos prontos para aplicar o Migrations digitando o seguinte comando:
dotnet ef --startup-project ..\contatos.web\contatos.web.csproj --project .\contatos.infra.csproj migrations add ContatosInicial
Observe que no comando definimos o projeto startup como sendo o projeto Web que contém a string de conexão e o projeto infra onde temos as referências ao Entity Framework.
Após a execução do comando teremos a pasta Migrations com os scripts de migração criados:
Agora podemos aplicar o Migrations para criar o banco de dados e a tabela Contatos; posicione-se na pasta do projeto contatos.infra e digite o comando:
dotnet ef --startup-project ..\contatos.web\contatos.web.csproj --project .\contatos.infra.csproj database update
E Voilá, temos o banco ContatosDDD e a tabela Contatos criados conforme podemos visualizar no SQL Server Management Studio:
Agora é só alegria...
Posicionando-se no projeto contatos.web e digitando : dotnet run
Incluindo alguns dados e abrindo o navegador em: https://localhost:5001/api/contatos teremos:
Temos assim uma solução com 4 projetos onde aplicamos alguns conceitos usados pela abordagem DDD para criar uma aplicação Web API no Visual Studio Code com camadas de forma desacoplada.
Pegue o projeto aqui : WebDDD.zip
"No princípio era o Verbo, e o
Verbo estava com Deus, e o Verbo era Deus.
Ele estava no princípio com Deus.
Todas as coisas foram feitas por ele, e sem ele nada do
que foi feito se fez."
João
1:1-3
Referências:
ASP .NET Core - Iniciando com o Blazor -
ASP .NET Core - CRUD usando Blazor e Entity ...