// projeto em produção

StocksMaster.API — .NET 8

API de gestão de estoque com Onion Architecture e regras de dependência verificadas em build (NetArchTest), JWT + RBAC, trilha de auditoria imutável e 209 testes. Dois vídeos narrados: a demo percorre o contrato da API; a faixa de observabilidade mostra a aplicação viva em produção.

StocksMaster.API — .NET 8demo · 0:50narrado

// o que esta demonstração mostra

  • Contrato antes da requisição: API versionada em /api/v1, corpo aceito declarado no OpenAPI.
  • Criação devolvendo 201 com o identificador do recurso; erro 400 estruturado por propriedade (errors[]).
  • Serilog em JSON com correlação por requisição: rota, status, tempo e origem em cada linha.
  • Trilha de auditoria imutável: INSERT em audit_log com valor antigo e novo, quem fez, de qual IP.
  • Delete lógico com deleted_at — e a leitura filtrando no banco, não na memória.

// case study de engenharia

Problema, arquitetura, decisões e o que foi medido — tudo rastreável às fontes listadas no fim. Sem números inventados.

Problema

Modelar o ciclo completo de uma operação comercial — cadastro de produtos, categorias, fornecedores, clientes, pedidos de compra e vendas com baixa automática de estoque — como uma API corporativa com autenticação, autorização, auditoria e observabilidade prontas para produção, e não como um CRUD de exemplo.

Contexto

O projeto usava vocabulário de arquitetura hexagonal sem a inversão que o justifica. Medição antes da correção: 61 de 66 arquivos do domínio (92%) importavam infraestrutura, a solução tinha um único projeto (nenhuma fronteira compilada) e a camada de aplicação anunciada no README tinha zero arquivos. A maior parte das violações era acidente de nomenclatura — conceitos de domínio (erros de notificação, validadores) morando numa pasta chamada Infrastructure. A demo pública roda no Fly.io (São Paulo) com PostgreSQL gerenciado no Neon; a arquitetura-alvo AWS (ECS Fargate, RDS Multi-AZ, ALB, WAF) está em Terraform, validada e não aplicada.

Arquitetura

Onion Architecture em três projetos com a regra de dependência imposta pelo compilador: StockMaster.Domain não referencia nada; StockMaster.Infrastructure referencia Domain; StockMaster.API referencia Infrastructure (e Domain). O domínio declara as portas de saída e a infraestrutura as implementa. Ressalva registrada no próprio repositório: é um Onion sem camada de aplicação — a orquestração de casos de uso ainda vive nos serviços de domínio (CustomCrud), acionados pelo repositório genérico.

Onion Architecture da StocksMaster.APITrês anéis concêntricos: StockMaster.Domain no centro (entidades, portas, Result), StockMaster.Infrastructure no anel do meio (EF Core, JWT, interceptor de auditoria) e StockMaster.API no anel externo (controllers, DTOs, middleware). As setas apontam para dentro: API depende de Infrastructure, que depende de Domain; o Domain não referencia nada. A regra é verificada em build por NetArchTest.Domainentidades · portas · ResultInfrastructureEF Core · JWT · interceptor de auditoriaAPIcontrollers · DTOs · middleware// regra de dependênciaAPI → Infrastructure → DomainDomain referencia: nada// verificada em buildNetArchTest: Domain não depende deInfrastructure, API, EF Core, ASP.NET Core+ sanidade do scan e da detecção// portas de saída (no Domain)IGenericRepository · IUnitOfWorkIJwtTokenService · IPasswordHasherServiceICurrentUser · IApplicationTimeZoneressalva: sem camada de aplicação —casos de uso vivem nos CustomCrud do Domain
diagrama SVG inline — sem biblioteca, herda o tema da página
  • StockMaster.Domain

    Entidades, regras de negócio, Notification e Result patterns e as portas de saída: IGenericRepository, IUserRepository, IInventoryService, ICurrentUser, IJwtTokenService, IPasswordHasherService, IUnitOfWork, IApplicationTimeZone. Única dependência externa: Microsoft.Extensions.Logging.Abstractions.

  • StockMaster.Infrastructure

    Adaptadores: EF Core 9 + Npgsql, migrations, hash de senha, emissão de JWT, interceptor de auditoria.

  • StockMaster.API

    Entrega HTTP: controllers versionados (/api/v1), DTOs, middleware, filtros, composição (Program.cs).

