Todo proyecto .NET comienza limpio. Después llegan el plazo, el «luego lo arreglo» y, seis meses más tarde, tienes un static Helper que hace de todo, un async void que se traga las excepciones y un catch que solo existe para fingir que trata los errores. Nada de eso es culpa de .NET. La plataforma ya te ofrece casi todo lo necesario para hacerlo bien. El problema es ignorar lo que ya viene incluido.

Inyección de dependencias: la nativa ya resuelve el problema
No necesitas Autofac, Ninject ni un ServiceLocator casero. Microsoft.Extensions.DependencyInjection cubre el 95 % de los casos. Registra las dependencias en el lugar correcto y deja que el contenedor las resuelva.
Lo que suele complicar las cosas es el tiempo de vida. Regla práctica: Singleton para cosas sin estado y costosas de crear (fábrica de HttpClient, caché), Scoped para todo lo que sigue el request (DbContext) y Transient para el resto. El error clásico es inyectar un Scoped dentro de un Singleton: el contenedor se queja, y con razón: ataste un objeto del request a toda la vida de la aplicación.
Programar contra una interfaz aquí no es purismo; es lo que permite probar el código sin depender de mocks mágicos.
async/await de verdad: CancellationToken y basta de async void
async void es una trampa. Las excepciones lanzadas allí no llegan al llamador: pueden derribar el proceso o desaparecer. Solo existe un uso legítimo: un event handler. Fuera de eso, usa async Task, siempre.
Y propaga el CancellationToken. No es un adorno en la firma: es lo que hace que tu API deje de procesar cuando el cliente ya abandonó la solicitud. Si lo recibes en el controller, pásalo al service, a HttpClient y a EF Core. Un token que muere en el primer método es un token inútil.
csharp public async Task<Pedido> BuscarAsync(int id, CancellationToken ct) { var pedido = await _db.Pedidos.FindAsync([id], ct); return pedido ?? throw new NotFoundException(id); }
Y, de paso: nada de .Result o .Wait(). Eso es un deadlock esperando ocurrir.
IDisposable y using: libera lo que retienes
Conexiones, streams y handles de archivos: todo lo que implementa IDisposable debe liberarse. Y la forma correcta no es escribir un try/finally a mano, sino usar using. La declaración using (sin llaves) mantiene el código plano y libera el recurso al final del ámbito:
csharp using var stream = File.OpenRead(caminho); var hash = await SHA256.HashDataAsync(stream, ct);
Si tu clase retiene un recurso descartable, también se convierte en IDisposable y delega el Dispose hacia abajo. Retener un recurso sin liberarlo es una fuga silenciosa que solo aparece en producción, bajo carga y en el peor momento.
Records e inmutabilidad: menos sorpresas, menos errores
DTO, value object, evento y mensaje: todo eso son datos que no deberían cambiar después de crearse. record te da eso de forma nativa: igualdad por valor, un ToString decente y with para copiar cambiando un campo.
csharp public record Cliente(string Nome, string Email);
Un objeto inmutable no puede modificarse por accidente en otro thread ni en otro método tres capas más abajo. Tener menos estado mutable circulando significa menos errores del tipo «pero yo no cambié eso». Reserva la class mutable para cuando realmente necesites identidad y ciclo de vida, no para cualquier contenedor de datos.
Configuración con IOptions, no con strings mágicos
Esparcir configuration["ChaveDaApi"] por el código es deuda garantizada: no hay tipos ni validación, y solo descubres que falta la clave cuando algo falla en runtime. Modela la configuración en una clase e inyecta IOptions<T>.
csharp builder.Services.AddOptions<EmailOptions>() .Bind(builder.Configuration.GetSection("Email")) .ValidateDataAnnotations() .ValidateOnStart();
ValidateOnStart es el detalle clave: una configuración inválida detiene la aplicación al arrancar, no en mitad de un envío a las dos de la madrugada. Si la configuración cambia en runtime, usa IOptionsSnapshot. Pero empieza por lo sencillo.
Logging estructurado: deja de concatenar strings
_logger.LogInformation("Pedido " + id + " criado") es un log que no puedes consultar correctamente. Usa placeholders: se convierten en campos indexados en tu backend de observabilidad:
csharp _logger.LogInformation("Pedido {PedidoId} criado para {ClienteId}", pedido.Id, cliente.Id);
Ahora puedes filtrar por PedidoId en Seq, Grafana o donde sea. Combina esto con ILogger<T> (que ya proporciona la categoría correcta) y con scopes para correlacionar requests, y el log deja de ser basura textual para convertirse en una herramienta de investigación.
Las buenas prácticas en .NET rara vez consisten en usar lo más nuevo de NuGet. Casi siempre consisten en usar lo que la plataforma ya ofrece, de la forma en que fue diseñada para utilizarse. El framework no te impedirá hacer un apaño, pero tampoco te obliga a hacerlo. La elección, como siempre, es tuya.


