ASP .NET Core - API com VS Code e conceitos do DDD

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;
2- A camada de aplicação conhece a infraestrutura e o domínio;

3- A camada de apresentação conhece todo mundo;

4- O domínio não conhece nada além dele;

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:

  • Mais fácil de testar
  • Mais fácil de manter
  • Melhor dependência de banco de dados
  • Melhor dependência de serviços
  • Comunicação entre as camadas com interfaces
  • Dependências externas são camadas exteriores
  • Entidades de domínio são camadas internas

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 :

  1. Contatos.sln - Solução
  2. Contatos.Web - projeto Web API Asp .NET Core
  3. Contatos.Application -  projeto Class Library
  4. Contatos.Domain - projeto Class Library
  5. Contatos.Infra - projeto Class Library

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:

  • dotnet new classlib --name Contatos.Application
  • dotnet new classlib --name Contatos.Infra
  • dotnet new webapi --name Contatos.Web

E a seguir, voltando para pasta WebDDD, vamos incluir todos esses projetos na solução usando os comandos:

  • dotnet sln add src/Contatos.Application/Contatos.Application.csproj
  • dotnet sln add src/Contatos.Infra/Contatos.Infra.csproj
  • dotnet sln add src/Contatos.Web/Contatos.Web.csproj

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:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

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 class ContatoRepository : Repository<Contato>
    {
        public ContatoRepository(AppDbContext context) : base(context)
        {}

        public override Contato GetById(int id)
        {

            var query = _context.Set<Contato>().Where(e => e.Id == id);

            if(query.Any())
                return query.First();

            return null;
        }

        public override IEnumerable<Contato> GetAll()
        {
            var query = _context.Set<Contato>();

            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 class UnitOfWork : IUnitOfWork
    {
        private readonly AppDbContext _context;

        public UnitOfWork(AppDbContext context)
        {
            _context = context;
        }

        public async Task Commit()
        {
            await _context.SaveChangesAsync();
        }
    }
}

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:


José Carlos Macoratti