Pular para o conteúdo principal

Arquitetura da Aplicação em Pacotes com Gateways e Interfaces

 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

  1. 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.
  2. 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?

  1. 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]
  2. 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]
  3. 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

Postagens mais visitadas deste blog

UML e comunicação entre Componentes

(conteúdo gerado com I.A.) Antes de abordarmos o conceito de Componentes, vale a pena analisar como um programador iniciante costuma estruturar uma aplicação. Geralmente, a escolha é construir um monólito tradicional, agrupando todas as classes de domínio (como Categoria, Produto, Compra, Venda, Pagamento, etc.) em uma única pasta de Entidades. Da mesma forma, criam-se pacotes globais para Repositórios, Serviços e Controllers. O grande problema dessa abordagem é que ela gera um forte acoplamento entre as classes e as camadas do sistema. À medida que a aplicação cresce, isso desencadeia uma série de dificuldades: Manutenção e modelagem complexas: As responsabilidades se misturam. Rapidamente, o código passa a ter diversos trechos onde as classes assumem funções que não deveriam ter, tornando a evolução do sistema confusa. Escalabilidade ineficiente: Para escalar o backend, é necessário replicar a aplicação inteira e todo o banco de dados. Além de consumir mais recursos, isso traz o ri...

Quando escolher Flask ou Quart no Python

Atualmente , considerando o suporte assíncrono já presente no Flask, a escolha entre Flask e Quart para uma API Server depende de vários fatores técnicos. Vejamos alguns critérios: Fonte:  https://pt.stackoverflow.com/questions/51268/qual-a-diferen%C3%A7a-entre-comunica%C3%A7%C3%A3o-ass%C3%ADncrona-e-s%C3%ADncrona Suporte Assíncrono : Flask : O Flask oferece suporte assíncrono por meio de extensões de terceiros, como  flask[async] . No entanto, seu desempenho assíncrono é inferior aos frameworks nativamente assíncronos. Quart : O Quart é uma reimplantação do Flask baseada no padrão ASGI (Asynchronous Server Gateway Interface). Ele utiliza E/S assíncrona em todo o código e é mais adequado para bases de código predominantemente assíncronas. Desempenho : Flask : Como aplicação WSGI, o Flask usa um único worker para cada ciclo de solicitação/resposta. Quando uma solicitação chega a uma visualização assíncrona, o Flask inicia um loop de eventos em uma thread, executa a função da vi...

Metodo para criar relacionamento entre tabelas sem dar erro cíclico EFCore

Quando estamos modelando o banco de dados no Entity Framework Core (ou em qualquer outra ferramenta) é muito comum criarmos o erro cíclico. Este erro ocorre devido ao banco de dados não conseguir determinar onde o relacionamento termina, causando um erro de loop. Vejamos uma situação real onde foi gerado esse problema: Um Professor leciona em várias Turmas, Um Professor leciona uma ou mais Disciplinas, Um Professor leciona determinadas Disciplinas em determinadas Turmas, Uma Turma tem muitos Professores, Em todas as Turmas tem 9 Disciplinas. Em todas as turmas podem ter até 9 Professores. Só por essa descrição já é possível perceber que os relacionamentos podem ser complexos de entender. E tem-se também o problema da navegação em via dupla: Professor-Turmas, Turma-Professores. Então, como modelar essas tabelas sem que o banco entre em loop? Para resolver esse problema no Entity Framework Core e evitar o erro de "Multiple Cascade Paths" (caminhos múltiplos de cascata) comum n...