Entrevista técnica: Las 10 preguntas más comunes para nivel Semi-senior .NET
Familiarízate con estas preguntas y la romperás en tus entrevistas capo 🐿️🔥

Hey developer, se me ocurrió una idea, la de hacer una serie de artículos llamada "Entrevista técnica" donde basado en mi experiencia como entrevistador técnico y la de compañeros y developers de la comunidad he podido recopilar, si logras leer y aprender todas las preguntas de esta serie, créeme que la vas a romper. Vamos!

Estas son las 10 preguntas más comunes para posiciones de ingeniero de software .NET en el nivel semi-senior te las iré respondiendo una a una con ejemplos reales 😉. En otros artículos añadiré más preguntas y más niveles como junior, seniors, arquitectos y staff.

1. ¿Cómo funciona el Garbage Collector (GC) en .NET y qué es la "generación" de objetos?

El Garbage collector o colector de basura es un administrador de memoria automático en .NET que libera objetos no utilizados, evitando memory leaks. No necesitas llamar a delete como se hace por ejemplo en C++.

Generaciones (0, 1, 2):

  • Gen 0: Son objetos nuevos y efímeros (ej: variables en un bucle). El GC se ejecuta aquí con más frecuencia.
  • Gen 1: Aquí van los objetos que sobrevivieron una recolección en Gen 0.
  • Gen 2: Estos son los objetos de larga vida (ej: singletons, cachés).

Cómo funciona:

  1. Cuando Gen 0 se llena, el GC inicia una recolección.
  2. Los objetos vivos se promueven a Gen 1.
  3. Si Gen 1 también se llena, el GC recolecta Gen 1 y Gen 0.
  4. Objetos en Gen 2 solo se recolectan en recolecciones "full GC" (menos frecuentes).

Consideraciones clave:

  • Evitar fugas: Asegurar que objetos no se mantengan referenciados innecesariamente (por ejemplo eventos no desuscritos).
  • Objetos no administrados: Si usas recursos como archivos o conexiones, implementa IDisposable y usa using.
  • Entender el GC ayuda a optimizar aplicaciones y evitar cuellos de botella en memoria. Por ejemplo, si una API crea millones de objetos efímeros por request, podrías reutilizarlos (pooling) para reducir presión en Gen 0.

2. ¿Qué ventajas tiene usar async/await frente a hilos tradicionales?

No bloquea hilos:

  • Hilos tradicionales (como el Thread o BackgroundWorker) consumen un hilo del pool durante toda la operación, incluso si está inactivo (ej: esperando una respuesta de BD).
  • async/await libera el hilo mientras espera I/O (por ejemplo una llamada HTTP o consulta SQL), permitiendo que se reutilice para otras tareas.

Escalabilidad:

  • En servidores (APIs, aplicaciones web), un endpoint async puede manejar miles de solicitudes concurrentes con pocos hilos.

Evita "Thread Pool Starvation":

  • Si usas hilos tradicionales para muchas operaciones bloqueantes (ej: 1000 requests simultáneas), agotas el pool de hilos y la aplicación colapsa.
  • Con async/await, los hilos solo se usan activamente, no durante la espera.

