Entrevista .NET: preguntas de C#, inyección de dependencias y código asíncrono
Quick Overview
Original .NET10.0.12 SDK10.0.401 real38checks: captive singleton root scope lifespan, escaped task disposed Lease vsawait-owned scope, async-before-await Task failure vsnonasync validation, realKestrelHttpClient linkedworktoken cancellation cooperate499effect0 vsignore200effect1. RequestAbortedfalse/no disconnect proof; memoryonly/noDBrollback/noGCload.
Una operación puede devolver un error y haber modificado datos. También puede seguir trabajando después de recibir una señal de cancelación. En una entrevista .NET, decir «uso async/await y servicios scoped» deja abiertas las preguntas decisivas: quién conserva cada dependencia, quién espera la operación y quién observa su resultado.
Aquí practicarás esas preguntas con un laboratorio original de C#: captura de un servicio scoped por un singleton, una Task que sobrevive a su scope y dos peticiones HTTP que reaccionan de manera distinta al mismo token cancelado. Puedes ampliar la práctica con las preguntas de C# y .NET para backend de PracHub, cuya cobertura incluye otros temas del ecosistema.
Límite de evidencia. Los hechos técnicos se apoyan en documentación oficial de Microsoft. Los resultados numéricos proceden del laboratorio adjunto; las recomendaciones de entrevista son inferencias de preparación. Las preguntas públicas de PracHub son material de práctica reportado, no una promesa de que una empresa exija C# o repita ese ejercicio. No usamos testimonios de candidatos para afirmar rondas, tiempos ni criterios actuales de contratación.

Empieza por el contrato de la operación
Imagina un endpoint que genera un informe. Recibe dependencias mediante DI, espera una consulta y finalmente escribe el resultado. Antes de elegir un lifetime, aclara si el informe debe terminar dentro de la petición o convertirse en trabajo independiente. Son contratos distintos, aunque ambos puedan emplear await.
Para el primer contrato, necesitas conservar los recursos hasta terminar la operación o atender su cancelación. Para el segundo, hacen falta un propietario del trabajo, datos de entrada independientes de HttpContext y una forma de observar errores. Lanzar una Task y devolver 202 no crea por sí solo una cola durable ni una política de recuperación. En este artículo ejecutamos el primer contrato y un contraejemplo de scope prematuramente cerrado; no construimos un worker de producción.
Una respuesta de entrevista puede comenzar así: «Primero decidiría quién debe completar el informe. Después dibujaría el scope y colocaría el último await antes de su liberación». Con ese orden puedes justificar cada API por el trabajo que debe conservar o completar.
DI: sigue la instancia, no solo su registro
Hecho oficial. La documentación de DI de .NET distingue transient, scoped y singleton. Scoped reutiliza una instancia dentro de un ámbito; singleton conserva una instancia del contenedor. En las peticiones HTTP de este laboratorio hay un scope por petición. «Scoped» no significa automáticamente «por hilo», ni cualquier aplicación .NET tiene exactamente la misma frontera de scope.
El laboratorio usa Lease, una clase IDisposable con una bandera Disposed. Su método Use lanza ObjectDisposedException si ya fue liberada. No representa una conexión real a base de datos: sirve para observar identidad, propietario y disposición sin confundirlos con rendimiento o garbage collection.
El registro peligroso es pequeño:
services.AddScoped<Lease>();
services.AddSingleton<BadOwner>();
public sealed class BadOwner(Lease lease)
{
public Lease Lease { get; } = lease;
}
BadOwner guarda la referencia a Lease durante toda la vida del singleton. Pedirlo desde un scope hijo no cambia el registro de singleton ni garantiza que su dependencia pertenezca a ese hijo. Activamos explícitamente ValidateScopes y ValidateOnBuild en un proveedor aislado: la construcción rechaza esa captura. No dependemos de que una plantilla o un entorno active la validación por defecto.
Con la validación desactivada deliberadamente, el resultado permite explicar el problema. Resolver Lease dos veces en el mismo scope devuelve la misma referencia; otro scope obtiene otra. Sin embargo, la Lease guardada por BadOwner es distinta de ambas y permanece idéntica al solicitar el singleton desde los dos hijos. Cerrar los hijos no la libera; cerrar el proveedor raíz sí.
Comparar referencias y observar Disposed permite saber qué ámbito conserva realmente la dependencia. No hemos medido un memory leak ni demostrado una carrera entre hilos. Inferencia de diseño: si esa dependencia guardara información específica de una petición, su vida prolongada podría mezclar responsabilidades; habría que revisar los datos concretos que conserva, no declarar inseguro cualquier singleton.
En una aplicación, revisa el constructor del singleton y las dependencias que arrastra. Cambiar todos los registros a singleton para evitar un error elimina una señal útil sin resolver el contrato. Para trabajo independiente que necesita dependencias scoped, un scope explícito puede ser apropiado si su propietario también espera el trabajo y lo dispone al terminar.
Una Task pendiente puede sobrevivir a un recurso liberado
El segundo fallo no necesita singleton. Creamos un scope, resolvemos Lease y empezamos un método que espera una puerta controlada. Salimos del using antes de abrir esa puerta:
Task<string> task;
using (var scope = provider.CreateScope())
{
var lease = scope.ServiceProvider.GetRequiredService<Lease>();
task = UseLater(lease, gate.Task);
}
gate.SetResult();
await task;
UseLater espera gate y después llama a lease.Use(). Justo antes de abrirla, la comprobación observa task.IsCompleted igual a false y Disposed igual a true. Al continuar, la Task falla con ObjectDisposedException. El await exterior observa el fallo, pero no resucita la dependencia ni prolonga retroactivamente el scope.
OwnUntilDone, incluido en Program.cs, mantiene el ámbito dentro del método que espera el trabajo. OwnUntilDone crea un scope con await using, resuelve Lease y espera la puerta dentro del mismo bloque. Antes de liberarla, el recurso sigue vivo. Las comprobaciones registran Disposed=false mientras la puerta está cerrada, el resultado «used» al abrirla y Disposed=true al terminar. No hace falta un retraso arbitrario para descubrir el error: la puerta fija el orden que queremos comprobar.
Una explicación breve sería: «La llamada devolvió una Task, pero el dueño del recurso terminó antes que ella. Mantengo el scope abierto hasta el último uso y espero el resultado antes de disponerlo». Después puedes discutir si esa responsabilidad pertenece al endpoint o a un trabajador independiente.

