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:

  1. Esse código está realmente protegido por testes para cenários de borda? (Strings nulas, formatos inesperados, concorrência, fusos horários atípicos).
  2. 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:

  1. 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.
  2. Padronização e Reuso: Elimina as versões divergentes de helpers de data, paginação e claims que costumam se proliferar entre projetos.
  3. FinOps & Densidade Computacional: APIs leves permitem escala a zero eficiente e menor custo na nuvem.
  4. 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> e ArrayPool<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:

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!