Manejo simplificado de errores:

  • Con async/await, usas try/catch como en código síncrono. Con hilos tradicionales, debes manejar excepciones en callbacks o Task.Result (que puede causar deadlocks).
    public async Task<ActionResult> GetUserData(int userId) {
        var data = await _dbContext.Users.FindAsync(userId); // Aquí libera el hilo haciendo que no se bloquee
        return Ok(data);
    }
    

    3. ¿Cómo implementarías Dependency Injection (DI) para un servicio con estado en .NET Core?

    Un servicio o componente "con estado" (stateful) es aquel que mantiene información o datos entre diferentes invocaciones o interacciones. En otras palabras, el resultado de una operación puede depender de interacciones previas.

    Para implementar Dependency Injection en un servicio con estado en .NET Core, debes elegir el ciclo de vida adecuado según el contexto. Si el servicio necesita mantener un estado compartido entre múltiples usuarios o solicitudes (como un caché en memoria), usa Singleton junto con estructuras thread-safe para evitar condiciones de carrera.

    Por ejemplo, un servicio que almacena datos temporales podría usar ConcurrentDictionary para garantizar acceso seguro desde diferentes hilos. Si el estado es temporal y solo debe persistir durante una solicitud HTTP (como información del usuario autenticado), usa Scoped, que crea una instancia por request.

    Ejemplo real: Imagina un servicio que rastrea métricas en tiempo real para un dashboard. Usarías un Singleton con un diccionario seguro para actualizar y leer métricas desde múltiples requests sin bloquear la aplicación:

    public class MetricsTracker {  
        private readonly ConcurrentDictionary<string, int> _metrics = new();  
        public void Increment(string metricName) => _metrics.AddOrUpdate(metricName, 1, (k, v) => v + 1);  
    }  
    // Registro en Program.cs:  
    builder.Services.AddSingleton<MetricsTracker>();
    

    4. ¿Cuándo usarías IEnumerable vs IQueryable en EF Core?

    Usa IQueryable cuando necesites filtrar, ordenar o paginar datos directamente en la base de datos, especialmente con grandes volúmenes de información. Esto genera consultas SQL optimizadas y evita transferir datos innecesarios. Usa IEnumerable cuando trabajes con colecciones ya cargadas en memoria o necesites operaciones que solo se ejecuten en el cliente (ej: métodos personalizados que no pueden traducirse a SQL).

    // Caso IQueryable (recomendado):  
    var usuariosActivos = _dbContext.Users  
        .Where(u => u.IsActive && u.FechaRegistro.Year == 2025) // Filtro en SQL  
        .Skip(10).Take(20)  
        .ToList(); // SQL: SELECT ... WHERE IsActive = 1 AND YEAR(FechaRegistro) = 2025 OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY  
    
    // Caso IEnumerable (usar con precaución):  
    var todosUsuarios = _dbContext.Users.ToList(); // Carga TODOS los usuarios  
    var filtrados = todosUsuarios  
        .Where(u => u.Nombre.Contains("a")) // Filtro en memoria (no escala)  
    

    Como regla práctica: Si la operación puede hacerse en la base de datos, usa IQueryable. Si los datos ya están en memoria o usas lógica compleja en C#, usa IEnumerable.

    5. ¿Qué es Middleware en ASP.NET Core y cómo crearías uno personalizado?

    Middleware es un componente que procesa solicitudes y respuestas en una aplicación ASP.NET Core. Se organiza en una cadena donde cada middleware puede manipular la petición antes de pasarla al siguiente o generar una respuesta directamente. Ejemplos comunes: autenticación, logging, manejo de errores.

    Un ejemplo común es manejar errores globalmente.

    public class ErrorHandlingMiddleware {  
        private readonly RequestDelegate _next;  
    
        public ErrorHandlingMiddleware(RequestDelegate next) => _next = next;  
    
        public async Task InvokeAsync(HttpContext context) {  
            try {  
                await _next(context);  
            }  
            catch (Exception ex) {  
                context.Response.StatusCode = 500;  
                await context.Response.WriteAsJsonAsync(new {  
                    Error = "Algo falló",  
                    Details = (context.Request.IsDevelopment()) ? ex.Message : null  
                });  
            }  
        }  
    }  
    
    // Registro en Program.cs:  
    app.UseMiddleware<ErrorHandlingMiddleware>();
    

    Este middleware captura excepciones no controladas y devuelve una respuesta JSON estructurada, útil para APIs.

    6. Explica un principio SOLID que hayas aplicado en un proyecto real

    En principio Single Responsibility Principle (SRP) establece que una clase debe tener una única razón para cambiar. Por ejemplo, evita que una clase maneje lógica de negocio, validación y acceso a datos simultáneamente. En un proyecto de gestión de pedidos, refactoricé una clase OrderProcessor que originalmente validaba datos, calculaba impuestos y guardaba en base de datos. La dividí en:

    • OrderValidator (solo validaciones),
    • TaxCalculator (cálculo de impuestos),
    • OrderRepository (persistencia en BD).
    // Antes (viola SRP):  
    public class OrderProcessor {  
        public void Process(Order order) {  
            if (order.Total < 0) throw new Exception("Inválido"); // Validación  
            var tax = order.Total * 0.19m; // Lógica de impuestos  
            _dbContext.Orders.Add(order); // Persistencia  
        }  
    }  
    
    // Después (cumple SRP):  
    public class OrderValidator {  
        public void Validate(Order order) => ...  
    }  
    public class TaxCalculator {  
        public decimal Calculate(Order order) => ...  
    }  
    public class OrderRepository {  
        public void Save(Order order) => ...  
    } 
    

    7. ¿Cómo manejarías concurrencia en EF Core (ej: optimistic locking)?

    El optimistic locking evita conflictos al asumir que las actualizaciones rara vez chocan. En EF Core, se implementa usando tokens de concurrencia (ej: una columna RowVersion en SQL). Si dos usuarios modifican el mismo registro, EF Core lanza DbUpdateConcurrencyException al detectar que el RowVersion actual no coincide con el original.

    Ejemplo real (gestión de inventario)

    // Modelo:  
    public class Product {  
        public int Id { get; set; }  
        public string Name { get; set; }  
        public int Stock { get; set; }  
        [Timestamp] // Marca la columna como token de concurrencia  
        public byte[] RowVersion { get; set; }  
    }  
    
    // Código al actualizar:  
    try {  
        var product = _dbContext.Products.Find(productId);  
        product.Stock -= quantity;  
        await _dbContext.SaveChangesAsync(); // Si otro usuario ya modificó el producto, lanza excepcion  
    }  
    catch (DbUpdateConcurrencyException ex) {  
        // Opciones: Recargar el registro, notificar al usuario, o mergear cambios  
        await ex.Entries[0].ReloadAsync();  
        product.Stock -= quantity; // Reintentar la operación  
        await _dbContext.SaveChangesAsync();  
    }  
    

    Evita que dos usuarios sobrescriban cambios accidentalmente, crítico en sistemas como reservas de stock o transacciones financieras.

    8. ¿Qué diferencias hay entre JWT y OAuth en una API .NET?

    JWT (JSON Web Token) es un formato estándar para transmitir información firmada o encriptada entre partes (ej: un token que contiene datos del usuario). OAuth 2.0 es un protocolo de autorización que define cómo obtener tokens de acceso (usando flujos como Authorization Code o Client Credentials). En APIs .NET, JWT suele usarse como el formato del token generado por OAuth.

    OAuth es el "proceso" para obtener acceso, JWT es el "formato" del token.

    9. ¿Cómo optimizarías una API lenta en .NET (herramientas y técnicas)?

    Primero, se debería identificar el cuello de botella con herramientas como Application Insights o logs detallados. Casos comunes:

    • Queries lentas: Usa AsNoTracking() en EF Core para consultas de solo lectura, evita Include innecesarios, o optimiza índices en la base de datos.
    • Falta de caché: Implementa IMemoryCache para datos estáticos (ej: listas de países) o Redis para datos compartidos entre instancias.
    • Código bloqueante: Asegura que toda operación que no depende de la aplicación como archivos, bases de datos o llamadas a otros API sean async/await para no bloquear hilos.
    // Antes (lento):  
    var orders = await _dbContext.Orders  
        .Include(o => o.Items)  
        .ThenInclude(i => i.Product)  
        .Where(o => o.CreatedAt.Year == 2025)  
        .ToListAsync();  
    
    // Después (optimizado):  
    var orders = await _dbContext.Orders  
        .AsNoTracking() // Sin tracking de cambios, se usa en operaciones de lectura
        .Where(o => o.CreatedAt.Year == 2025)  
        .Select(o => new OrderDto { // Proyecta solo los datos necesarios  
            Id = o.Id,  
            Total = o.Total,  
            ProductNames = o.Items.Select(i => i.Product.Name).ToList()  
        })  
        .Take(100) // Paginación  
        .ToListAsync();  
    

    10. ¿Qué es CQRS y en qué escenarios lo recomendarías?

    CQRS (Command Query Responsibility Segregation) es un patrón que separa las operaciones de lectura (Query) y escritura (Command) en modelos distintos. Recomiéndalo cuando:

    1. Lecturas y escrituras tienen necesidades técnicas diferentes (ej: escrituras con validaciones complejas vs lecturas que requieren alta velocidad).
    2. La escalabilidad es crítica (ej: APIs con miles de lecturas por segundo y pocas escrituras).
    3. Necesitas proyecciones de datos específicas (ej: un dashboard que agrega datos de múltiples fuentes).

    Ejemplo real:
    En una aplicación de comercio electrónico:

    • Escritura: Usa un modelo estricto para crear pedidos (validación de stock, cálculo de impuestos).
    • Lectura: Usa un modelo denormalizado (ej: datos de pedidos + historial de cliente) almacenado en una base de datos optimizada para consultas rápidas (como Redis o Cosmos DB).
    // Comando (Escritura):  
    public class CreateOrderCommand : IRequest<Guid> {  
        public int ProductId { get; set; }  
        public int Quantity { get; set; }  
    }  
    
    public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid> {  
        public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct) {  
            // Lógica de validación y persistencia  
        }  
    }  
    
    // Consulta (Lectura):  
    public class GetOrderQuery : IRequest<OrderDto> {  
        public Guid OrderId { get; set; }  
    }  
    
    public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto> {  
        public async Task<OrderDto> Handle(GetOrderQuery request, ct) {  
            // Consulta optimizada desde una vista precalculada  
        }  
    }  
    

    Y bien estimado dev eso es todo por el momento, ya sabes, practica.

    Se vendrán más artículos de este tipo y estarán agrupados todos en la sección Entrevistas técnicas.

    Y si este artículo te ha encantado, compártelo! 🐿️😉

    Créditos de foto de portada: Basado en Foto de Sameer en Unsplash

    Un comentario en «Entrevista técnica: Las 10 preguntas más comunes para nivel Semi-senior .NET»

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *