Boas-vindas a mais um curso de DDD na plataforma Alura! Eu sou Daniel Portugal, desenvolvedor .NET, e serei seu instrutor ao longo deste curso.
Audiodescrição: Daniel se descreve como um homem branco, de cabelos escuros, olhos castanhos, bigode e barba por fazer. Usa óculos de aramação quadrada na cor preta e veste uma camiseta azul-clara. Ao fundo, há um fundo um armário de madeira sob uma iluminação azul.
Como este é um curso mais avançado, será necessário ter conhecimentos prévios para absorver bem o conteúdo. Entre esses conhecimentos, estão Orientação a Objetos e a linguagem C#, que devem estar bem compreendidos. Além disso, é importante ter conhecimento sobre persistência de banco de dados, principalmente SQL Server, com Entity Framework Core, pois utilizaremos bastante essas ideias para implementar as soluções discutidas aqui.
Também é necessário ter experiência com a implementação de API usando o AspNet Core, especificamente com o Minimal API, além de APIs seguras, autenticação e autorização com o Access Token. Já teremos uma solução pronta para isso.
Outro ponto importante é conhecer os princípios de arquitetura limpa e os conceitos do DDD estratégico, que complementam o DDD tático, principalmente bounded context, subdomínios e seus tipos, além de linguagem ubíqua.
Vamos complementar todo esse conhecimento prévio finalizando a parte do DDD estático. Vamos aprender mais sobre design de código, implementando o domínio do sistema e utilizando os padrões do DDD tático.
Além disso, aprenderemos a integrar os contextos de forma assíncrona e desacoplada, protegendo a linguagem ubíqua por meio do sistema de mensagens. Também conheceremos todos os cenários e situações onde aplicaremos determinados padrões, o que tornará nossas regras de negócio e a consistência de dados mais organizadas e flexíveis.
Tudo isso será feito através de um projeto prático, no qual continuaremos evoluindo a API da empresa de aluguel de containers, a ContainRs. Neste contexto, fazemos parte do time de desenvolvimento que continuará trabalhando com essa API.
Não devemos nos ater apenas aos vídeos, pois há bastante conteúdo interessante no curso, especialmente nas atividades onde aprofundaremos alguns conceitos, como os "Para Saber Mais". Teremos atividades que nos desafiarão a explorar e avançar mais no conhecimento, implementando aspectos que não foram abordados no curso.
Também é importante aproveitar os recursos da plataforma, como o fórum, onde podemos tirar dúvidas e conversar, além do servidor do Discord da Alura, onde podemos esclarecer mais questões.
Vamos em frente?
Vamos apresentar o projeto do curso, que já é um velho conhecido nosso: a API da ContainRs, uma empresa de locação de containers. Essa API suporta um aplicativo de autoatendimento para os clientes.
O projeto já está organizado segundo o DDD estratégico. Isso significa que já temos, no nosso gerenciador de soluções, os projetos organizados em seus contextos delimitadores.
Dentro do Visual Studio, no explorador à esquerda, temos o gerenciador de soluções. Dentro da pasta contextos
, estão todos os contextos delimitadores encontrados até agora: ContainRs.Clientes
, ContainRs.Engenharia
, ContainRs.Financeiros
e ContainRs.Vendas
.
No projeto de vendas, com o contexto de vendas, temos os subdomínios de locações e propostas. Dentro de cada subdomínio, encontramos os conceitos e regras de negócio, além dos endpoints.
Para exemplificar, deixamos aberto o conteúdo do arquivo PropostasEndpoints.cs
, que contém os endpoints de propostas. Cada contexto possui sua linguagem ubíqua, ou seja, sua linguagem de negócio associada. Assim, cada contexto tem seus subdomínios e linguagem ubíqua. O arquivo que contém os endpoints de propostas está organizado de acordo com a seguinte separação:
namespace ContainRs.Vendas.Propostas;
public static class Propostas Endpoints
Primeiro, temos o namespace PropostasEndpoints
. Esse namespace está estruturado em segmentos, onde:
PropostasEndpoints
.Como mencionado anteriormente, o subdomínio é responsável por abrigar tanto os conceitos e regras de negócio quanto os próprios endpoints.
O que está faltando, e que vamos começar a estudar, é definir melhor quais são as funções e casos de uso em cada subdomínio. Um subdomínio é uma área funcional com atividades de negócios específicas.
No exemplo de propostas, temos atividades como enviar, aprovar, recusar e colocar comentários em uma proposta. Essas atividades de negócios são relevantes tanto para a pessoa cliente quanto para a equipe de vendas.
Identificar a atividade de aprovar proposta no código pode não ser tão simples, especialmente se não tivermos um entendimento claro do que cada endpoint representa. Se descermos até a linha 23 do arquivo, vemos o mapeamento do endpoint responsável por essa ação. O nome do método é MapPatchAcceptProposta
.
group
.MapPostProposta()
.MapGetPropostas()
.MapGetPropostaById()
.MapPatchAcceptProposta()
.MapPatchRejectProposta()
.MapPostComentarioProposta()
Perceba o desalinhamento: enquanto no negócio falamos em aprovar proposta, no código usamos MapPatchAcceptProposta
. Isso mostra a falta de conexão entre a linguagem do negócio e a forma como ela é representada no código. Nosso objetivo é justamente alinhar esses dois mundos. O código precisa refletir a linguagem do negócio de forma clara e direta.
Voltando ao tema da arquitetura limpa: nela, temos a camada de aplicação, responsável por representar os casos de uso do sistema. E é isso que está faltando aqui: o caso de uso aprovar proposta precisa ser representado como um tipo no .NET. Vamos começar a construir isso agora.
AprovarProposta
Vamos criar uma classe chamada AprovarProposta
no subdomínio de propostas. No gerenciador de soluções, clicamos com o botão direito na pasta Propostas
e adicionamos uma classe chamada AprovarProposta
. Após adicionar, tornamos a classe pública.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ContainRs.Vendas.Propostas
{
public class AprovarProposta
{
}
}
O conteúdo dessa classe será tudo que o caso de uso precisa para ser executado. Para aprovar uma proposta, precisamos de duas informações: uma propriedade do tipo Guid
, que é o identificador do pedido relacionado à proposta, e outra propriedade também do tipo Guid
, que identifica a proposta em si.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ContainRs.Vendas.Propostas
{
public class AprovarProposta
{
public Guid IdPedido { get; set; }
public Guid IdProposta { get; set; }
}
}
AprovarProposta
Um objeto do tipo AprovarProposta
precisa obrigatoriamente dessas duas informações. Então, o que faremos é criar um construtor que exija esses dois dados como argumentos.
Para isso, removeremos a diretiva set
nas linhas 11 e 12 e selecionaremos essas duas propriedades. Em seguida, pressionamos "Ctrl + ." (ponto) e aceitamos a sugestão do Visual Studio para gerar o construtor automaticamente.
Com isso, o construtor já exige os dois argumentos no momento da criação do objeto. Ou seja, sempre que alguém instanciar um AprovarProposta
, será obrigado a fornecer essas informações, o que é excelente para garantir a integridade dos dados.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ContainRs.Vendas.Propostas
{
public class AprovarProposta
{
public AprovarProposta(Guid idPedido, Guid idProposta)
{
IdPedido = idPedido;
IdProposta = idProposta;
}
public Guid IdPedido { get; }
public Guid IdProposta { get; }
}
}
Agora, vamos utilizar esse caso de uso. Salvamos tudo e voltamos para a aba PropostaEndpoints
. No método MapPatchAcceptProposta
, faremos as mudanças necessárias. Na linha 23, pressionamos "Ctrl" e clicamos no método, que se tornará um link, direcionando-nos para a declaração do método MapPatchAcceptProposta
, localizado na linha 119.
Neste ponto, o que precisamos fazer é criar o caso de uso, do tipo AprovarProposta
, e passar as duas informações necessárias: ID do pedido e ID da proposta. O próprio editor já sugere isso para nós.
builder.MapPatch("{id:guid}/proposals/{propostaId:guid}/accept", async (
[FromRoute] Guid id,
[FromRoute] Guid propostaId,
[FromServices] IRepository<Proposta> repoProposta,
[FromServices] IRepository<Locacao> repoLocacao) =>
{
var casoUso = new AprovarProposta(id, propostaId);
});
Criamos o objeto, mas agora precisamos executá-lo. Ou seja, chamar o método responsável por essa ação. Ainda não temos o método Executar
, então vamos criá-lo agora.
Vamos selecionar o trecho de código do endpoint, da linha 130 até a 152, e recortar com "Ctrl + Shift". Em seguida, vamos até a classe AprovarProposta
, pulamos uma linha na altura da linha 18, e criamos o método public void Executar()
. Em seguida, basta colar o código dentro desse método.
Código omitido;.
Com isso, naturalmente surgirão alguns erros de compilação, e nós vamos corrigindo todos eles passo a passo, sem problema.
Recapitulando: estamos começando a explicitar as atividades de negócio do subdomínio de propostas. Criamos uma classe que representa uma dessas atividades, AprovarProposta
, instanciamos esse objeto no endpoint, e agora centralizamos a lógica da execução dentro do caso de uso.
Essa solução segue um padrão bastante conhecido chamado Command (comando). O padrão Command consiste em criar um tipo que representa uma função ou ação no sistema, geralmente relacionada a uma mudança de estado. E foi exatamente isso que fizemos: criamos nosso primeiro Command, o AprovarProposta
.
A execução completa desse caso de uso será o nosso próximo passo!
Criamos nosso primeiro caso de uso, chamado AprovarProposta
, utilizando o padrão Command. Agora, precisamos trabalhar na sua execução. Sabemos que não é agradável lidar com muitos erros de compilação, então vamos resolver isso.
Primeiramente, transferimos o código que estava no endpoint para a classe AprovarProposta
. O retorno do método não será void
, pois ele será assíncrono. O nome do método precisa seguir a convenção de métodos assíncronos, adicionando o sufixo async
ao final. Essa é apenas uma convenção e não resolve erros de compilação, mas é importante segui-la. Para ser um método assíncrono, o retorno precisa ser uma task
, e retornaremos o próprio objeto do tipo proposta. Esse retorno pode ser nulo.
Vamos começar a definir a estrutura do nosso método assíncrono. Inicialmente, criamos a assinatura do método ExecutarAsync
que retornará uma Task<Proposta?>
.
public async Task<Proposta?> ExecutarAsync()
Como o método é assíncrono e já utilizamos await
na linha 24 para uma operação assíncrona, precisamos anotar o método com a palavra reservada async
. Isso já reduz alguns erros. Na linha 28, se a proposta for nula, retornamos nulo, eliminando mais um erro.
public async Task<Proposta?> ExecutarAsync()
{
var proposta = await repoProposta
.GetFirstAsync(
p => p.Id == propostaId && p.SolicitacaoId == id,
p => p.Id);
if (proposta is null) return null;
// Código omitido
Restam dois tipos de erro: um relacionado à variável repoProposta
, que é o repositório de proposta, e outro na linha 44, relacionado à variável repoLocacao
, que é o repositório de locação.
Para resolver esses erros, precisamos declarar todas as informações necessárias para a execução do caso de uso. Podemos ter uma propriedade do tipo repositório de proposta, chamada repoProposta
, e fazer o mesmo para o repositório de locação.
public Guid IdPedido { get; }
public Guid IdProposta { get; }
public IRepository<Proposta> repoProposta { get; set; }
É importante refletir sobre o que realmente é necessário para aprovar uma proposta. Precisamos de um ID do pedido e um ID da proposta, além dos repositórios de proposta e locação. No entanto, ao falar com uma pessoa vendedora, ela apenas precisa saber qual é a proposta e vincular o pedido que originou essa proposta. Repositórios não fazem parte do negócio, mas sim da solução técnica.
Vamos criar uma classe que representa a solução para o problema de aprovar a proposta. O método executarSync
não ficará na classe AprovarProposta
; será separado em outra classe. Essa nova classe ficará na pasta Propostas
. Vamos adicionar uma classe que será representada por uma interface chamada IPropostaService
, que servirá todas as funções relacionadas à proposta.
Dentro dessa interface, criamos a assinatura do método que retorna uma proposta, que pode ser nula. O argumento desse método será o objeto do tipo AprovarProposta
, que chamaremos de comando
, para lembrar que é um Command. Todas as funções relacionadas a esse subdomínio se tornarão métodos nessa interface.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ContainRs.Vendas.Propostas;
public interface IPropostaService
{
Task<Proposta?> AprovarAsync(AprovarProposta comando);
}
Para implementar essa interface, deixaremos o código no arquivo PropostaService
, mas é possível separar IPropostaService
e PropostaService
, que é a implementação da interface. Assim, teremos uma classe que implementa IPropostaService
.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ContainRs.Vendas.Propostas
public interface IPropostaService
{
Task<Proposta?> AprovarAsync(AprovarProposta comando);
}
public class PropostaService : IPropostaService
{
public Task<Proposta?> AprovarAsync(AprovarProposta comando)
{
throw new NotImplementedException();
}
}
Agora, vamos voltar ao AprovarProposta
, selecionar da linha 26 até a linha 48 e teclar "Ctrl + X" para recortar. Em seguida, colamos no lugar da linha throw new NotImplementedException()
, no código de IPropostaService
. Após colar, vamos anotar esse método com a palavra reservada async
, para representar que estamos executando esse método de forma assíncrona.
Precisamos criar um repositório de proposta, que chamaremos de repoProposta
, para facilitar. Na linha seguinte, vamos criar um repositório de locação, chamado repoLocacao
, para simplificar na hora de substituir.
public class PropostaService : IPropostaService
{
private readonly IRepository<Proposta> repoProposta;
private readonly IRepository<Locacao> repoLocacao;
public async Task<Proposta?> AprovarAsync(AprovarProposta comando)
{
var proposta = await repoProposta
.GetFirstAsync(
p => p.Id == propostaId && p.SolicitacaoId == id,
p => p.Id);
if (proposta is null) return null;
proposta.Situacao = SituacaoProposta.Aceita;
// Código omitido
Esses dois campos não são propriedades, mas sim campos da classe PropostaService
. Vamos selecioná-los e pedi-los no construtor, seguindo a mesma lógica. Para ter um objeto do tipo PropostaService
, precisamos, necessariamente, desses dois repositórios.
public class PropostaService : IPropostaService
{
private readonly IRepository<Proposta> repoProposta;
private readonly IRepository<Locacao> repoLocacao;
public PropostaService(IRepository<Proposta> repoProposta, IRepository<Locacao> repoLocacao)
{
this.repoProposta = repoProposta;
this.repoLocacao = repoLocacao;
}
// Código omitido
No método AprovarAsync
, agora na execução, o repositório já está funcionando. Está ocorrendo um erro nos dois IDs, pois eles não estão mais vindo de uma variável propostaId
, mas sim de um comando, IdProposta
, e a solicitação vem do comando IdPedido
.
public async Task<Proposta?> AprovarAsync(AprovarProposta comando)
{
var proposta = await repoProposta
.GetFirstAsync(
p => p.Id == comando.IdProposta && p.SolicitacaoId == IdPedido,
p => p.Id);
if (proposta is null) return null;
proposta.Situacao = SituacaoProposta.Aceita;
// Código omitido
Quando a proposta não é encontrada, ela retorna nulo. Após tudo isso, precisamos retornar a proposta em si. Agora, não temos mais erro de compilação na execução da aprovação, através do método AprovarAsync
.
// Código omitido
await repoProposta.UpdateAsync(proposta);
await repoLocacao.AddAsync(locacao);
scope.Complete();
return proposta;
Precisamos fazer as correções necessárias nas outras classes. No AprovarProposta
, não precisamos mais do repositório, então a linha 22 (public IRepository<Proposta> repoProposta { get; set; }
) será excluída, assim como a execução. Vamos excluir o método ExecutarAsync
e as linhas restantes, de 22 e a 27.
No tratamento do endpoint, não vamos mais chamar o executar no caso de uso. Em vez de pedir os repositórios como argumento desse método, vamos pedir aquele serviço. Vamos chamar o IPropostaService
de service
. Agora, faremos um await service.AprovarAsync
passando o caso de uso. Pode ser chamado de caso de uso, mas se preferir, pode chamar de comando também.
Para finalizar, precisamos criar uma variável do tipo proposta que recebe o resultado da execução do AprovarAsync
e verificar se essa proposta é nula. Se for nula, retornaremos um NotFound
.
builder.MapPatch("{id:guid}/proposals/{propostaId:guid}/accept", async (
[FromRoute] Guid id,
[FromRoute] Guid propostaId,
[FromServices] IPropostaService service
) =>
{
var casoUso = new AprovarProposta(id, propostaId);
var proposta = await service.AprovarAsync(casoUso);
if (proposta is null) return Results.NotFound();
return Results.Ok(PropostaResponse.From(proposta));
})
Agora, temos o tratamento do endpoint, que basicamente delega a responsabilidade desse endpoint para o comando e para o serviço, o PropostaService
.
Para finalizar, já que estamos pedindo um objeto do tipo IPropostaService
dentro do serviço, com a anotação de FromServices
, precisamos injetá-lo no container de injeção de dependência.
Vamos abrir o arquivo Program.cs
no projeto ContainRs.Api
. No conteúdo, na linha 30, vamos pular uma linha e adicionar mais um serviço do tipo Scoped
, que ficará dentro da requisição, especificamente. Será uma implementação para o IPropostaService
, no caso, o próprio PropostaService
: builder.Services.AddScoped<IPropostaService, PropostaService>();
.
builder.Services.AddScoped<IRepository<Cliente>, ClienteRepository>();
builder.Services.AddScoped<IRepository<PedidoLocacao>, SolicitacaoRepository>();
builder.Services.AddScoped<IRepository<Proposta>, PropostaRepository>();
builder.Services.AddScoped<IRepository<Locacao>, LocacaoRepository>();
builder.Services.AddScoped<IRepository<Conteiner>, ConteinerRepository>();
builder.Services.AddScoped<IPropostaService, PropostaService>();
builder.Services.AddScoped<IAcessoManager, AcessoManagerWithIdentity>();
Conhecemos mais um padrão. Esse serviço é um padrão relacionado a servir as funções da camada de aplicação, chamado de Application Service. Será uma interface com métodos para todas as funções relacionadas àquela categoria. Portanto, PropostaService
terá funções que atenderão à atividade de negócio da proposta.
Esse processo que vimos neste vídeo, de separar o problema da solução em classes diferentes, é o assunto deste curso. Anteriormente, conhecemos o DDD Estratégico. Neste curso, falaremos do DDD Tático.
No DDD Estratégico, focamos no problema, no que precisamos resolver, com uma preocupação em estar alinhados à linguagem do negócio. Falamos principalmente das atividades de negócio, como aprovação de proposta, emissão de fatura, pedido de locação, tudo isso dentro do problema e alinhado à linguagem do negócio.
No DDD Tático, focaremos na solução, em como resolver esses problemas. Vamos discutir padrões, tecnologias, ferramentas e bibliotecas. Já começamos a falar de alguns padrões, como o padrão repositório, padrão Command, e o padrão Application Service, tudo isso para tentar solucionar os problemas levantados no DDD Estratégico.
Esse é o DDD Tático, e falaremos muito mais sobre isso nos próximos vídeos. Nos vemos lá!
O curso Arquitetura .NET: modelando aplicações com Domain-Driven Design Tático possui 180 minutos de vídeos, em um total de 50 atividades. Gostou? Conheça nossos outros cursos de .NET em Programação, ou leia nossos artigos de Programação.
Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:
Impulsione a sua carreira com os melhores cursos e faça parte da maior comunidade tech.
1 ano de Alura
Assine o PLUS e garanta:
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
No Discord, você tem acesso a eventos exclusivos, grupos de estudos e mentorias com especialistas de diferentes áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Acelere o seu aprendizado com a IA da Alura e prepare-se para o mercado internacional.
1 ano de Alura
Todos os benefícios do PLUS e mais vantagens exclusivas:
Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos, corrige exercícios e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com a Luri até 100 mensagens por semana.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Transforme a sua jornada com benefícios exclusivos e evolua ainda mais na sua carreira.
1 ano de Alura
Todos os benefícios do PRO e mais vantagens exclusivas:
Mensagens ilimitadas para estudar com a Luri, a IA da Alura, disponível 24hs para tirar suas dúvidas, dar exemplos práticos, corrigir exercícios e impulsionar seus estudos.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.