Decisões e trade-offs

  1. decisão 01

    Onion em vez de Hexagonal: o controller invoca o repositório diretamente — não existe porta de entrada (um IUseCase) entre a entrega HTTP e a lógica. A Hexagonal exige portas nos dois lados; aqui só há o lado de saída, em camadas concêntricas nomeadas — o que caracteriza Onion (e é como a comunidade .NET nomeia o arranjo Domain / Infrastructure / API).

    em vez de
    Hexagonal (Ports & Adapters) ou Clean Architecture.
    trade-off
    A camada de aplicação continua ausente: a regra de negócio vive em CustomCrud, acionada pelo repositório — acoplamento entre persistência e domínio que a separação de projetos, sozinha, não desfaz. Extrair casos de uso é a próxima evolução, declarada como aberta.
    evidência
    docs/ARQUITETURA.md do repositório ("Por que Onion e não Hexagonal" e "Aberto")
  2. decisão 02

    Regra de dependência verificada em build por testes de arquitetura (NetArchTest): domínio não depende de Infrastructure, API, EF Core nem ASP.NET Core; infraestrutura não depende da API; DTOs não referenciam schemas de persistência.

    em vez de
    Convenção de pastas e revisão manual — o diagnóstico mostrou 92% de violação sob esse regime.
    trade-off
    Para os testes não virarem decoração, duas verificações extras: sanidade do scan (falha se o analisador não enxergar tipo nenhum) e sanidade da detecção (exige que a regra acuse a dependência real de Infrastructure para EF Core).
    evidência
    test/StockMaster.API.UnitTest/Architecture/DependencyRuleTests.cs
  3. decisão 03

    Sem MediatR/CQRS: para uma API majoritariamente CRUD, um handler por operação adiciona cerimônia sem resolver acoplamento.

    em vez de
    MediatR + handlers por comando/consulta.
    trade-off
    GenericBaseRepository segue acumulando validação, mapeamento, auditoria e persistência (~280 linhas) — registrado como ponto aberto, não escondido.
    evidência
    docs/ARQUITETURA.md ("Decisões deliberadas" e "Aberto")
  4. decisão 04

    Trilha de auditoria automática via interceptor do EF Core — criação, alteração e exclusão capturam valores antigo/novo, usuário, IP, sessão e correlação; o operador vem do token, não do corpo da requisição.

    em vez de
    Registrar a auditoria manualmente em cada serviço.
    trade-off
    Impossível de esquecer, ao custo de modelo: cada entidade crítica tem tabela-espelho de auditoria (15) mais audit_log (valores antigo/novo em jsonb) e authentication_events, num total de 33 tabelas.
    evidência
    README ("Modelo de dados" e "Boas práticas adotadas")
  5. decisão 05

    Secure by default: FallbackPolicy exige autenticação em todo endpoint salvo [AllowAnonymous] explícito; JWT Bearer HS256; senha com PBKDF2-HMAC-SHA512 (ASP.NET Core Identity PasswordHasher); segredos só por variável de ambiente e a aplicação não inicia sem JWT_SECRET_KEY de 32+ bytes.

    trade-off
    HS256 é simétrico — RS256 fica anotado como melhoria para cenários multi-serviço; refresh token com rotação e denylist de jti (revogação imediata) ainda não existem.
    evidência
    README ("Funcionalidades", "Configuração" e "Possíveis melhorias futuras")
  6. decisão 06

    Exclusão lógica via deleted_at com filtro global de consulta, integridade referencial em Restrict (excluir um operador não apaga o histórico) e concorrência otimista.

    trade-off
    Linhas nunca saem do banco; a leitura depende do filtro — o vídeo mostra a consulta filtrando deleted_at IS NULL no banco, não na memória.
    evidência
    README ("Modelo de dados") · clipe stockmaster-obs
  7. decisão 07

    TimeProvider injetável — nenhuma leitura direta de relógio, tornando regras temporais testáveis; fuso de negócio configurável (APP_TIMEZONE, padrão America/Sao_Paulo).

    trade-off
    Mais uma dependência a compor; em troca, testes de fuso horário cobrem inclusive horário de verão histórico.
    evidência
    README ("Boas práticas adotadas" e "Como executar os testes")

Implementação

  • Separar os projetos fez o compilador encontrar três acoplamentos reais: IMovementAuditRepository morava em Infrastructure (movida para Domain); BootStrapper registrava middlewares da API (registro movido para Program.cs); DomainBase.SetId era internal e só funcionava num assembly único — resolvido com InternalsVisibleTo("StockMaster.Infrastructure") em vez de torná-lo público.
  • Regras de estoque no domínio: baixa automática na venda, bloqueio de saldo negativo e total da venda calculado a partir dos itens (o cliente não define o total).
  • Autorização RBAC com papéis, claims e policies; eventos de autenticação (login, logout, falhas) persistidos para análise forense.
  • Modelo de dados com 33 tabelas, UUID como chave primária e operator_user_id em toda tabela de negócio — base da rastreabilidade.
  • Rate limiting, health checks de liveness/readiness separados, contêiner multi-stage Alpine non-root com health check.
  • CI/CD no GitHub Actions: build, testes, análise estática (CodeQL) e scan de imagem (Trivy) a cada push.

Testes

209 testes (xUnit, FluentAssertions, EF Core InMemory, NetArchTest) rodando no CI a cada push.

  • Entidades de domínio e regras de estoque.
  • Hashing de senha e fuso horário (inclusive horário de verão histórico).
  • Trilha de auditoria.
  • Regra de dependência da arquitetura, com as duas verificações de sanidade do próprio mecanismo.
  • Lacuna declarada: testes de integração cobrindo o pipeline HTTP → repositório → banco estão listados como melhoria futura.

Observabilidade

Logs estruturados em JSON com correlação por requisição e trilha de auditoria consultável — o que a faixa de observabilidade mostra em produção.

  • Serilog em JSON com correlação por requisição: rota, status, tempo e origem em cada linha.
  • INSERT em audit_log com valor antigo e novo, quem fez e de qual IP — e authentication_events para login/logout/falhas.
  • Health checks separados de liveness e readiness.
  • Contrato de erro estruturado por propriedade (errors[]) em 400 e 201 com identificador do recurso na criação.

Resultados e aprendizados

  • Regra de dependência saiu de 92% de violação (61 de 66 arquivos) para fronteira imposta pelo compilador e por testes de arquitetura.
  • Três acoplamentos que a revisão manual só suspeitava viraram erro de compilação — e foram corrigidos na direção certa (interface para o domínio, middleware para a API, InternalsVisibleTo nominal).
  • 209 testes, CodeQL e Trivy no pipeline; API publicada com Swagger ao vivo.
  • Aprendizado: nomear a arquitetura corretamente (Onion, sem camada de aplicação) e listar o que falta (casos de uso, God class no repositório genérico, isolamento multiempresa por linha já com a claim company_id emitida) vale mais do que anunciar uma camada vazia.

Fontes

Tudo o que está escrito acima é rastreável a estes arquivos e repositórios.

eduardo.valente © 2026 // engenharia em produção