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 8 — demo · 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.
Problema, arquitetura, decisões e o que foi medido — tudo rastreável às fontes listadas no fim. Sem números inventados.
01Problema
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.
02Contexto
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.
03Arquitetura
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.
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.
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")
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).
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")
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")
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")
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
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")
05Implementaçã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.
06Testes
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.
07Observabilidade
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.
08Resultados 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.
09Fontes
Tudo o que está escrito acima é rastreável a estes arquivos e repositórios.