Uma jornada prática de estudos que transformou rotinas utilitárias do dia a dia em micro-pacotes com testes unitários e benchmarks, culminando em bibliotecas desacopladas e templates para o ecossistema .NET.
Todo desenvolvedor .NET com alguns anos de estrada já viveu exatamente esta cena:
Você cria uma nova API ou microsserviço e, na primeira hora de trabalho — antes mesmo de escrever a primeira regra de negócio —, abre o Google, o StackOverflow ou um projeto antigo no GitHub para pescar código utilitário:
- Aquele método para ler a
[Description]de um Enum; - A formatação e validação de datas comerciais ou fusos UTC;
- O algoritmo de paginação Keyset no EF Core;
- A lógica para mascarar dados sensíveis (LGPD) ou extrair claims de um token JWT.
A gente joga tudo isso dentro de uma pasta chamada Utils, Common ou Shared, promete para si mesmo que "dessa vez vai ser organizado e temporário" e segue o jogo.
O problema é o tempo. Seis meses e quatro microsserviços depois, aquela pasta Utils já possui versões divergentes espalhadas pela empresa. Um bug corrigido no serviço de Pagamentos continua vivo no serviço de Faturamento. E o pior de tudo: quase ninguém escreve testes unitários ou mede o custo de alocação de memória de um código que foi apenas copiado e colado.
Eu me cansei desse retrabalho. O que começou despretensiosamente como uma forma de estudo nos tempos livres e a vontade de "organizar rotinas comuns em pacotes leves e testados" acabou evoluindo de forma consistente: hoje são 32 pacotes publicados no NuGet, somando mais de 35.000 downloads, com baterias completas no BenchmarkDotNet e dois templates arquiteturais oficiais (TL.LiteCore e TL.ResilientCore).
Neste primeiro artigo da série, quero abrir os bastidores reais dessa jornada: o estudo prático nos tempos livres, a obsessão por zero dependências externas, os números reais de performance e o porquê de criar dois templates cirúrgicos para resolver o "Day 0".
1. O Ponto de Partida: Curiosidade Técnica e Eliminação do Retrabalho
No início, eu não tinha a pretensão de lançar um ecossistema. O objetivo era muito mais modesto: estudar a fundo as entranhas do C# e do runtime .NET.
Eu queria pegar aquelas rotinas que usamos de olhos fechados no dia a dia e submetê-las a duas perguntas incômodas:
- Esse código está realmente protegido por testes para cenários de borda? (Strings nulas, formatos inesperados, concorrência, fusos horários atípicos).
- Qual é o custo real dessa abstração em CPU e alocações no Heap (Garbage Collector)?
Conforme me deparava com rotinas repetitivas nos projetos do dia a dia, passei a isolá-las e organizá-las em soluções dedicadas. O que no início eram métodos soltos evoluiu organicamente para uma suíte de extensões puras (TL.*ExtensionsLibrary), aceleradores de dados (TL.DataHelpers), blocos de resiliência e contratos fundamentais (TL.BuildingBlocks) e barramento de mensageria (TL.Messaging).
Para manter esse ecossistema organizado, versionado e de fácil consumo entre diferentes soluções, passei a empacotar e publicar tudo abertamente no NuGet Gallery (ThaylonMALopes). Com a evolução contínua das versões, testes e integrações, o perfil hoje reúne um histórico consolidado:
┌────────────────────────────────────────────────────────────────────────┐ │ NUGET GALLERY HIGHLIGHTS │ │ │ │ 📦 Total de Pacotes Públicos: 32 pacotes │ │ 🚀 Total Acumulado de Downloads: +35.070 downloads │ │ │ │ • TL.EnumExtensionsLibrary ➔ 2.450+ downloads (v0.8.0) │ │ • TL.NumericExtensionsLibrary ➔ 2.120+ downloads (v0.8.0) │ │ • TL.CollectionExtensionsLibrary ➔ 2.120+ downloads (v0.8.0) │ │ • TL.ObjectExtensionsLibrary ➔ 1.800+ downloads (v0.8.0) │ └────────────────────────────────────────────────────────────────────────┘ 2. A Regra de Ouro: "Zero Dependências Externas" (Pureza BCL)
Quando você decide criar bibliotecas utilitárias, a primeira tentação comum é baixar pacotes populares de terceiros para resolver problemas corriqueiros. Mas isso traz armadilhas severas:
- Dependências transitivas desnecessárias: Para usar um método simples, seu projeto arrasta 15 DLLs que você nem conhece.
- Risco de Supply Chain: Quanto maior a árvore de pacotes de terceiros, maior a superfície para vulnerabilidades e quebras de compatibilidade.
- Mudança de Licenciamento: Como vimos recentemente no ecossistema .NET, bibliotecas consagradas que eram gratuitas mudaram de licença comercial. Ficar refém de dependências externas para mecânicas básicas é um risco de negócio real.
Para as bibliotecas de extensão e contratos (TL.*ExtensionsLibrary e TL.BaseContracts), a diretriz foi inegociável: zero dependências externas e pureza de BCL. Se o runtime moderno do C# e do .NET 8/9 oferece Span<T>, Memory<T>, ArrayPool<T> e System.Text.Json com alta eficiência, não há motivo para inflar essas bases com pacotes de terceiros. Já para os pacotes de integração e infraestrutura (como RabbitMQ, Dapper e Mongo), o foco foi criar adaptadores finos e resilientes, sem arrastar dezenas de DLLs indiretas para o projeto.
3. O Choque dos Benchmarks: A Memória Oculta do "Copia e Cola"
Ao isolar os métodos e submetê-los ao BenchmarkDotNet, a teoria deu lugar aos fatos empíricos. Muitos códigos "clássicos" copiados de fóruns são verdadeiros vilões de alocação no Heap, sobrecarregando o Garbage Collector (GC).
Veja dois testes reais executados no ecossistema:
Caso 1: Lendo Descrições de Enums (TL.EnumExtensionsLibrary)
O método tradicional em quase toda empresa usa reflexão pura direta no Enum (field.GetCustomAttributes(...)) a cada requisição. No TL.EnumExtensionsLibrary, implementamos um cache genérico de thread-safe zero-alloc:
| Abordagem | Tempo Médio | Memória Alocada (Heap) | Razão de Alocação |
|---|---|---|---|
| Reflection Direta (Baseline habitual) | 263.6 μs | 1.560 Bytes | 1.00 |
TL.EnumExtensionsLibrary (Generic Cache) | 274.1 μs | 400 Bytes | 0.26 (-74%) |
Ganho Real: 74% a menos de alocação no Heap. Em cenários de alto throughput com muitas requisições por segundo, essa redução contínua de alocações diminui sensivelmente o trabalho de coleta e a pressão sobre o Garbage Collector (GC).
Caso 2: Ordenação Dinâmica no LINQ (TL.QueryableExtensionsLibrary)
Endpoints com filtros e ordenação dinâmica em tempo de execução (?sortBy=CreatedAt&desc=true) costumam recorrer a reflexão sem cache para montar expressões dinâmicas. Ao comparar a reflexão tradicional com a compilação e cache de Expression Trees da biblioteca:
| Abordagem | Tempo Médio | Memória Alocada (Heap) | Impacto Real |
|---|---|---|---|
| Reflection sem Cache (Baseline tradicional) | 11.98 ms | 445.99 KB | Alocação excessiva no Heap |
TL.QueryableExtensionsLibrary (Cached Expressions) | 12.83 ms | 212.39 KB | -52,4% de memória alocada |
Medir esses números provou que código utilitário não é "código secundário". Como essas rotinas rodam no alicerce de praticamente todas as requisições, qualquer otimização de alocação acumula ganhos consistentes ao longo do ciclo de vida da aplicação.
4. O Dilema do "Day 0": Do Pacote Avulso aos Templates CLI
Depois de consolidar e publicar as bibliotecas, esbarrei no próximo atrito da rotina de desenvolvimento:
"Ter ótimas bibliotecas no NuGet resolve o código compartilhado, mas não elimina o trabalho de setup inicial: a cada novo serviço, ainda era necessário recriar a estrutura de pastas, configurar injeção de dependência, Docker Chiseled, probes de HealthCheck e padronizar OpenAPI."
Era hora de automatizar o "Day 0" com Templates CLI (dotnet new). Mas aqui mora um dos maiores erros da arquitetura: tentar criar um único template que sirva para tudo.
Se você escolhe uma estrutura com DDD completo, CQRS, RabbitMQ e Outbox Pattern para criar uma API simples de consulta de CEP ou um webhook satélite, você gera sobre-engenharia (overengineering) e infla o consumo de nuvem sem necessidade.
Por outro lado, se cria uma API rasa e sem isolamento para um core financeiro ou de pedidos, o projeto logo se torna insustentável.
Por isso, dividi a estratégia em dois templates complementares:
┌────────────────────────────────────────────────────────────────────────┐ │ MATRIZ COMPARATIVA DE TEMPLATES │ ├────────────────────────────┬─────────────────────┬─────────────────────┤ │ Dimensão │ ⚡ TL.LiteCore │ 🛡️ TL.ResilientCore │ ├────────────────────────────┼─────────────────────┼─────────────────────┤ │ Paradigma │ Minimal APIs REST │ Event-Driven & CQRS │ │ Modelo Transacional │ ACID Local direto │ Transactional Outbox│ │ Mensageria Interna │ Nenhuma (Bloqueada) │ RabbitMQ + DLQ │ │ Banco Local em Dev │ SQLite em memória │ Postgres 16 (Docker)│ │ Consumo de RAM │ ~35 MB │ 120 MB a 180 MB │ │ Tempo de Startup (Boot) │ < 80 ms │ 350 ms a 600 ms │ │ Tamanho do Container │ < 110 MB (Chiseled) │ ~210 MB (Chiseled) │ │ Rastreabilidade │ Logs estruturados │ W3C Tracing nativo │ │ Cenário Recomendado │ CRUDs, BFFs, PoCs │ Sistemas Críticos │ └────────────────────────────┴─────────────────────┴─────────────────────┘ 1. TL.LiteCore (dotnet new tl-litecore)
Focado em alta densidade, baixo custo (FinOps) e startup ultrarrápido (<80ms). Ele é um Clean Skeleton (sem entidades falsas para apagar). Já vem com Result Pattern embutido via TL.BaseContracts, documentação Scalar OpenAPI e testes funcionais em memória com WebApplicationFactory. Consumindo em torno de ~35MB de RAM em repouso, o template oferece uma excelente densidade de recursos (FinOps), ideal para cenários com múltiplos microsserviços satélites ou modelos de escala a zero.
# Instalar e criar uma Minimal API síncrona pronta em 30 segundos dotnet new install TL.LiteCore.Template dotnet new tl-litecore -n MinhaOrganizacao.CatalogoApi 2. TL.ResilientCore (dotnet new tl-resilientcore)
Desenhado para o coração do negócio. Já nasce com Transactional Outbox Pattern (mensagens só saem no RabbitMQ se o banco commitar), Transactional Inbox (garantia de consumo idempotente), políticas de resiliência compostas pelo Polly v8 e rastreamento distribuído ponta a ponta (traceparent W3C).
O grande trunfo é a consistência de engenharia: o desenvolvedor usa os mesmos contratos, as mesmas extensões e o mesmo tratamento de erros tanto no serviço de 35MB quanto no serviço core orientado a eventos.
5. O Real Valor Entregue: Por que tudo isso valeu a pena?
Ao olhar para essa jornada, o valor real entregue não é medido apenas pelas linhas de código escritas, mas pelo impacto prático:
- Aceleração do Day 0: Todo o trabalho repetitivo de setup inicial de infraestrutura, arquitetura e testes é resolvido em um único comando CLI, entregando uma solução pronta e com testes verdes de primeira.
- Padronização e Reuso: Elimina as versões divergentes de helpers de data, paginação e claims que costumam se proliferar entre projetos.
- FinOps & Densidade Computacional: APIs leves permitem escala a zero eficiente e menor custo na nuvem.
- Resiliência Transacional sem Licenças Caras: Solução robusta de Outbox e Inbox no RabbitMQ sem amarras proprietárias.
🗺️ O Que Vem Pela Frente
Este é o primeiro artigo de uma série técnica onde pretendo abrir o código, a arquitetura interna e os benchmarks de cada um desses componentes. Alguns dos tópicos que quero explorar em profundidade nos próximos textos:
- Result Pattern vs Exceções: O benchmark definitivo medindo a diferença de latência e alocação de memória no fluxo de negócio.
- Técnicas de Zero-Allocation no C#: Uso prático de
Span<T>,Memory<T>eArrayPool<T>para processamento de alto rendimento sem sobrecarregar o Garbage Collector. - Keyset Pagination na Prática: Como estruturar consultas com cursores para paginar milhões de registros em $O(1)$.
- Resiliência e Mensageria Defensiva: Implementando Transactional Outbox, Dead Letter Queues (DLQ) e consumo idempotente com RabbitMQ.
- TL.LiteCore vs TL.ResilientCore: Um guia arquitetural aprofundado para orientar a escolha do template ideal para o perfil do seu serviço.
Conheça e Teste o Ecossistema
Todos os pacotes estão disponíveis publicamente no NuGet:
- 📦 Perfil no NuGet Gallery: nuget.org/profiles/ThaylonMALopes
- 💻 Repositórios e Código Aberto: github.com/thaylonlopes
E no seu time? Como vocês lidam com a tentação da pasta "Utils" e a padronização no início de novos microsserviços? Compartilhe sua experiência nos comentários!