Todo projeto .NET começa limpo. Aí entra o prazo, entra o "depois eu arrumo", e seis meses depois você tem um static Helper que faz tudo, um async void que engole exceção e um catch que só existe pra fingir que trata erro. Nada disso é culpa do .NET. A plataforma já te dá quase tudo pronto pra fazer certo. O problema é ignorar o que já vem na caixa.

Injeção de dependência: a nativa já resolve
Não precisa de Autofac, Ninject nem de um ServiceLocator caseiro. O Microsoft.Extensions.DependencyInjection cobre 95% dos casos. Registre no lugar certo e deixe o container resolver.
O que trava gente é o tempo de vida. Regra prática: Singleton pra coisa sem estado e cara de criar (HttpClient factory, cache), Scoped pra tudo que segue o request (DbContext), Transient pro resto. O erro clássico é injetar um Scoped dentro de um Singleton — o container reclama, e reclama com razão: você prendeu um objeto de request na vida inteira da aplicação.
Programar contra interface aqui não é purismo; é o que deixa o teste possível sem mock mágico.
async/await de verdade: CancellationToken e chega de async void
async void é uma armadilha. Exceção lançada ali não sobe pro chamador — ela derruba o processo ou some. Só existe um uso legítimo: event handler. Fora disso, é async Task, sempre.
E propague o CancellationToken. Ele não é enfeite de assinatura: é o que faz sua API parar de processar quando o cliente já desistiu. Recebeu no controller, passa pro service, passa pro HttpClient, passa pro EF Core. Um token que morre no primeiro método é um token inútil.
public async Task<Pedido> BuscarAsync(int id, CancellationToken ct)
{
var pedido = await _db.Pedidos.FindAsync([id], ct);
return pedido ?? throw new NotFoundException(id);
}
De quebra: nada de .Result ou .Wait(). Isso é deadlock esperando acontecer.
IDisposable e using: solte o que você segura
Conexão, stream, handle de arquivo — tudo que implementa IDisposable precisa ser liberado. E a forma correta não é try/finally na mão, é using. O using declaration (sem chaves) deixa o código plano e libera no fim do escopo:
using var stream = File.OpenRead(caminho);
var hash = await SHA256.HashDataAsync(stream, ct);
Se sua classe segura um recurso descartável, ela também vira IDisposable — e repassa o Dispose pra baixo. Segurar recurso sem soltar é vazamento silencioso que só aparece em produção, sob carga, na pior hora.
Records e imutabilidade: menos surpresa, menos bug
DTO, value object, evento, mensagem: tudo isso é dado que não deveria mudar depois de criado. record te dá isso de graça — igualdade por valor, ToString decente e with pra copiar mudando um campo.
public record Cliente(string Nome, string Email);
Objeto imutável não é modificado por engano em outra thread nem em outro método três camadas abaixo. Menos estado mutável circulando é menos bug de "mas eu não mudei isso". Guarde class mutável pra quando você realmente precisa de identidade e ciclo de vida — não pra todo saco de dados.
Configuração com IOptions, não com string mágica
Espalhar configuration["ChaveDaApi"] pelo código é dívida garantida: sem tipo, sem validação, e você só descobre que a chave sumiu quando estoura em runtime. Modele a configuração numa classe e injete IOptions<T>.
builder.Services.AddOptions<EmailOptions>()
.Bind(builder.Configuration.GetSection("Email"))
.ValidateDataAnnotations()
.ValidateOnStart();
ValidateOnStart é o pulo do gato: config inválida derruba a aplicação na subida, não no meio de um envio às duas da manhã. Se a config muda em runtime, use IOptionsSnapshot. Mas comece pelo simples.
Log estruturado: pare de concatenar string
_logger.LogInformation("Pedido " + id + " criado") é um log que você não consegue consultar. Use os placeholders — eles viram campos indexados no seu backend de observabilidade:
_logger.LogInformation("Pedido {PedidoId} criado para {ClienteId}", pedido.Id, cliente.Id);
Agora você filtra por PedidoId no Seq, no Grafana, onde for. Junte isso a ILogger<T> (que já te dá a categoria certa) e a scopes pra correlacionar requests, e o log deixa de ser lixo textual e vira ferramenta de investigação.
Boa prática em .NET raramente é a coisa mais nova do NuGet. Quase sempre é usar o que a plataforma já entrega, do jeito que ela foi feita pra ser usada. O framework não vai te impedir de fazer gambiarra — mas também não te obriga. A escolha, como sempre, é sua.