C# asíncrono: localiza dónde aparece la excepción
Hecho oficial. En métodos async que devuelven Task, las excepciones del cuerpo se almacenan en la Task y se observan al esperarla, según la documentación de valores devueltos asíncronos. Esto incluye una excepción lanzada antes del primer await dentro de ese método. No equivale a decir que cualquier llamada que devuelve Task tenga siempre esa conducta.
Nuestra fixture compara dos funciones. FailBeforeAwait es async Task y lanza InvalidOperationException antes de Task.Yield. La llamada entrega una Task ya fallida; await observa el mensaje «before await». ValidateBeforeTask es un método ordinario que valida el argumento antes de devolver Task.CompletedTask: un número negativo lanza ArgumentOutOfRangeException de forma síncrona.
En una revisión, pregunta dónde está exactamente el try/catch. Si rodea únicamente la creación de la Task, puede no observar un error que forma parte de su resultado. Si el método valida sin ser async, ese mismo límite de llamada sí puede recibir la excepción directamente. Mostrar las dos firmas evita explicar una regla más amplia que la evidencia.
Después concreta quién registra o transforma el fallo. El laboratorio solo comprueba su observación; no configura middleware global ni una política de reintentos. Reintentar una operación con efectos exige saber qué alcanzó a ejecutar, no limitarse a capturar Exception.
Cancelación HTTP: señal, cooperación y efecto son tres observaciones
Hecho oficial. La cancelación administrada de .NET es cooperativa: solicitarla comunica una intención y el código debe atenderla. HttpContext.RequestAborted permite transmitir la señal asociada a una petición. Tener acceso a un token no demuestra que cada operación lo use.
La parte HTTP inicia Kestrel en loopback y envía peticiones con HttpClient. Para controlar el experimento, enlaza RequestAborted con un token de trabajo que cancela el propio harness. La conexión permanece abierta: RequestAborted termina en false en ambos casos. Resultado y frontera: comprobamos cancelación controlada de trabajo dentro de peticiones reales; no comprobamos la detección de una desconexión de red.
Las dos rutas esperan una puerta antes de incrementar un contador en memoria. cooperate usa WaitAsync con el token enlazado; ignore espera la misma puerta sin pasarlo. Cancelamos el token después de confirmar que el handler ha empezado.
| Ruta del laboratorio | Tras cancelar el trabajo | Resultado al finalizar |
|---|---|---|
| cooperate | La espera atiende el token | HTTP 499, contador 0, scope dispuesto |
| ignore | Sigue pendiente y conserva su scope | Al abrir la puerta: HTTP 200, contador 1, scope dispuesto |
El código 499 es una elección de esta fixture para hacer visible la diferencia; no es una obligación de ASP.NET Core. Tampoco convertimos un contador en una prueba de rollback de pagos o de base de datos. La observación útil es que cancelar un token no impidió el incremento cuando esa ruta no lo atendió.
Si el entrevistador cambia el caso a una escritura ya confirmada, reconoce el límite: una señal posterior no deshace automáticamente el efecto. Tendrías que definir confirmación, idempotencia o compensación según el negocio y probar esas fronteras por separado. Aquí no están implementadas.
Ejecuta y defiende el recibo de 38 comprobaciones
Descarga el laboratorio completo. Requiere el SDK 10.0.401 fijado en global.json; la ejecución registrada utilizó .NET y ASP.NET Core 10.0.12. Desde la carpeta descomprimida, ejecuta dotnet run --project Lab.csproj. El programa escribe results.json y termina después de detener el servidor.
El recibo contiene 38 comprobaciones con valores observados y esperados. Incluye rechazo del singleton cautivo, resolución scoped desde la raíz, identidad entre scopes, disposición, Task fallida y las dos rutas HTTP. Los TaskCompletionSource controlan el orden; el watchdog de cinco segundos detecta una prueba bloqueada y no define el timeout de una API comercial.
El analizador emite avisos ASP0000 porque el harness construye proveedores aislados para comparar configuraciones. El README explica ese uso deliberado; no copies BuildServiceProvider dentro del registro de servicios de tu aplicación. Los binarios y directorios bin/obj quedan fuera del paquete.
Antes del ensayo, comprueba que puedes explicar estas tres fronteras sin atribuir al recibo más de lo que mide. Lease registra IDisposable, no comportamiento de Entity Framework. Los contadores viven en memoria; no prueban confirmación, rollback ni persistencia tras reiniciar el proceso. Dos peticiones dirigidas no constituyen una prueba de carga, de GC o de seguridad concurrente. Esa precisión hace que el ejemplo sea defendible cuando cambien las condiciones.
Cinco ejercicios para transferir el razonamiento
Material reportado de práctica. Estas preguntas verificadas de PracHub permiten trasladar el razonamiento a otros contratos backend. No son una lista específica de C# atribuida a las empresas. Explica en qué parte introducirías C# y qué comportamiento tendrías que probar antes de afirmar que funciona.
| Pregunta completa | Qué preparar desde este laboratorio |
|---|---|
| Design an async donation payment platform | Distingue terminar una petición de completar un pago y define quién observa el trabajo. |
| Design an exception monitoring system with top‑K | Explica quién recibe un error asíncrono y qué contexto conserva para investigarlo. |
| Design scheduled payments and cancellation | Separa cancelar trabajo pendiente de revertir un efecto confirmado. |
| Heap vs Stack Memory, Threads vs Processes, and Semaphores vs Mutexes | No confundas un lifetime de DI, un recurso dispuesto y memoria administrada. |
| Thread-Safe Buffered Batch Writer for Streaming Records into a Database | Define propietario, espera de finalización y liberación del escritor; añade pruebas concurrentes específicas. |
Siguiente práctica, por inferencia: toma el escritor por lotes y dibuja el scope de su dependencia. Marca cuándo acepta registros, quién espera el vaciado y qué ocurre si llega cancelación. Luego formula una comprobación por cada frontera. Habrás convertido preguntas generales de .NET en decisiones que puedes explicar y verificar.
Comments (0)