Para implementar um pacote conceitual de Compras em C# com EF Core, usamos os conceitos de Entidade Agregada (onde Compra é a raiz e CompraItem é um item dependente). Usaremos Owned Entity Types (Entidades Proprietárias) e o padrão Repository/Gateway para isolar o acesso aos dados. [1, 2]
1. Modelo de Domínio (Entidades e Interfaces)
Dentro do namespace do nosso pacote, definimos as classes principais e a interface do repositório, garantindo a separação de responsabilidades.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MeuProjeto.Compras
{
// 1. Entidade Raiz (Aggregate Root)
public class Compra
{
public Guid Id { get; private set; }
public DateTime DataCompra { get; private set; }
public decimal ValorTotal { get; private set; }
// Propriedade de Navegação para itens (Encapsulada)
private readonly List<CompraItem> _itens = new();
public IReadOnlyCollection<CompraItem> Itens => _itens.AsReadOnly();
// Construtor protegido exigido pelo EF Core
protected Compra() { }
public Compra(DateTime dataCompra)
{
Id = Guid.NewGuid();
DataCompra = dataCompra;
ValorTotal = 0;
}
public void AdicionarItem(string produto, int quantidade, decimal precoUnitario)
{
var item = new CompraItem(produto, quantidade, precoUnitario);
_itens.Add(item);
CalcularValorTotal();
}
private void CalcularValorTotal()
{
decimal total = 0;
foreach (var item in Itens)
{
total += item.Quantidade * item.PrecoUnitario;
}
ValorTotal = total;
}
}
// 2. Entidade Dependente (Value Object / Owned Entity)
public class CompraItem
{
public string Produto { get; private set; }
public int Quantidade { get; private set; }
public decimal PrecoUnitario { get; private set; }
protected CompraItem() { }
public CompraItem(string produto, int quantidade, decimal precoUnitario)
{
Produto = produto;
Quantidade = quantidade;
PrecoUnitario = precoUnitario;
}
}
// 3. Interface do Gateway / Repositório
public interface ICompraRepository
{
Task<Compra> ObterPorIdAsync(Guid id);
Task AdicionarAsync(Compra compra);
Task SalvarAsync();
}
}
2. Configuração do EF Core e Mapeamento
Usamos a configuração do EF Core para mapear CompraItem como uma entidade contida (agregada) em Compra, garantindo que não precise de uma chave primária própria. [1, 3]
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace MeuProjeto.Compras
{
// 4. Configuração Fluent API
public class CompraConfiguration : IEntityTypeConfiguration<Compra>
{
public void Configure(EntityTypeBuilder<Compra> builder)
{
builder.HasKey(c => c.Id);
builder.Property(c => c.DataCompra)
.IsRequired();
builder.Property(c => c.ValorTotal)
.HasColumnType("decimal(18,2)");
// Configurando CompraItem como Owned Entity Type (Owned Collection)
builder.OwnsMany(c => c.Itens, item =>
{
item.WithOwner().HasForeignKey("CompraId");
item.Property<int>("Id"); // Chave sombra implícita
item.HasKey("Id");
item.Property(i => i.Produto)
.IsRequired()
.HasMaxLength(150);
item.Property(i => i.PrecoUnitario)
.HasColumnType("decimal(18,2)");
});
// Exemplo de configuração de navegação como Backing Field
builder.Metadata.FindNavigation(nameof(Compra.Itens))
.SetPropertyAccessMode(PropertyAccessMode.Field);
}
}
}
3. Implementação do Gateway / Repositório
Esta classe implementa a interface de gateway para interagir com o
Entity Framework Core, isolando o restante do sistema dos detalhes de persistência.
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace MeuProjeto.Compras
{
public class CompraRepository : ICompraRepository
{
private readonly DbContext _context;
public CompraRepository(DbContext context)
{
_context = context;
}
public async Task<Compra> ObterPorIdAsync(Guid id)
{
return await _context.Set<Compra>()
.Include(c => c.Itens)
.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task AdicionarAsync(Compra compra)
{
await _context.Set<Compra>().AddAsync(compra);
}
public async Task SalvarAsync()
{
await _context.SaveChangesAsync();
}
}
}
Para manter os pacotes Compras e Catalogo estritamente desacoplados — seguindo os princípios de Design Orientado a Domínio (DDD) e diagramação em camadas por pacotes —, o pacote Compras nunca deve referenciar as classes Produto, Categoria ou Fabricante diretamente.
Em vez disso, a entidade CompraItem armazena apenas o ProdutoId como um identificador primitivo (um Value Object ou uma propriedade simples).
Aqui está a implementação dessa arquitetura em C# com EF Core:
1. Pacote Catálogo (Independente)
Este pacote gerencia o ciclo de vida dos produtos. Ele não sabe que o pacote de compras existe.
namespace MeuProjeto.Catalogo
{
public class Fabricante
{
public Guid Id { get; private set; }
public string Nome { get; private set; }
}
public class Categoria
{
public Guid Id { get; private set; }
public string Nome { get; private set; }
}
public class Produto
{
public Guid Id { get; private set; }
public string Nome { get; private set; }
public decimal PrecoAtual { get; private set; }
public Guid FabricanteId { get; private set; }
public Guid CategoriaId { get; private set; }
}
}
2. Pacote Compras (Desacoplado via Identificadores)
A entidade CompraItem agora faz referência ao catálogo utilizando apenas o ProdutoId. O preço unitário é copiado no momento da compra para garantir o histórico financeiro, mesmo que o produto mude de valor no catálogo futuramente.
using System;
using System.Collections.Generic;
namespace MeuProjeto.Compras
{
public class Compra
{
public Guid Id { get; private set; }
public DateTime DataCompra { get; private set; }
public decimal ValorTotal { get; private set; }
private readonly List<CompraItem> _itens = new();
public IReadOnlyCollection<CompraItem> Itens => _itens.AsReadOnly();
protected Compra() { }
public Compra(DateTime dataCompra)
{
Id = Guid.NewGuid();
DataCompra = dataCompra;
ValorTotal = 0;
}
// Passamos o ProdutoId externamente para realizar a associação
public void AdicionarItem(Guid produtoId, int quantidade, decimal precoUnitario)
{
var item = new CompraItem(produtoId, quantidade, precoUnitario);
_itens.Add(item);
CalcularValorTotal();
}
private void CalcularValorTotal()
{
decimal total = 0;
foreach (var item in Itens)
{
total += item.Quantidade * item.PrecoUnitario;
}
ValorTotal = total;
}
}
public class CompraItem
{
// Chave externa lógica (Sem chave estrangeira física do banco ligando tabelas de pacotes diferentes)
public Guid ProdutoId { get; private set; }
public int Quantidade { get; private set; }
public decimal PrecoUnitario { get; private set; }
protected CompraItem() { }
public CompraItem(Guid produtoId, int quantidade, decimal precoUnitario)
{
ProdutoId = produtoId;
Quantidade = quantidade;
PrecoUnitario = precoUnitario;
}
}
}
3. Integração Desacoplada (O Gateway/Service de Integração)
Para que a camada de aplicação possa validar se o ProdutoId enviado existe ou buscar dados adicionais do catálogo (como o preço real do produto), usamos uma interface que atua como Gateway de Integração (ou Anti-Corruption Layer).
using System;
using System.Threading.Tasks;
namespace MeuProjeto.Compras
{
// Interface dentro do pacote Compras que define o que ele precisa do Catálogo
public interface ICatalogoGateway
{
Task<bool> ProdutoExisteAsync(Guid produtoId);
Task<decimal> ObterPrecoProdutoAsync(Guid produtoId);
}
}
A implementação concreta dessa interface consome o pacote Catalogo sem misturar as entidades de persistência no banco de dados.
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MeuProjeto.Catalogo; // Importado apenas na infraestrutura de integração
namespace MeuProjeto.Infraestrutura.Integracao
{
public class CatalogoGateway : Compras.ICatalogoGateway
{
private readonly DbContext _context;
public CatalogoGateway(DbContext context)
{
_context = context;
}
public async Task<bool> ProdutoExisteAsync(Guid produtoId)
{
return await _context.Set<Produto>().AnyAsync(p => p.Id == produtoId);
}
public async Task<decimal> ObterPrecoProdutoAsync(Guid produtoId)
{
var produto = await _context.Set<Produto>()
.FirstOrDefaultAsync(p => p.Id == produtoId);
return produto?.PrecoAtual ?? throw new Exception("Produto não encontrado.");
}
}
}
4. Configuração do EF Core
No mapeamento da Fluent API do pacote Compras, não haverá nenhuma declaração de .HasOne<Produto>(). O EF Core tratará o ProdutoId apenas como um campo comum de tipo Guid.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace MeuProjeto.Compras
{
public class CompraConfiguration : IEntityTypeConfiguration<Compra>
{
public void Configure(EntityTypeBuilder<Compra> builder)
{
builder.HasKey(c => c.Id);
builder.Property(c => c.ValorTotal).HasColumnType("decimal(18,2)");
builder.OwnsMany(c => c.Itens, item =>
{
item.WithOwner().HasForeignKey("CompraId");
item.Property<int>("Id");
item.HasKey("Id");
// Mapeado apenas como um identificador numérico, sem restrição de FK rígida
item.Property(i => i.ProdutoId)
.IsRequired();
item.Property(i => i.PrecoUnitario)
.HasColumnType("decimal(18,2)");
});
}
}
}
Diminuindo o Acoplamento entre os Pacotes
Você está coberto de razão. No design de software modular ou em microsserviços, cada pacote (ou contexto delimitado) deve ser dono exclusivo dos seus dados. Permitir que o pacote de Compras consulte as tabelas do Catalogo via banco de dados quebraria o isolamento e criaria um acoplamento oculto e perigoso.
Ao usar um gateway que consome uma API via HttpClient, o pacote Compras consome apenas um contrato (um JSON público) e não se importa se o Catalogo usa SQL Server, MongoDB ou se mudou a estrutura interna de suas tabelas.
Veja abaixo como reestruturar o CatalogoGateway para utilizar o HttpClient:
1. O Contrato de Resposta (DTO) no Pacote Compras
O pacote de compras cria o seu próprio modelo para receber esses dados, mantendo-se agnóstico às entidades reais de banco do catálogo.
namespace MeuProjeto.Compras
{
// DTO focado apenas no que o contexto de Compras precisa saber
public record ProdutoCatalogoDto(Guid Id, string Nome, decimal Preco, int EstoqueAtual);
public interface ICatalogoGateway
{
Task<ProdutoCatalogoDto?> ObterProdutoAsync(Guid produtoId);
}
}
2. Implementação do Gateway com HttpClient
Esta classe converte a chamada de domínio em uma requisição HTTP real para o serviço de catálogo.
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace MeuProjeto.Infraestrutura.Gateways
{
public class CatalogoHttpGateway : Compras.ICatalogoGateway
{
private readonly HttpClient _httpClient;
// O HttpClient é injetado já pré-configurado com a URL base do microsserviço/módulo de catálogo
public CatalogoHttpGateway(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<Compras.ProdutoCatalogoDto?> ObterProdutoAsync(Guid produtoId)
{
try
{
// Faz a chamada HTTP para o endpoint do pacote de Catálogo
var resposta = await _httpClient.GetAsync($"api/produtos/{produtoId}");
if (resposta.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
resposta.EnsureSuccessStatusCode();
// Desserializa diretamente para o DTO esperado pelo pacote Compras
return await resposta.Content.ReadFromJsonAsync<Compras.ProdutoCatalogoDto>();
}
catch (HttpRequestException)
{
// Tratamento de resiliência (ex: log, fallback ou retransmitir erro controlado)
throw new ApplicationException("Não foi possível conectar ao serviço de Catálogo.");
}
}
}
}
3. Exemplo de Uso na Camada de Aplicação (Caso de Uso)
Veja como as peças se encaixam de forma assíncrona e segura ao criar uma nova compra:
using System;
using System.Threading.Tasks;
namespace MeuProjeto.Compras.Aplicacao
{
public class CriarCompraCommandHandler
{
private readonly ICompraRepository _compraRepository;
private readonly ICatalogoGateway _catalogoGateway;
public CriarCompraCommandHandler(ICompraRepository compraRepository, ICatalogoGateway catalogoGateway)
{
_compraRepository = compraRepository;
_catalogoGateway = catalogoGateway;
}
public async Task ExecutarAsync(Guid produtoId, int quantidade)
{
// 1. Valida a existência e obtém dados em tempo real via HTTP Gateway
var produto = await _catalogoGateway.ObterProdutoAsync(produtoId);
if (produto == null)
throw new Exception("Produto inexistente no catálogo.");
// 2. Valida regras de negócio do contexto de compras baseadas no estoque remoto
if (produto.EstoqueAtual < quantidade)
throw new Exception("Quantidade solicitada indisponível no estoque.");
// 3. Executa as operações na entidade de domínio
var novaCompra = new Compra(DateTime.UtcNow);
// Registra o item com o preço histórico retornado pelo catálogo
novaCompra.AdicionarItem(produto.Id, quantidade, produto.Preco);
// 4. Salva apenas a Compra no banco de dados local do pacote Compras
await _compraRepository.AdicionarAsync(novaCompra);
await _compraRepository.SalvarAsync();
}
}
}
4. Registro da Injeção de Dependência (Program.cs)
Usamos o padrão IHttpClientFactory do .NET para gerenciar o ciclo de vida dos sockets e configurar o Gateway:
using System;
using Microsoft.Extensions.DependencyInjection;
using MeuProjeto.Compras;
using MeuProjeto.Infraestrutura.Gateways;
var builder = WebApplication.CreateBuilder(args);
// Configura o HttpClient especificamente para o Gateway do Catálogo
builder.Services.AddHttpClient<ICatalogoGateway, CatalogoHttpGateway>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Urls:CatalogoApi"] ?? "https://catalogo.local");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(5); // Fail-fast para chamadas síncronas entre pacotes
});
// Registra os demais componentes
builder.Services.AddScoped<ICompraRepository, CompraRepository>();
builder.Services.AddScoped<CriarCompraCommandHandler>();
Se quiser, posso te mostrar como evoluir este cenário adicionando políticas de resiliência como Retentativas automáticas (Retry) e Circuit Breaker usando a biblioteca Polly, ou como disparar um Evento de Domínio via mensageria para dar baixa no estoque do catálogo após a compra ser finalizada.
Para implementar políticas de Retry (Retentativas) e Circuit Breaker (Disjuntor) no .NET 8 ou superior, a abordagem padrão e moderna utiliza o pacote oficial Microsoft.Extensions.Http.Resilience combinado com o Polly v8.
Essa biblioteca integra-se nativamente ao IHttpClientFactory, permitindo configurar a resiliência diretamente onde o serviço é registrado, sem poluir o código do seu Gateway.
1. Instalação do Pacote Nuget
Primeiro, adicione a extensão de resiliência HTTP ao seu projeto de infraestrutura:
dotnet add package Microsoft.Extensions.Http.Resilience
2. Configuração no Program.cs
A maneira mais limpa de aplicar essas políticas é através do método AddStandardResilienceHandler(). Ele configura automaticamente um pipeline de resiliência otimizado contendo: Rate Limiter → Total Timeout → Circuit Breaker → Attempt Timeout → Retry.
Veja como configurar as regras específicas de Retry e Circuit Breaker para o seu CatalogoHttpGateway:
using System;
using Microsoft.Extensions.DependencyInjection;
using HttpRoute = Polly.Simmy; // Opcional, para testes de falha
using MeuProjeto.Compras;
using MeuProjeto.Infraestrutura.Gateways;
var builder = WebApplication.CreateBuilder(args);
// 1. Configura o HttpClient e injeta as políticas de resiliência padrão do Polly
builder.Services.AddHttpClient<ICatalogoGateway, CatalogoHttpGateway>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Urls:CatalogoApi"] ?? "https://catalogo.local");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddStandardResilienceHandler(options =>
{
// ---- 2. CONFIGURAÇÃO DA POLÍTICA DE RETENTATIVAS (RETRY) ----
options.Retry.MaxRetryAttempts = 3; // Tenta até 3 vezes antes de desistir
options.Retry.Delay = TimeSpan.FromMilliseconds(200); // Tempo de espera inicial entre as tentativas
options.Retry.BackoffType = Polly.DelayBackoffType.Exponential; // Aumenta o tempo exponencialmente (200ms, 400ms, 800ms...)
options.Retry.UseJitter = true; // Adiciona variação aleatória para evitar sobrecarga simultânea no servidor (Thundering Herd)
// ---- 3. CONFIGURAÇÃO DO DISJUNTOR (CIRCUIT BREAKER) ----
options.CircuitBreaker.FailureRatio = 0.5; // Abre o circuito se 50% das requisições falharem...
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10); // ...dentro deste intervalo de 10 segundos
options.CircuitBreaker.MinimumThroughput = 8; // Só avalia a regra se houver no mínimo 8 requisições nesse tempo
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30); // Mantém o circuito aberto (bloqueando chamadas) por 30 segundos
});
// Registra os repositórios e comandos normalmente
builder.Services.AddScoped<ICompraRepository, CompraRepository>();
Como essa arquitetura protege o pacote de Compras?
- O Retry atua em instabilidade rápida: Se a rede oscilar ou o pacote
Catalogo sofrer um breve pico de CPU e retornar um erro HTTP 5xx, o Polly intercepta a falha e repete a chamada invisivelmente. O usuário final nem percebe que houve uma falha interna na primeira tentativa. - O Circuit Breaker atua em falhas sistêmicas: Se o pacote
Catalogo cair completamente, o Circuit Breaker entra no estado Aberto. Em vez de travar o processo de Compras esperando o timeout de rede em cada clique de cliente, ele rejeita as chamadas instantaneamente disparando uma BrokenCircuitException. Isso preserva recursos de memória e threads do seu próprio servidor. [1]
4. Tratando a Exceção do Circuito no Caso de Uso
Se o circuito estiver aberto, o seu CatalogoHttpGateway lançará um erro. É uma boa prática capturar isso na sua camada de aplicação para dar uma resposta amigável ou seguir um fluxo alternativo (fallback).
using System;
using System.Threading.Tasks;
using Polly.CircuitBreaker; // Necessário se quiser isolar o tipo exato da exceção
namespace MeuProjeto.Compras.Aplicacao
{
public class CriarCompraCommandHandler
{
private readonly ICompraRepository _compraRepository;
private readonly ICatalogoGateway _catalogoGateway;
public CriarCompraCommandHandler(ICompraRepository compraRepository, ICatalogoGateway catalogoGateway)
{
_compraRepository = compraRepository;
_catalogoGateway = catalogoGateway;
}
public async Task ExecutarAsync(Guid produtoId, int quantidade)
{
try
{
// Este método agora está protegido por Retry e Circuit Breaker
var produto = await _catalogoGateway.ObterProdutoAsync(produtoId);
if (produto == null)
throw new Exception("Produto inexistente no catálogo.");
if (produto.EstoqueAtual < quantidade)
throw new Exception("Quantidade solicitada indisponível no estoque.");
var novaCompra = new Compra(DateTime.UtcNow);
novaCompra.AdicionarItem(produto.Id, quantidade, produto.Preco);
await _compraRepository.AdicionarAsync(novaCompra);
await _compraRepository.SalvarAsync();
}
catch (BrokenCircuitException)
{
// Fallback quando o circuito está aberto (o Catálogo está fora do ar)
throw new ApplicationException("O sistema de compras está temporariamente indisponível. Por favor, tente novamente em alguns instantes.");
}
}
}
}
Para evoluir a arquitetura para um modelo assíncrono e resiliente, removemos o acoplamento temporal. Após salvar a compra no banco de dados do pacote Compras, disparados um Evento de Domínio (ex: CompraFinalizadaEvent). Um broker de mensageria (como o RabbitMQ) entrega essa mensagem ao pacote Catalogo, que processará a baixa de estoque em segundo plano.
A ferramenta padrão da indústria .NET para abstrair essa mensageria de forma robusta e implementar o padrão Outbox (evitando perder mensagens se o broker cair) é o MassTransit.
1. Instalação dos Pacotes NuGet
Adicione os pacotes necessários ao seu projeto de infraestrutura:
dotnet add package MassTransit.RabbitMQ
dotnet add package MassTransit.EntityFrameworkCore
2. O Evento de Domínio (Contrato Compartilhado)
Os pacotes compartilham apenas um contrato de dados simples (uma interface ou record). Nenhuma classe de entidade é exposta.
using System;
using System.Collections.Generic;
namespace MeuProjeto.Contratos.Eventos
{
// O evento representa um fato que já aconteceu no passado
public record CompraFinalizadaEvent(
Guid CompraId,
DateTime DataCriacao,
List<ItemCompraEfetivadaDto> Itens
);
public record ItemCompraEfetivadaDto(
Guid ProdutoId,
int Quantidade
);
}
3. Disparando o Evento no Caso de Uso de Compras
Usamos a interface IPublishEndpoint do MassTransit para publicar o evento. Graças ao ecossistema do MassTransit, podemos usar o padrão Transactional Outbox, que garante que o evento só seja enviado ao RabbitMQ se a transação do banco de dados da Compra for concluída com sucesso.
using System;
using System.Linq;
using System.Threading.Tasks;
using MassTransit;
using MeuProjeto.Contratos.Eventos;
namespace MeuProjeto.Compras.Aplicacao
{
public class CriarCompraCommandHandler
{
private readonly ICompraRepository _compraRepository;
private readonly ICatalogoGateway _catalogoGateway;
private readonly IPublishEndpoint _publishEndpoint; // Injetado pelo MassTransit
public CriarCompraCommandHandler(
ICompraRepository compraRepository,
ICatalogoGateway catalogoGateway,
IPublishEndpoint publishEndpoint)
{
_compraRepository = compraRepository;
_catalogoGateway = catalogoGateway;
_publishEndpoint = publishEndpoint;
}
public async Task ExecutarAsync(Guid produtoId, int quantidade)
{
// 1. O Gateway HTTP ainda pode ser usado para validar o preço/estoque inicial
var produto = await _catalogoGateway.ObterProdutoAsync(produtoId);
if (produto == null) throw new Exception("Produto inexistente.");
// 2. Cria a entidade de negócio localmente
var novaCompra = new Compra(DateTime.UtcNow);
novaCompra.AdicionarItem(produto.Id, quantidade, produto.Preco);
// 3. Salva a compra no banco de dados local
await _compraRepository.AdicionarAsync(novaCompra);
await _compraRepository.SalvarAsync();
// 4. Publica o evento de domínio assincronamente
var evento = new CompraFinalizadaEvent(
novaCompra.Id,
novaCompra.DataCompra,
novaCompra.Itens.Select(i => new ItemCompraEfetivadaDto(i.ProdutoId, i.Quantidade)).ToList()
);
await _publishEndpoint.Publish(evento);
}
}
}
4. Consumindo o Evento no Pacote Catálogo
No pacote Catalogo (que pode ser outro projeto/deploy executando independentemente), criamos um Consumer. Ele escuta a fila do RabbitMQ e processa a baixa de estoque utilizando o repositório ou banco de dados do próprio catálogo.
using System;
using System.Threading.Tasks;
using MassTransit;
using Microsoft.EntityFrameworkCore;
using MeuProjeto.Contratos.Eventos;
using MeuProjeto.Catalogo; // Acesso permitido apenas no próprio pacote Catalogo
namespace MeuProjeto.Catalogo.Consumidores
{
public class CompraFinalizadaConsumer : IConsumer<CompraFinalizadaEvent>
{
private readonly DbContext _context; // DB Context exclusivo do Catálogo
public CompraFinalizadaConsumer(DbContext context)
{
_context = context;
}
public async Task Consume(ConsumeContext<CompraFinalizadaEvent> context)
{
var evento = context.Message;
foreach (var item in evento.Itens)
{
// Busca o produto no banco do catálogo
var produto = await _context.Set<Produto>()
.FirstOrDefaultAsync(p => p.Id == item.ProdutoId);
if (produto != null)
produto.DeduzirEstoque(item.Quantidade);
}
// Salva as alterações de estoque de forma atômica
await _context.SaveChangesAsync();
}
}
}
Nota: A classe Produto no domínio do Catálogo ganharia um método como DeduzirEstoque(int qtd) { EstoqueAtual -= qtd; } encapsulando a regra.
5. Configuração do Barramento (Program.cs)
No arquivo de inicialização da aplicação, configuramos o MassTransit acoplado ao RabbitMQ e ativamos o padrão Outbox no Entity Framework para garantir consistência de dados (atômico: salva no banco e agenda o envio da mensagem).
using System;
using MassTransit;
using Microsoft.Extensions.DependencyInjection;
using MeuProjeto.Catalogo.Consumidores;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMassTransit(x =>
{
// 1. Registra o consumidor (Usado no projeto que executa o Catálogo)
x.AddConsumer<CompraFinalizadaConsumer>();
// 2. Configura o suporte a transações integradas com o banco de dados (Outbox)
x.AddEntityFrameworkOutbox<DbContext>(o =>
{
o.UseSqlServer(); // Ou UseNpgsql(), UseMySql() conforme seu banco
o.UseBusOutbox(); // Sincroniza o disparo do barramento com o SaveChangesAsync
});
// 3. Configura o transporte via RabbitMQ
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(builder.Configuration["RabbitMQ:Host"] ?? "localhost", "/", h =>
{
h.Username("guest");
h.Password("guest");
});
// Configura automaticamente as filas com base nos consumidores registrados
cfg.ConfigureEndpoints(context);
});
});
Vantagens dessa Abordagem
- Desacoplamento Total: O microsserviço ou pacote de compras não precisa saber como o catálogo atualiza seu estoque, nem se o catálogo está temporariamente fora do ar.
- Resiliência Absoluta (Outbox + RabbitMQ Retry): Se o RabbitMQ cair enquanto a compra é salva, a mensagem fica guardada com segurança na tabela Outbox do SQL. Assim que o broker voltar, a mensagem é despachada. Se o processo do
Catálogo falhar ao processar a mensagem, o RabbitMQ a coloca em uma fila de retentativa (retry queue) ou de erro (dead-letter queue) sem afetar a experiência do cliente que realizou a compra.
Se desejar, posso demonstrar como gerenciar cenários de compensação (Padrão Saga) caso o catálogo descubra, tardiamente, que o estoque furou e a compra precise ser cancelada ou estornada.
A mudança de uma comunicação síncrona (HTTP) para uma comunicação assíncrona baseada em eventos (Mensageria) é considerada uma evolução arquitetural madura na engenharia de software porque resolve o problema do Acoplamento Temporal.
Por que a Mensageria é uma evolução sobre o HTTP?
- Autonomia e Disponibilidade (Sem efeito dominó): No modelo HTTP, se o pacote
Catalogo estiver fora do ar ou lento, o pacote Compras também cai ou fica lento. Com mensageria, se o Catalogo estiver em manutenção, Compras continua vendendo normalmente. As mensagens ficam guardadas com segurança no Broker (ex: RabbitMQ) e serão processadas assim que o Catalogo voltar. [1] - Desempenho e Latência: Uma requisição HTTP obriga o cliente a esperar a jornada completa: Rede -> Validar Compra -> Chamar Catálogo via HTTP -> Atualizar Banco do Catálogo -> Responder HTTP -> Salvar Banco de Compras. Com mensageria, o pacote
Compras apenas grava localmente e dispara o evento. A resposta ao usuário é imediata. [2] - Consistência Eventual sobre Consistência Transacional: Em vez de tentar travar dois sistemas ao mesmo tempo (o que é impossível de garantir sem transações distribuídas complexas de duas fases / 2PC), aceitamos que o estoque será atualizado alguns milissegundos após a compra. [3]
Como integrar os modelos de forma 100% Assíncrona
Para que o desacoplamento seja real, o pacote Compras não pode mais chamar o Catalogo via HTTP para perguntar o preço ou se o produto existe no momento da finalização.
A Solução: O pacote Compras mantém uma tabela local simplificada (uma projeção ou cache de leitura) com os dados básicos dos produtos que ele precisa para vender (Id, Preço, Estoque). Essa tabela é alimentada assincronamente por eventos enviados pelo Catalogo.
1. O Modelo de Dados Isolado (Persistência)
No banco de dados do pacote Compras, além de Compra e CompraItem, criamos uma tabela chamada ProdutoEspelho (ou Read Model). Não há Chave Estrangeira (FK) física entre ela e a tabela real do catálogo.
using System;
using System.Collections.Generic;
namespace MeuProjeto.Compras.Dominio
{
// Entidade local do pacote Compras para consulta rápida
public class ProdutoEspelho
{
public Guid Id { get; private set; }
public decimal Preco { get; private set; }
public int Estoque { get; private set; }
protected ProdutoEspelho() { }
public ProdutoEspelho(Guid id, decimal preco, int estoque)
{
Id = id;
Preco = preco;
Estoque = estoque;
}
public void AtualizarDados(decimal preco, int estoque)
{
Preco = preco;
Estoque = estoque;
}
public void DeduzirEstoqueLocal(int quantidade)
{
Estoque -= quantidade;
}
}
}
2. Atualizando o Espelho quando o Catálogo muda (Consumidor)
Sempre que um produto for criado, alterado ou tiver o estoque reposto no pacote Catalogo, ele publica um evento (ex: ProdutoAtualizadoEvent). O pacote Compras escuta esse evento e atualiza sua tabela local.
// Contrato compartilhado de integração
namespace MeuProjeto.Contratos.Eventos
{
public record ProdutoAtualizadoEvent(Guid Id, decimal Preco, int EstoqueAtual);
}
using System.Threading.Tasks;
using MassTransit;
using Microsoft.EntityFrameworkCore;
using MeuProjeto.Contratos.Eventos;
using MeuProjeto.Compras.Dominio;
namespace MeuProjeto.Compras.Consumidores
{
// Executado no escopo do pacote Compras
public class ProdutoAtualizadoConsumer : IConsumer<ProdutoAtualizadoEvent>
{
private readonly DbContext _context; // DbContext do pacote Compras
public ProdutoAtualizadoConsumer(DbContext context)
{
_context = context;
}
public async Task Consume(ConsumeContext<ProdutoAtualizadoEvent> context)
{
var dadosNovoProduto = context.Message;
var produtoLocal = await _context.Set<ProdutoEspelho>()
.FirstOrDefaultAsync(p => p.Id == dadosNovoProduto.Id);
if (produtoLocal == null)
{
// Se não existe localmente no módulo de compras, cria o registro
produtoLocal = new ProdutoEspelho(dadosNovoProduto.Id, dadosNovoProduto.Preco, dadosNovoProduto.EstoqueAtual);
await _context.Set<ProdutoEspelho>().AddAsync(produtoLocal);
}
else
{
// Se já existe, atualiza o preço e estoque locais
produtoLocal.AtualizarDados(dadosNovoProduto.Preco, dadosNovoProduto.EstoqueAtual);
}
await _context.SaveChangesAsync();
}
}
}
3. Realizando a Compra de Forma 100% Local (Caso de Uso)
Agora, o processo de criação de compra é extremamente rápido e autônomo. Ele consulta a tabela espelho local, valida as regras e salva. Nada sai da aplicação via rede de forma síncrona.
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MassTransit;
using MeuProjeto.Compras.Dominio;
using MeuProjeto.Contratos.Eventos;
namespace MeuProjeto.Compras.Aplicacao
{
public class CriarCompraCommandHandler
{
private readonly DbContext _context; // Contexto local de Compras
private readonly IPublishEndpoint _publishEndpoint;
public CriarCompraCommandHandler(DbContext context, IPublishEndpoint publishEndpoint)
{
_context = context;
_publishEndpoint = publishEndpoint;
}
public async Task ExecutarAsync(Guid produtoId, int quantidade)
{
// 1. Consulta o produto DIRETAMENTE no banco local de Compras
var produto = await _context.Set<ProdutoEspelho>()
.FirstOrDefaultAsync(p => p.Id == produtoId);
if (produto == null)
throw new Exception("Produto não disponível para venda.");
if (produto.Estoque < quantidade)
throw new Exception("Estoque insuficiente.");
// 2. Modifica o estado local das entidades de Compras
var novaCompra = new Compra(DateTime.UtcNow);
novaCompra.AdicionarItem(produto.Id, quantidade, produto.Preco);
// Otimismo: Deduz temporariamente no estoque local para evitar vendas duplicadas rápidas
produto.DeduzirEstoqueLocal(quantidade);
// 3. Salva no banco de compras (Compra + Itens + Atualização do ProdutoEspelho)
await _context.Set<Compra>().AddAsync(novaCompra);
await _context.SaveChangesAsync();
// 4. Dispara o evento avisando o mundo que a compra foi feita
// O pacote Catálogo escutará este evento para consolidar a baixa definitiva lá
await _publishEndpoint.Publish(new CompraFinalizadaEvent(
novaCompra.Id,
novaCompra.DataCompra,
new List<ItemCompraEfetivadaDto> { new ItemCompraEfetivadaDto(produtoId, quantidade) }
));
}
}
}
Resumo do Fluxo Arquitetural Evoluído
[Catálogo] -> Publica "ProdutoAtualizadoEvent" -> [Broker] -> Consumido por [Compras] (Preenche ProdutoEspelho local)
|
[Cliente] -> Envia requisição "Comprar" -> [Compras] (Valida e grava tudo 100% LOCAL no seu banco)
|
[Compras] -> Publica "CompraFinalizadaEvent" -> [Broker] -> Consumido por [Catálogo] (Atualiza estoque oficial)
Deseja compreender como tratar o cenário de exceção conhecido como Compensação, caso o estoque oficial no pacote Catálogo falhe ao processar a mensagem final, ou prefere focar em como mapear essa tabela espelho no EF Core?
Comentários