Formato confirmado — martes 8 sep, 8:00 am CDMX.
En vivo, pantalla compartida, tu editor. Construyes 3 piezas: (1) una consulta SQL a mano, (2) un método de servicio .NET que la ejecuta, (3) el endpoint que la expone. Más una parte hablada de Angular. Guion completo en la pestaña La prueba.
Lo que NO necesitas: proyecto suyo, su base de datos, compilar, ejecutar. Un .cs suelto basta. Te dan las firmas de sus clases por chat — no se mide memoria. Sin documentación, internet ni IA: apaga Copilot / la extensión de Claude antes de entrar.
| # | Foco | Por qué | Dónde |
|---|---|---|---|
| 1 | Joins + paginación (OFFSET/FETCH y keyset) + transacciones en SQL Server | Es la pregunta que decide | Pestaña SQL |
| 2 | Acceso a datos "a mano": Dapper / ADO.NET con IDbTransaction | "La mitad del trabajo" según ellos | Pestaña SQL |
| 3 | AWS→Azure: tabla de equivalencias y no-equivalencias | Pregunta 4, tu punto débil de cloud | Pestaña Cloud |
| 4 | Respuesta honesta y ensayada sobre tu inglés | Pregunta 5, la van a testear | Pestaña Preguntas |
| 5 | Native Federation: beneficios y dolores concretos, poder dibujarlo | Pregunta 2, tu fuerte — hay que lucirlo | Pestaña Angular |
Red de seguridad: si no pasa la prueba de SQL, sigues siendo candidato fuerte para el puesto de Front End Developer que NEWCO también tiene abierto. No es un fracaso — pero el objetivo es Full-Stack.
Hoy es jueves 3 — según tu propio plan (pestaña Plan al 8 sep), hoy toca "las 3 piezas completas + la voz". Empieza por Guion completo — es la versión de una pantalla de lo que vas a teclear el martes, con los trucos para que se quede pegado. La prueba es donde vive el porqué de cada línea; vuelve ahí solo cuando algo no cuadre.
Todo lo que necesitas el martes vive en esta única pestaña: las 3 piezas de código, la parte hablada de Angular y tu anécdota de testing — comprimido a lo que de verdad tienes que traer en la cabeza y decir en voz alta. Es arriesgado apoyarte solo en esto en vez del detalle completo, pero está pensado para poder seguirla casi como un guion en vivo. El razonamiento largo de cada línea sigue en sus pestañas propias (SQL, Backend .NET, Angular, Testing, Preguntas) — vuelve ahí solo cuando algo no cuadre.
Por qué te cuesta memorizarlo: no es un texto, son decisiones técnicas encadenadas. Un texto se memoriza repitiéndolo; una decisión se memoriza entendiendo qué pasa si la tomas al revés. Por eso cada bloque trae su "si no lo haces así, se rompe esto" o la frase exacta que la defiende — ese es el gancho que se queda, no la sintaxis.
Todo el ejercicio son bloques que caen en el mismo orden siempre. Si memorizas el orden, cada bloque te recuerda al siguiente — es la misma "costura" que ya usas en SEUS (Interfaz→Service→Assembly→Promise), un peldaño más largo.
/* */, en español antes que en C#: SELECT → FROM+JOIN → WHERE → ORDER BY → OFFSET → COUNT. La pieza que decide.const string, conexión, QueryAsync, devuelves la lista.[HttpGet], validas el rango (400), delegas al servicio, Ok(result) / NotFound().No hay hueco entre lo que tecleas y lo que dices: vas diciendo la frase de la fila mientras escribes esa línea, no antes ni después. Cuando termines una fila, ya estás diciendo la siguiente sin pausa.
| Tecleas | Dices exactamente en ese momento |
|---|---|
SELECT o.OrderId, o.OrderDate, | "Empiezo por el SELECT — columnas con alias claros para que el DTO mapee directo." |
p.Sku, p.Name AS ProductName, ol.Quantity, ol.UnitPrice | "El total lo voy a calcular después, no lo guardo como columna — se desincroniza si cambia el precio." |
FROM dbo.OrderLine ol | "Arranco el FROM por la tabla más fina, OrderLine, y le voy a colgar el resto encima." |
JOIN dbo.[Order] o ON o.OrderId = ol.OrderId | "Join a Order por OrderId." |
JOIN dbo.Product p ON p.ProductId = ol.ProductId | "Y join a Product, para el SKU y el nombre — mis 3 tablas, nada más." |
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive | "El filtro de fecha lo hago semiabierto — mayor o igual, menor estricto. Nunca BETWEEN: si OrderDate tiene hora, BETWEEN pierde el último día completo." |
ORDER BY o.OrderDate DESC, o.OrderId DESC, ol.OrderLineId DESC | "El ORDER BY termina en una columna única, OrderLineId — OFFSET/FETCH lo exige. Sin desempate único, la página 2 repite o se salta filas." |
OFFSET (@pageIndex * @pageSize) ROWS FETCH NEXT @pageSize ROWS ONLY; | "Y pagino con OFFSET/FETCH. Si esperara scroll infinito con volumen alto, usaría keyset en vez de esto." |
| Tecleas | Dices exactamente en ese momento |
|---|---|
| "Empiezo por la interfaz — así el controller depende de una abstracción, no de Dapper directo, y la puedo mockear en un test sin tocar SQL Server." |
| "La firma recibe los mismos parámetros del filtro más un CancellationToken — lo voy a pasar hasta el fondo." |
| "La implementación recibe la cadena de conexión por configuración, inyectada por constructor." |
| "Es async — libero el hilo mientras espera la base, no lo bloqueo. El SQL va en una constante, el mismo bloque de arriba." |
| "Abro la conexión con await using para que se cierre sola. Los parámetros van en un objeto anónimo — nunca concateno strings, eso es inyección SQL." |
| "Y el CancellationToken llega hasta acá: si el cliente cierra la petición, dejo de pegarle a la base a medio camino." |
| Tecleas | Dices exactamente en ese momento |
|---|---|
| "[ApiController] activa la validación automática de modelo — un 400 gratis si el binding falla, sin código extra mío." |
| "Inyecto la interfaz, no la implementación concreta — mismo motivo que en el servicio." |
| "ActionResult<T> porque necesito variar el código de estado — 200, 404 o 400 — con un tipo de retorno plano no puedo." |
| "Valido el paginado antes de tocar la base — 400 si viene mal, ni siquiera llego al servicio." |
| "Delego al servicio, y devuelvo 404 si no hay filas, 200 con los datos si sí las hay." |
builder.Services.AddScoped<IOrderService, OrderService>(); | "Y en Program.cs lo registro como Scoped — una instancia por request, correcto para algo que abre una conexión a base de datos." |
CatalogoController, BRGuardarPedido) devuelve RespuestaGenericaDto<T> directo, sin ActionResult, y buena parte es síncrono. Respuesta en tres frases, sin disculparte: "Ese es un contrato interno pre-existente — siempre 200 HTTP, el éxito o error va en el body. Es consistente entre decenas de módulos, y es correcto para ese ecosistema. Sé cuál es el estándar REST y por qué lo uso hoy: ActionResult + códigos de estado explícitos." Detalle completo en la pestaña Backend .NET.
| Te preguntan… | Tu línea, lista para decir |
|---|---|
| Observable vs Promesa | "Observable: 0..n valores, lazy, cancelable — para streams y HTTP que puede abortarse. Promise: un valor, eager, no cancelable — para un paso secuencial de un solo disparo. En MasterGuardarPedidoService uso async/await sobre Promises porque es validar-y-si-pasa-enviar: nada que cancelar, nada que recombinar." |
Un caso real de forkJoin | "Un cotizador que dispara 7-8 llamadas en paralelo — existencia, precio, validación RARF, alertas, lealtad. forkJoin porque son peticiones de una sola respuesta y necesito esperarlas a todas juntas; catchError al final, no por llamada, porque es una unidad de negocio: si una falla, falla el cotizador completo." |
| Native Federation — qué aportó | "Sostengo más de una docena de microfrontends: deploy independiente por equipo, cada uno con su remoteEntry mapeado en federation.manifest.json, dependencias compartidas (Angular, RxJS, Material) negociadas una sola vez con la config shared." |
| Native Federation — qué costó | "Version skew: un remoto con otra versión de Angular o Material y terminas con dos instancias de Angular cargadas, NG0203 al inyectar fuera de contexto. Se alinea con singleton: true y strictVersion en shared. También comunicación entre microfrontends — no hay solución integrada, es custom events o un bus de estado." |
MasterGuardarPedidoService — guardar pedido manual. Cubre tres ramas: si la validación falla no sigue y llama al manejador de errores; si el negocio no permite generar la solicitud, oculta el spinner y corta; y el camino feliz hasta enviarlo. TestBed con el servicio solo (sin componente), sus 4 dependencias como jasmine.SpyObj, y resolveTo() en los spies porque el servicio es async/await, no Observable."| Te preguntan… | Tu ancla de una línea |
|---|---|
| "¿Y la transacción?" | "Esto es un SELECT, no la necesita. La necesitaría en una escritura de varias tablas: crear pedido + renglones + descontar stock." |
| "¿Y si son 100,000 filas?" | "OFFSET grande relee y descarta; para eso, keyset — filtro por la última fila vista." |
| "¿Cómo lo probarías?" | "Un test del controller mockeando IOrderService — no toca SQL Server. Igual que hice con MasterGuardarPedidoService." |
Recuperación activa, no relectura. Leer esta pestaña otra vez se siente productivo y no lo es. Lo que funciona: pantalla en blanco, recitas de memoria antes de mirar. Fallar y corregir es lo que fija el recuerdo.
Dilo y tecléalo a la vez (doble codificación). La voz usa una memoria distinta a la de los dedos. Por eso cada bloque trae su frase exacta: si solo tecleas en silencio, memorizas sintaxis suelta; si narras mientras tecleas, memorizas una decisión con su razón.
Trocea en bloques con nombre, no en líneas sueltas. Cuando se te vaya algo en vivo, no busques la línea — pregúntate "¿en qué bloque de los 9 estoy" y esa frase te devuelve el código.
Intercala, no repitas lo mismo 3 horas. SQL → backend → Angular → testing → SQL otra vez rinde más que 3 horas seguidas de lo mismo. El cerebro fija mejor cuando alterna que cuando satura un solo tema.
Enseña el porqué a un principiante. Si puedes explicarle a alguien que nunca vio esto por qué no usas BETWEEN, o por qué esto es Promise y no Observable, lo tienes de verdad. Memorizar sintaxis se olvida con los nervios; entender el porqué no.
SQL Server + .NET SDK + Angular CLI ~20 montados y probados · solución scratch pre-creada · BD de práctica sembrada (tablas del bloque 1) · screen share y audio probados · Copilot / IA apagados · correo de Alberto releído una vez el lunes, no el martes en la mañana.
Regla de uso de esta pestaña: practícala completa, de punta a punta, sin mirar, al menos dos veces antes del martes. Si un bloque no te sale de memoria, es la señal de ir a su pestaña completa (SQL / Backend .NET / Angular / Testing) a repasar el porqué — no a memorizar más fuerte el mismo bloque.
Martes 8 sep, 8:00 am CDMX. Pantalla compartida, tu editor, un fichero suelto. No compila nada. Se valora el código y lo que dices mientras lo escribes.
El correo de Alberto, en claro:
Cómo hablas — 3 reglas (feedback de tu ex jefe, aplican a TODO lo hablado).
debounceTime y un hash de petición descartamos las respuestas viejas."OnPushfederation.manifest.json · el import mapSERIALIZABLE · un bloqueo de rangoPrepara tu ejemplo AHORA — no lo inventes en vivo. El dominio y las tablas los eliges tú; las firmas de código te las dan por chat y te adaptas (sección abajo). Llega con el esquema decidido y practicado: un reporte con JOIN de 3 tablas + filtro de fecha + paginación. Mantenlo mínimo — 3 tablas, sin adornos:
[Order] (OrderId PK, OrderDate datetime2, Status)
Product (ProductId PK, Sku, Name)
OrderLine (OrderLineId PK, OrderId FK→[Order], ProductId FK→Product, Quantity, UnitPrice)
Al empezar, tú lo anuncias: "Voy a hacer un reporte de líneas de pedido en un rango de fechas, paginado. Contra estas tres tablas —las dibujo—: un pedido, sus líneas, y el producto de cada línea. Si prefieren otra cosa, díganme."
El ejemplo ya está montado con datos reales — para practicar la consulta contra una BD de verdad (en la prueba no la tendrás, pero para ensayar sí ayuda):
datos-prueba.sql → crea la BD PruebaCocoMarch en localhost\SQLEXPRESS con las 3 tablas y datos de Vita Tienda (10 productos, 20 pedidos entre dic-2025 y feb-2026, 37 líneas). Ya ejecutado. Re-crear: sqlcmd -S "localhost\SQLEXPRESS" -E -C -i datos-prueba.sqlverificar.sql → la consulta de la prueba corriendo, más la demo del rango semiabierto (pedido del 31-ene 23:30 entra, el del 1-feb 00:00 no) y de la paginación determinista.prueba.cs (Escritorio) → arranca con el esqueleto en blanco (encabezado + using + los 3 marcadores). El objetivo completo está en "El archivo completo" abajo.reset-prueba.bat → deja prueba.cs en blanco y lo abre en VS Code. Tecleas la solución narrando, ejecutas verificar.sql para comprobar, y repites. reset-bd.bat recrea la BD si ensayaste las variaciones de INSERT.Server=localhost\SQLEXPRESS;Database=PruebaCocoMarch;Trusted_Connection=True;TrustServerCertificate=TrueDatos pensados para lucir el filtro: con fromDate = 2026-01-01 y toExclusive = 2026-02-01 salen 22 líneas de enero; diciembre y febrero quedan fuera, y el pedido a medianoche del 1-feb demuestra por qué < @toExclusive y no BETWEEN.
Haces: VS Code ▸ Ctrl+Shift+X ▸ escribe "Claude" ▸ en Claude Code pulsa Disable. Repite con "Copilot" si aparece. Luego Ctrl+Shift+P ▸ "Reload Window".
Por qué: Alberto lo pide explícito y en el screen share se nota si un autocompletado fantasma te sugiere líneas enteras. Que no haya ninguna duda de que el código es tuyo.
Truco: conviértelo en el primer gesto físico de tu ritual de entrada — como abrocharte el cinturón. Hazlo siempre en cada ensayo, en el mismo orden, y el martes las manos lo hacen solas sin que lo pienses.
Haces: Ctrl+N (archivo nuevo) ▸ Ctrl+S ▸ guárdalo como prueba.cs en el Escritorio.
Por qué: "un fichero suelto" es literal — no abras carpeta, no crees proyecto, no hay que compilar. La extensión .cs es solo para que VS Code lo coloree y se lea ordenado en pantalla.
Truco: practica siempre con el mismo reset-prueba.bat — ver el archivo en blanco una y otra vez es la señal que tu cerebro asocia con "ahora toca recitar sin mirar".
Haces: el editor ocupando ~⅔, y la ventana de la reunión con el chat abierto en el ⅓ restante (o en un segundo monitor). Ahí van a pegar las firmas.
Por qué: si tienes que Alt+Tab cada vez que pegan algo, pierdes el hilo y se te ve titubear. Con el chat siempre a la vista, copias y sigues.
Truco: ensaya con esta misma distribución de pantalla desde ya — el cerebro asocia el layout con la tarea, y llegar a un layout ya familiar el martes quita un nervio de encima.
Haces: auriculares con micro (no el del portátil). Entra 2 min antes, comparte una pantalla de prueba, confirma que se te oye.
Por qué: "si te quedas en silencio no podemos valorar lo que haces" — el audio es la evaluación. Un corte de sonido a mitad te cuesta la prueba.
Truco: en cada corrida de práctica grábate la voz (ya lo pide la Corrida 3 de tu plan) — oírte a ti mismo narrando es lo que te va a decir si de verdad puedes sostener 15 minutos hablando sin apagones.
Dices: "Perfecto. Entonces necesitan tres cosas: una consulta SQL a mano, un método de servicio en .NET que la ejecute, y el endpoint que la expone. Trabajo en este fichero suelto y les voy narrando. ¿Correcto?"
Por qué: confirmas que entendiste, fijas el marco, y les cedes el turno para matizar antes de teclear nada.
Truco: es la única frase que puedes dejar casi memorizada palabra por palabra — di siempre las mismas 3 piezas en el mismo orden (SQL · método · endpoint) y se vuelve automática de tanto repetirla igual en cada ensayo.
Dices: "Como el dominio lo elijo yo, voy con un reporte de líneas de pedido en un rango de fechas, paginado — ejercita bien el JOIN, el filtro y la paginación. Contra tres tablas: un pedido, sus líneas, y el producto de cada línea. Si prefieren otra cosa, díganme."
Por qué: propones, no preguntas "¿qué quieren?". Eliges un caso realista y explicas por qué luce lo que evalúan. Dejas la puerta abierta por si tienen preferencia.
Truco: usa siempre el mismo ejemplo en cada ensayo — pedido/línea/producto. Cambiar de dominio cada vez que practicas es la razón número uno por la que "se te olvida": no repites el guion, inventas uno nuevo cada vez.
Escribes:
// Prueba tecnica - reporte de lineas de pedido por rango de fechas, paginado
//
// Mis 3 tablas:
// [Order] (OrderId PK, OrderDate, Status)
// Product (ProductId PK, Sku, Name)
// OrderLine (OrderLineId PK, OrderId -> [Order], ProductId -> Product, Quantity, UnitPrice)
//
// Firmas que me pasen por el chat:
// (pegar aqui)
Dices: "Dejo el esquema escrito para tenerlo a la vista, y un hueco para las firmas que me pasen."
Por qué: el esquema en pantalla te ancla y les enseña el modelo. El hueco evita que pierdas las firmas en el scroll del chat.
Truco: tecléalo tú a mano en cada práctica, nunca lo copies. Son 5 líneas cortas — el punto no es ahorrarte 10 segundos, es que teclearlo es lo que lo fija (doble codificación, pestaña Guion completo).
Por qué: traduces un requerimiento a una spec — es literal lo que pide el puesto. Y te ahorra rehacer a mitad.
Truco: no memorices las 3 preguntas literales — memoriza que siempre preguntas 2 antes de teclear. El hábito importa más que el texto exacto; en vivo puedes improvisar la redacción si tienes el reflejo de parar y preguntar.
Dónde trabajas de aquí en adelante: un solo archivo, prueba.cs, abierto en VS Code. Nada más. No abres SSMS, no ejecutas nada, no hay proyecto — el correo lo dice: "no hace falta que compiles ni ejecutes". (SSMS y la BD PruebaCocoMarch fueron solo para tu práctica de estos días.)
La consulta SQL se escribe a mano dentro de prueba.cs: primero como un bloque de comentario /* … */ para pensarla, y luego la copias a una cadena de C# en el método. Así queda el archivo:
prueba.cs
├─ // encabezado: esquema + hueco para firmas ← Paso 1.3 y 2.2
├─ /* ---- La consulta ---- ... ---- fin ---- */ ← Paso 3 (SQL a mano)
├─ using System.Data; using Dapper; ... ← Paso 4.1
├─ public class OrderReportService(...) { ... } ← Paso 4 (método de servicio)
├─ [ApiController] public class OrderReportController(...) { ... } ← Paso 5 (endpoint)
└─ public record OrderLineRow(...); public record PagedResult<T>(...); ← al final (si no te dan los tipos)
Las 3 piezas que siguen son un esqueleto invariante. Lo único que cambia con sus firmas son nombres y formas. Ese ajuste es la habilidad.
PagedResult<T> o como lo llamen), a veces un objeto de query y/o una interfaz de servicio.Truco: no memorices "qué te pueden dar" como una lista — memoriza que siempre son las mismas 4 piezas (DTO, resultado, query, interfaz) y que cada una tiene un hueco fijo esperándola (tabla 2.4). Reconocer la pieza es más fácil que recordar la lista.
prueba.cs. Que se vea en pantalla — es tu contrato.GetOrderLinesAsync, devuelve Task<PagedResult<OrderLineDto>> y recibe ReportQuery. OrderLineDto tiene OrderLineId, OrderId, OrderDate, ProductName, LineTotal. ¿Lo leo bien?" — gana tiempo y demuestra que lo miraste.Truco: practica el read-back como una fórmula fija: "Ok, entonces mi método es X, devuelve Y, recibe Z." Memoriza la forma de la frase, no un ejemplo — así te sirve sin importar qué firma te den.
| Te dan… | Preguntas |
|---|---|
| Un DTO / fila | ¿LineTotal/Amount lo calculo yo (Quantity*UnitPrice) o esperan una columna? ¿El nombre de cada propiedad es tal cual el que pongo en el AS? (Dapper mapea por nombre.) Si trae menos campos de los que yo traería → recorto el SELECT. Si no trae OrderLineId y sí un SUM/Total → "¿esto es agregado por pedido? Entonces GROUP BY" (poco probable si anuncié líneas de pedido, pero lo confirmo). |
| Un objeto de query | ¿Page/PageNumber es base 0 o base 1? (cambia el OFFSET.) ¿La fecha de fin es exclusiva o inclusiva? ¿PageSize trae tope o lo pongo yo? |
| Un tipo de resultado | ¿El campo de conteo (Total/Count/TotalCount) es int o long? ¿record posicional o clase con propiedades? ¿Qué campos lleva — solo Items+Total, o también PageIndex/PageSize/TotalPages/HasNext que yo tengo que derivar? |
| Una interfaz de servicio | ¿La implemento en una clase concreta (asumo que sí)? ¿Lleva CancellationToken? ¿Cómo llega la conexión — IDbConnection inyectado, IConfiguration, connection string suelto? (asumo constructor y lo digo) |
Truco: no memorices las preguntas — memoriza una sola pregunta madre por firma: "¿qué pasa si esto no cuadra con mi base?" Cada fila de la tabla es esa misma pregunta aplicada a una pieza distinta.
| Firma | Dónde encaja |
|---|---|
| Campos del DTO | Los alias del SELECT — Dapper mapea por nombre, cada alias debe coincidir exacto. Y el ReadAsync<SuDto>(). |
| Objeto de query | Los parámetros del método (ReportQuery q en vez de 4 sueltos) y el binding del endpoint ([FromQuery] ReportQuery q). Dentro usas q.From… |
| Tipo de resultado | El return new SuTipo(...) y el ActionResult<SuTipo> del endpoint. |
| Interfaz de servicio | public class OrderReportService : ISuInterfaz + la firma del método copiada literal. |
| Nombre del método | Ese, tal cual. No lo "mejores". |
Truco: esta tabla es la más importante para memorizar de toda la prueba, porque es la que te salva si te dan firmas distintas a las que practicaste. Apréndela por columna derecha: "DTO → alias del SELECT", "query → parámetros", "resultado → el return", "interfaz → el : I...".
Tu esqueleto no cambia. Sus firmas solo rellenan estos 4 huecos con otros nombres y formas. Míralo así:
// (1) INTERFAZ + (2) NOMBRE Y PARÁMETROS DEL MÉTODO ← te los dan literal
public class OrderReportService(string connectionString) : IOrderReport
{
public async Task<PagedResult<OrderLineRow>> GetOrderLinesAsync(
DateTime fromDate, DateTime toExclusive, int pageIndex, int pageSize /*, CancellationToken ct*/)
{
const string sql = @"SELECT ol.OrderLineId, o.OrderId, o.OrderDate,
p.Name AS ProductName, ol.Quantity, ol.UnitPrice,
ol.Quantity * ol.UnitPrice AS LineTotal -- (3) ALIAS = props del DTO
FROM ... WHERE ... ORDER BY ... OFFSET ... ;
SELECT COUNT(*) FROM ... WHERE ... ;";
using var conn = new SqlConnection(connectionString);
using var grid = await conn.QueryMultipleAsync(sql, new { fromDate, toExclusive, pageIndex, pageSize });
var items = (await grid.ReadAsync<OrderLineRow>()).ToList(); // (3) <SuDto>
var total = await grid.ReadSingleAsync<int>(); // (4) int o long, según su tipo
return new PagedResult<OrderLineRow>(items, total, pageIndex, pageSize); // (4) SU contenedor
}
}
GetOrderLinesAsync, RunAsync, QueryAsync…), los parámetros (4 sueltos o un objeto ReportQuery q), el CancellationToken si viene.SELECT, exactos (Dapper mapea por nombre). Y el genérico: ReadAsync<SuDto>().return new SuTipo(...) y el ActionResult<SuTipo> del endpoint. Sus campos de página (TotalPages, HasNext) los derivas tú.El grano no cambia: una fila por línea de pedido, JOIN de tus 3 tablas (OrderLine→Order→Product), filtro de fecha, paginado, + el COUNT para el paginador. Salvo que su DTO claramente pida otra cosa (sin OrderLineId, con un SUM), no cambies de forma — confírmalo y sigue.
| Eje | Lo que te pueden pasar | Cómo lo adaptas (1 línea) |
|---|---|---|
| Nombre del método | RunAsync, GetReportAsync, QueryAsync… | Cópialo literal. No lo "mejores". |
| Parámetros: sueltos vs objeto | (DateTime from, …) vs (ReportQuery q) | Con objeto: usas q.From dentro y [FromQuery] ReportQuery q en el endpoint. |
| Base de página | Page/PageNumber base 1 vs PageIndex base 0 | Base 1 → OFFSET ((@page - 1) * @size) ROWS. Base 0 → OFFSET (@pageIndex * @pageSize). |
| Semántica de la fecha de fin | ToExclusive vs To/ToDate (¿inclusiva?) | Si es inclusiva, pregunto: "¿me mandan fin de día o primer instante del día siguiente?" y mantengo el rango semiabierto. |
| Campo de conteo: tipo | int Total vs long Count vs long TotalCount | ReadSingleAsync<long>() si es long. El nombre solo importa en el return. |
| Contenedor: campos extra | TotalPages, HasNext, PageCount | Se derivan: TotalPages = (int)Math.Ceiling(total / (double)pageSize); HasNext = (pageIndex + 1) * pageSize < total. |
Contenedor: record vs clase | new Paged<T>(rows, count) vs new Paged<T> { Rows = …, Count = … } | Posicional → paréntesis en orden. Con props → inicializador con nombres. |
| Nombre del campo de lista | Items / Rows / Data / Results | Solo cambia el return. La lista que leo de Dapper es la misma. |
| Tipo de la lista | List<T> / IReadOnlyList<T> / IEnumerable<T> | .ToList() cubre las tres (implementa todas). |
| Propiedades del DTO: nombres/casing | ProductName vs Product vs NombreProducto | Renombro el alias del SELECT, no el DTO. Dapper mapea por nombre. |
| Propiedades del DTO: menos campos | Solo OrderId, ProductName, LineTotal | Recorto el SELECT a esas columnas. Menos es más. |
| Campo calculado | El DTO trae LineTotal/Subtotal | ol.Quantity * ol.UnitPrice AS LineTotal — lo calculo en SQL, no lo guardo. |
CancellationToken en la firma | …, CancellationToken ct) | Lo enhebro: new CommandDefinition(sql, params, cancellationToken: ct). |
| Cómo llega la conexión | IDbConnection inyectado · IConfiguration · string suelto | Ajusto el constructor. Si es IDbConnection ya abierta, no la envuelvo en using. |
| Envuelven la respuesta | Result<T> / ApiResponse<T> / RespuestaGenerica<T> | return Result.Ok(paged). "Esto se parece al contrato que usamos en SEUS — éxito/error en el body." |
Truco: no memorices la tabla — memoriza que las firmas solo tocan 4 cosas (interfaz+método, DTO, contenedor, cómo llega la conexión) y que el grano y las 3 tablas no se negocian salvo que el DTO grite lo contrario. Todo lo demás es renombrar.
Te pegan esto en el chat (nada de esto cambia tus 3 tablas):
public sealed record PageRequest(int PageNumber, int PageSize, DateTime FromDate, DateTime ToDate);
public sealed class OrderLineDto
{
public int LineId { get; init; }
public int OrderId { get; init; }
public DateTime OrderDate { get; init; }
public string Product { get; init; } = "";
public int Qty { get; init; }
public decimal Amount { get; init; } // total de la línea
}
public sealed record PagedResponse<T>(IReadOnlyList<T> Data, long TotalCount, int PageNumber, int TotalPages);
public interface IOrderReportService
{
Task<PagedResponse<OrderLineDto>> GetOrderLinesAsync(PageRequest request, CancellationToken cancellationToken);
}
Read-back en voz alta: "Mi método es GetOrderLinesAsync, recibe un PageRequest y un CancellationToken, devuelve PagedResponse<OrderLineDto>. Confirmo tres cosas antes de teclear."
Lo que confirmas / decides:
PageNumber — asumo base 1. Entonces OFFSET ((@pageNumber - 1) * @pageSize) ROWS."ToDate — ¿es inclusiva? Si sí, ¿me mandan el fin del día o prefieren que yo filtre < @toDate y ustedes manden el primer instante del día siguiente? Mantengo el rango semiabierto de un lado u otro."Amount como total de línea → lo calculo, ol.Quantity * ol.UnitPrice AS Amount. Y sigue trayendo LineId/OrderId: es una fila por línea, no agregado. Mi grano no cambia."TotalCount es long → ReadSingleAsync<long>(). Y TotalPages lo derivo yo."Cómo queda (solo cambian nombres y 2 cálculos):
public async Task<PagedResponse<OrderLineDto>> GetOrderLinesAsync(
PageRequest request, CancellationToken cancellationToken)
{
const string sql = @"
SELECT ol.OrderLineId AS LineId, o.OrderId, o.OrderDate,
p.Name AS Product, ol.Quantity AS Qty,
ol.Quantity * ol.UnitPrice AS Amount
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
INNER JOIN dbo.Product AS p ON p.ProductId = ol.ProductId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive
ORDER BY o.OrderDate DESC, ol.OrderLineId DESC
OFFSET ((@pageNumber - 1) * @pageSize) ROWS FETCH NEXT @pageSize ROWS ONLY;
SELECT COUNT(*) FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive;";
using var conn = new SqlConnection(connectionString);
var cmd = new CommandDefinition(sql, new {
fromDate = request.FromDate,
toExclusive = request.ToDate, // tras confirmar la semántica
pageNumber = request.PageNumber,
pageSize = request.PageSize
}, cancellationToken: cancellationToken);
using var grid = await conn.QueryMultipleAsync(cmd);
var data = (await grid.ReadAsync<OrderLineDto>()).ToList();
var total = await grid.ReadSingleAsync<long>();
var totalPages = (int)Math.Ceiling(total / (double)request.PageSize);
return new PagedResponse<OrderLineDto>(data, total, request.PageNumber, totalPages);
}
Endpoint: [HttpGet("order-lines")] ... GetOrderLines([FromQuery] PageRequest request, CancellationToken ct) → valida (PageSize con tope, FromDate < ToDate), delega, Ok(result).
Truco: ensáyalo cambiando un eje cada vez (hoy base 0, mañana un record posicional, pasado sin CancellationToken) — si repites siempre la misma firma memorizas el resultado, no la habilidad. La habilidad es "leo la firma, identifico qué eje cambió, adapto solo eso".
Si NO te dan firmas y dicen "estructúralo como lo harías": usas las tuyas (Paso 3–5), PagedResult<OrderLineRow>, y explicas cada decisión. Prepárate para ambos.
La escribes como SQL puro primero, en un bloque comentado, para pensarla sin pelear con el C#. Luego la mueves al método. La construyes por trozos, narrando.
Escribes (debajo del encabezado, en prueba.cs):
/* ---- La consulta ----
SELECT
ol.OrderLineId,
o.OrderId,
o.OrderDate,
p.Name AS ProductName,
ol.Quantity,
ol.UnitPrice,
ol.Quantity * ol.UnitPrice AS LineTotal
Dices: "Empiezo por lo que devuelve el reporte: id de línea, datos del pedido, nombre del producto, cantidad y precio. El total de línea no lo guardo, lo calculo — Quantity * UnitPrice."
Por qué: arrancar por el SELECT te obliga a decidir la forma de la fila antes que nada. Alias legibles (ProductName, LineTotal) para que el DTO mapee por nombre. Total calculado = no duplicas un dato derivable.
Truco: frase 1 de las 6 (pestaña Guion completo): "traigo columnas con alias claros; el total lo calculo, no lo guardo." Si recuerdas por qué no guardas LineTotal como columna —se desincroniza si cambia el precio—, la línea de código te sale sola.
Escribes:
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
INNER JOIN dbo.Product AS p ON p.ProductId = ol.ProductId
Dices: "Arranco por OrderLine, que es el grano del reporte — una fila por línea. Le pego el pedido por OrderId y el producto por ProductId. INNER porque solo quiero líneas completas; si quisiera incluir líneas sin producto sería LEFT. Alias cortos y prefijo todas las columnas."
Por qué: empezar por la tabla más fina y colgar las demás es el patrón estándar de un reporte tipo estrella. [Order] entre corchetes porque Order es palabra reservada. Prefijar columnas evita ambigüedad.
Truco: "el grano manda" — la tabla con la que arrancas siempre es la que tiene una fila por cada resultado que quieres mostrar. Si el reporte fuera por pedido (no por línea), arrancarías en [Order]. Esa sola idea reemplaza memorizar "siempre empiezo en OrderLine".
Escribes:
WHERE o.OrderDate >= @fromDate
AND o.OrderDate < @toExclusive
Dices: "Rango semiabierto: mayor o igual que el inicio, y estrictamente menor que el fin. No uso BETWEEN porque incluye el extremo, y con datetime eso te mete o te deja fuera el último día según la hora. El cliente manda como toExclusive el primer instante del día siguiente. Y filtro contra OrderDate tal cual, sin CAST ni CONVERT, para que el predicado sea SARGable y pueda usar un índice."
Por qué: es EL detalle que separa a alguien que ha hecho reportes de fechas de alguien que no. BETWEEN + datetime = bug clásico de "me falta o me sobra el último día". CAST(OrderDate AS date) = el motor no puede usar el índice → scan.
Truco: "≥ abre, < cierra, nunca ENTRE." Si memorizas solo una línea de toda la prueba, que sea esta — es la que ellos citan como "la pregunta que decide" en sus propias notas.
Escribes:
ORDER BY o.OrderDate DESC, ol.OrderLineId DESC
Dices: "Ordeno por fecha descendente, que es lo que le importa al usuario, y desempato con OrderLineId, que es único. OFFSET/FETCH necesita un orden total: si dos filas tienen la misma fecha y no hay desempate, el motor las puede devolver en cualquier orden y al paginar se repiten o se saltan filas."
Por qué: paginación sin orden determinista = filas duplicadas o perdidas entre páginas. La columna final tiene que ser única (la PK sirve). Es sutil y es justo lo que quieren oírte razonar.
Truco: "sin desempate único, no hay orden real." Pregúntate siempre: "¿mi última columna del ORDER BY es una PK o algo único?" Si la respuesta es no, falta una columna — no importa si es esta prueba u otra distinta el martes.
Escribes:
OFFSET (@pageIndex * @pageSize) ROWS FETCH NEXT @pageSize ROWS ONLY;
Dices: "Paginación en el servidor: salto pageIndex * pageSize filas y traigo las siguientes pageSize. Para un reporte donde saltas a la página N, OFFSET está bien. Si fuera scroll infinito usaría keyset — filtrar por la última fila vista — que no se degrada al avanzar."
Por qué: OFFSET es simple y correcto aquí; mencionar keyset muestra que sabes su límite (en la página 10.000, OFFSET aún lee y descarta 100.000 filas) sin complicar el ejercicio.
Truco: la fórmula completa cabe en 4 palabras: "salta, trae; o sigue." OFFSET = salta N, trae M. Keyset = "sigue después de la última que viste." Nombrar el segundo sin implementarlo ya es la señal senior — no necesitas escribirlo.
Escribes (y cierras el bloque):
SELECT COUNT(*)
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
WHERE o.OrderDate >= @fromDate
AND o.OrderDate < @toExclusive;
---- fin ---- */
Dices: "Y el total para el paginador, con el mismo WHERE pero sin el JOIN a producto ni el ORDER BY, que no hacen falta para contar. Los dos SELECT los mando juntos, un solo viaje a la base."
Por qué: el paginador necesita el total de filas que cumplen el filtro, no el de la tabla. Contar no necesita ordenar ni el join a Product.
Truco: "mismo filtro, menos peso." El COUNT copia solo el WHERE de arriba — ni el JOIN a Product, ni el ORDER BY. Si te preguntas "¿qué llevo y qué quito?", la respuesta siempre es esa.
Si preguntan por índices: "Un índice en [Order](OrderDate) para el filtro y el orden; y las FK de OrderLine (OrderId, ProductId) indexadas para el JOIN. Con eso no hay scan."
Escribes (debajo del bloque SQL; usa la firma que te dieron, o esta):
using System.Data;
using Dapper;
using Microsoft.Data.SqlClient;
public class OrderReportService(string connectionString)
{
public async Task<PagedResult<OrderLineRow>> GetOrderLinesAsync(
DateTime fromDate, DateTime toExclusive, int pageIndex, int pageSize)
{
}
}
Dices: "El servicio recibe el connection string por constructor — en real vendría de configuración e inyección de dependencias. El método es async porque va a la base."
Por qué: si te dieron una interfaz, aquí pones : IEsaInterfaz y copias la firma literal. El primary constructor (string connectionString) es C# moderno, ahorra ceremonia.
Truco: el patrón "clase + constructor primario + método async" es idéntico en cualquier servicio que hayas escrito en SEUS. No es sintaxis nueva que memorizar — es la misma forma que ya usas, con otro nombre.
Escribes (dentro del método) — copias la consulta del bloque de arriba a una cadena:
const string sql = @"
SELECT ol.OrderLineId, o.OrderId, o.OrderDate,
p.Name AS ProductName, ol.Quantity, ol.UnitPrice,
ol.Quantity * ol.UnitPrice AS LineTotal
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
INNER JOIN dbo.Product AS p ON p.ProductId = ol.ProductId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive
ORDER BY o.OrderDate DESC, ol.OrderLineId DESC
OFFSET (@pageIndex * @pageSize) ROWS FETCH NEXT @pageSize ROWS ONLY;
SELECT COUNT(*)
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive;";
Dices: "Muevo la consulta a una cadena literal — el @ me deja escribirla en varias líneas. Es la misma de antes."
Por qué: pensaste la SQL como SQL; ahora es un dato del método. const porque no cambia.
Truco: es un copy-paste literal del bloque del Paso 3, no una reescritura — si Paso 3 ya salió de memoria, este paso no añade nada nuevo que aprender, solo mover.
Escribes:
using var conn = new SqlConnection(connectionString);
using var grid = await conn.QueryMultipleAsync(sql,
new { fromDate, toExclusive, pageIndex, pageSize });
Dices: "Abro la conexión con using para que se cierre sola. Los parámetros van como objeto anónimo — Dapper los convierte en parámetros de SqlCommand, nunca los concateno en el texto: eso es lo que mata la inyección SQL. Y como son dos SELECT, QueryMultiple: un viaje, leo los dos resultados."
Por qué: parametrizar = seguridad + plan de ejecución reutilizable. using = no fugas de conexión. QueryMultiple = lista + count en un round-trip.
Truco: tres palabras, tres líneas: using (se cierra sola) → objeto anónimo (nunca texto concatenado) → Multiple (porque son 2 SELECT). Si dices las tres palabras en voz alta, el código sale detrás.
Escribes:
var items = (await grid.ReadAsync<OrderLineRow>()).ToList();
var total = await grid.ReadSingleAsync<int>();
return new PagedResult<OrderLineRow>(items, total, pageIndex, pageSize);
Dices: "Leo la primera consulta como la lista de filas, la segunda como un entero — el total. Y devuelvo el resultado paginado con lo que el cliente necesita para pintar el paginador."
Por qué: el orden de los Read sigue el orden de los SELECT. Si el tipo de resultado que te dieron se llama distinto o tiene otros campos, aquí lo ajustas (Paso 2).
Truco: "leo en el mismo orden en que escribí." Los dos Read son un espejo exacto de los dos SELECT de arriba — si te pierdes, cuenta cuántos SELECT escribiste y sabrás cuántos Read te faltan.
Escribes (debajo del servicio):
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/reports")]
public class OrderReportController(OrderReportService service) : ControllerBase
{
[HttpGet("order-lines")]
public async Task<ActionResult<PagedResult<OrderLineRow>>> GetOrderLines(
[FromQuery] DateTime fromDate,
[FromQuery] DateTime toExclusive,
[FromQuery] int pageIndex = 0,
[FromQuery] int pageSize = 20)
{
}
}
Dices: "GET porque es una lectura. Los filtros van en la query string: fromDate, toExclusive, y la paginación con valores por defecto. El servicio entra por constructor."
Por qué: [ApiController] te da el binding y las respuestas de error automáticas. ActionResult<T> te deja devolver el dato o un código de error.
Truco: es el mismo esqueleto [ApiController] + constructor + [HttpGet] que ya escribes en SEUS cada vez que expones un endpoint. No hay nada aquí que no hayas tecleado cien veces con otro nombre de recurso.
Escribes (dentro de la acción):
if (toExclusive <= fromDate)
return BadRequest("El rango de fechas no es valido.");
pageIndex = Math.Max(0, pageIndex);
pageSize = Math.Clamp(pageSize, 1, 100);
Dices: "Valido lo que el servicio no puede saber: que el rango tenga sentido → 400. Y saneo la paginación aquí, en el borde: pageIndex no negativo, pageSize con tope de 100, para que nadie pida un millón de filas de golpe."
Por qué: el controller es la frontera con el mundo — es donde se valida la entrada. El tope de página protege la base.
Truco: "la frontera valida, el servicio confía." Todo lo que entra de fuera (fechas, números de página) se limpia aquí, antes de llegar al servicio — así nunca tienes que recordar validar dos veces en dos capas.
Escribes:
var result = await service.GetOrderLinesAsync(fromDate, toExclusive, pageIndex, pageSize);
return Ok(result);
Dices: "Llamo al servicio y devuelvo 200 con el resultado. Si no hay filas en ese rango, sigue siendo 200 con lista vacía — no 404: el reporte existe, simplemente no tiene datos ese mes. El controller queda fino: valida, delega, devuelve. Nada de SQL aquí."
Por qué: separación de capas. 404 es "el recurso no existe", no "la consulta no trajo nada".
Truco: regla de una línea para todos los códigos HTTP de la prueba: "el recurso existe pero está vacío = 200; el recurso no existe = 404; tú mandaste algo mal = 400." Con esa frase decides cualquier código sin memorizar una tabla.
Antes de decir "listo", recorre el código señalando en pantalla y recita:
ORDER BY determinista que termina en columna única."using en conexión y grid."Truco: practica este repaso señalando en pantalla con el cursor cada cosa que nombras, incluso solo en tus ensayos. El gesto físico ancla la lista mejor que decirla mirando al techo.
SELECT: no necesita transacción explícita. Corre bajo READ COMMITTED por defecto y me vale."SELECT (lista y COUNT). Entre uno y otro podría colarse un INSERT y que el total no cuadre con la página. Si esa consistencia importa, envuelvo los dos en una transacción con aislamiento SNAPSHOT —lectura consistente sin bloquear escritores— o REPEATABLE READ."Order + sus OrderLine + descontar Stock. O las tres o ninguna."conn.BeginTransaction(), se lo paso a cada Execute, Commit() al final, Rollback() en el catch. El using de la transacción hace rollback si salgo por excepción sin llegar al commit."SERIALIZABLE solo si necesito que nadie inserte en el rango que estoy leyendo mientras decido —por ejemplo, validar que no haya solapamiento antes de insertar."Truco: una sola pregunta desata las 5 frases: "¿esto lee o escribe en más de una tabla?" Lee = no necesita transacción. Escribe en varias = la necesita, y ahí es donde entra BeginTransaction/Commit/Rollback.
El orden que te van a seguir (sale de las notas del entrevistador — cada uno tiene su guion abajo):
Puede colarse signals + change detection (8.4) porque SEUS es signals-first, y algo de la cola larga (8.7). Lo básico —"qué es un componente", "qué es RxJS"— lo dijeron explícito que lo saltan; no pierdas tiempo ahí.
Truco: no memorices las respuestas una por una — memoriza el molde de 3 partes (8.0) que las genera todas: afirmación directa + caso real en SEUS + término exacto. Aplicado a cualquier pregunta nueva que no esté aquí, sigue funcionando.
La parte hablada la abre el entrevistador con dos preguntas (Observable vs Promesa · Native Federation) y sigue con el último test, AWS→Azure e inglés. Dijo "saltar lo básico" — nada de "qué es un componente". Ve a profundidad. Todo lo que digas pasa por el mismo molde:
[Afirmación directa] → [En SEUS: qué módulo · qué problema · qué decidí] → [el término exacto] → [cierre: el trade-off o el matiz]
| Regla del ex jefe | Flojo — no lo digas | Con el molde |
|---|---|---|
| 1 · Seguridad | "Creo que switchMap cancela la anterior…" | "switchMap cancela la suscripción interna anterior." |
| 2 · Contexto | "switchMap sirve para búsquedas." | "En el buscador de producto de SEUS cada tecla dispara una consulta; con debounceTime y switchMap descarto las respuestas viejas y solo pinto la última." |
| 3 · Lenguaje técnico | "esa cosa que evita recargar de más" | "OnPush, y en lo nuevo, change detection granular por señal." |
Las respuestas de abajo ya vienen con el molde puesto. Cada una: qué decir, qué no decir, y qué hacer si repreguntan.
Respuesta lista: "En SEUS convivimos con las dos y la elección es deliberada. Una promesa es un solo valor, ansiosa —arranca al crearse— y no se cancela. Un Observable es de cero a n valores en el tiempo, perezoso —no pasa nada hasta el subscribe—, cancelable, y con operadores. Uso promesas en la capa de datos de cada módulo, la .promise.ts: un guardar es request-response de un disparo y un await secuencial se lee mejor que encadenar operadores. Uso Observable en el buscador de producto de la librería —cada tecla es una consulta, con debounceTime(500) y switchMap descarto las viejas—, en valueChanges de los formularios, y en el cotizador que dispara siete u ocho llamadas en paralelo con forkJoin. La regla: si hay que cancelar, recombinar o es un stream → Observable; si es un disparo secuencial → promesa con async/await."
❌ No digas: "los dos sirven para lo mismo" · "uso Observable porque Angular lo usa" · "creo que la promesa no se puede cancelar" — afírmalo: no se cancela.
subscribe reejecuta el productor (un HttpClient.get); hot / shareReplay(1) = multicast, comparten una ejecución. Uso shareReplay(1) para cachear un GET de catálogo que piden varios componentes.switchMap (cancela la anterior — buscador), mergeMap (todas en paralelo — N validaciones independientes), concatMap (cola en orden — guardar renglones sin pisarse), exhaustMap (ignora nuevas mientras trabaja — botón "confirmar", anti doble-submit).forkJoin vs combineLatest: forkJoin espera a que todas terminen y emite una vez (datos de arranque); combineLatest reemite con cada cambio (combinar filtros). El cotizador usa forkJoin + catchError al final: si una llamada falla, falla el cotizador entero — es una unidad de negocio.subscribe sin cerrar sigue vivo tras destruir el componente. Se evita con el pipe async, takeUntilDestroyed() (v16+) o takeUntil(destroy$) en ngOnDestroy. El buscador de la librería usa takeUntil(destroy$).MasterGuardarPedidoService está en async/await sobre promesas: secuencial —validar, y solo si pasa, enviar—, con dos cortes tempranos; nada que cancelar ni recombinar. El código, en la pestaña Angular · §1.5.Respuesta lista (aportó): "SEUS son más de una docena de microfrontends con @angular-architects/native-federation: un host/shell, y cada módulo —compra directa, recepción electrónica, monitor de embarques— es un remoto con su remoteEntry.json, mapeado en federation.manifest.json. Lo concreto que aportó: [actualicé el módulo de X y salió a producción sin recompilar el shell ni tocar los otros remotos]. Cada equipo tiene su release, dejan de pisarse. Y la librería común —el buscador de producto, el manejo de errores, el spinner— se carga una sola vez en vez de duplicarse en cada remoto."
Respuesta lista (costó): "Tres frentes. Uno, version skew: si un remoto trae otra versión de @angular/core o de la librería que el shell, cargas dos instancias de Angular y salta NG0203, inyección fuera de contexto; se controla con shared en singleton: true y strictVersion. Dos, la comunicación entre microfrontends: no hay bus de estado nativo, [lo resolvimos con X — eventos / un servicio de la librería]. Tres, el tooling: para dev local necesitas todos los remotos corriendo o mockeados, un manifest por entorno, y cuando un remoto no carga el error dice poco — toca mirar el import map generado y la pestaña de red."
El dibujo (si te lo piden): shell lee federation.manifest.json → genera el import map → carga el remoteEntry de cada microfrontend bajo demanda → monta su ruta. Deps compartidas resueltas una vez.
❌ No digas: solo las ventajas. Nombrar los dolores concretos (NG0203, dev local con N remotos, debugging opaco) es lo que prueba que lo hiciste y no que leíste el README.
Respuesta lista: "El último fue el spec de MasterGuardarPedidoService, el servicio que orquesta validar y enviar un pedido manual. Lo testeé porque es lógica de negocio sobre promesas con varias ramas de corte temprano —donde un cambio rompe un if en silencio. Cubrí las tres ramas: validación falla (!procesoCorrecto) → llama al manejador de errores y corta; el negocio no deja generar la solicitud (bGenerarSolicitud = false) → oculta el spinner y no envía; y el camino feliz hasta enviarGuardarPedido. Montaje: TestBed sin componente, solo el servicio y sus cuatro dependencias como jasmine.createSpyObj. Y como el servicio es async/await sobre promesas, no Observable, el test es async con .and.resolveTo() en los spies, no fakeAsync con tick()."
❌ No digas: "no me acuerdo del último" · "casi siempre los tests los hace otro". Llega con este, concreto y verificable.
HttpTestingController: expectOne(url) intercepta, verificas req.request.method y el body, req.flush(data) simula la respuesta, httpMock.verify() al final. Sin red.TestBed.createComponent, fixture.componentInstance (la clase) vs fixture.nativeElement (el DOM), fixture.detectChanges() para aplicar cambios a la vista.fakeAsync + tick(ms) / flush() para tiempo controlado; o async + await fixture.whenStable() para promesas.loader.getHarness(MatButtonHarness…)) — el test no depende del DOM interno de Material, sobrevive a los upgrades.El código del spec, en la pestaña Testing.
Respuesta lista: "El código nuevo de SEUS —compra directa, consulta de facturas, devoluciones a proveedor, inventarios— va signals-first: signal<T>() para el estado del componente, computed() para lo derivado. En confirmación de lote tengo puedeAgregarLote = computed(() => …) que se recalcula solo cuando cambian sus dependencias. effect() lo reservo para efectos de verdad —logging, sincronizar con algo externo—; derivar estado con effect es un olor, para eso está computed. En los módulos viejos, servicios + Observable y OnPush en los componentes con listas o inputs pesados. El fondo: Angular va a zoneless, y con signals el change detection es granular —solo se re-evalúa lo que lee esa señal— en vez de marcar todo el árbol como sucio."
Interop: toSignal(obs$) para consumir un Observable como señal en la vista; toObservable(sig) para lo inverso. No migro todo a signals porque sí.
❌ No digas: "signals reemplazan a RxJS". Signals = estado síncrono de vista; RxJS = async y streams. Conviven.
Respuesta lista: "Mi experiencia de nube es AWS Lambda, así que lo que traigo es el modelo serverless: funciones stateless, cold starts, idempotencia, límites de ejecución. A Azure traslada casi directo el enfoque event-driven, Blob ≈ S3, y App Insights me daría más que CloudWatch para .NET. Lo que no equivale: Azure Functions usa bindings declarativos en vez de llamar al SDK a mano; Durable Functions no es Step Functions —es orquestación en código C#, no una máquina de estados JSON—; Cosmos no es DynamoDB en particionado ni en costeo (Request Units); y la identidad pasa de IAM roles a Managed Identity + RBAC. Y para el grueso de una API .NET probablemente no sería Functions sino App Service o Container Apps — ¿qué usan ustedes?"
❌ No digas: que sabes Azure si no lo has tocado — lo notan. La señal buena es nombrar lo que no equivale. Tabla completa en la pestaña Cloud.
Respuesta honesta (en español): "Mi inglés es [básico / básico-intermedio — sé honesto], sobre todo de lectura. Consumo documentación técnica en inglés —la de Angular ya así—. Hablado todavía no tengo fluidez; lo trabajo a diario [cómo — Anki / una app]. Para comunicación escrita asíncrona me defiendo; para una reunión hablada en inglés todavía no. Sé que SP-API, Stripe y Angular están documentados en inglés y con eso puedo."
Si cambian a inglés, di esto (ensáyalo en voz alta hasta que salga sin pensar):
"I'm a full-stack developer with five years at a pharmacy retail company, working on their point-of-sale and back-office system — around eighteen modules in production today. My strongest area is the front end: Angular, currently version 20, with RxJS and Angular Material. We moved the app to micro-frontends with Native Federation, and I was the main contributor to that repository. I test with Karma/Jasmine and Jest. On the back end I work with .NET, REST APIs and SQL Server, writing the data access by hand. What I'm looking for is a fully remote full-stack role with ownership of complete features — and your stack is almost the same one I already work with."
Si te trabas con una repregunta en inglés: "Could you say that again, please?" · "I want to be precise, so let me answer that part in Spanish." — sin disculparte tres veces.
❌ No digas: que tu inglés es "intermedio" o "bueno" si no lo es — lo comprueban ahí mismo. Claridad + plan de mejora, y sigues.
Menos probable que caiga (el entrevistador dijo "saltar lo básico"), pero ten el núcleo + el término + un gancho a SEUS:
| Tema | Núcleo + término | Gancho a SEUS |
|---|---|---|
| TypeScript | Genéricos, utility types (Partial, Pick, Omit), strict, discriminated unions para respuestas del back. Tipar el contrato, nada de any. | Las interface de respuesta son el contrato con .NET, en camelCase, mismos nombres que el JSON. |
| Angular Material + CDK | MatTable con paginación server-side, MatAutocomplete, MatSnackBar para feedback, theming con tokens, CDK Overlay/a11y. | El buscador de producto es MatAutocomplete + paginación por hash al hacer scroll al fondo. |
| Reactive Forms a fondo | FormBuilder/FormGroup tipado, validadores sync y async (composeAsync), cross-field en el grupo, ControlValueAccessor para un control propio, updateOn: 'blur'. | Reactive Forms en casi todos los módulos; los formularios de alta reflejan los mismos límites que el backend .NET. |
| Routing | Lazy con loadComponent/loadChildren, guards funcionales (CanActivateFn), CanDeactivate para cambios sin guardar, resolvers, withComponentInputBinding(). | En el host cada módulo es una ruta lazy que carga su remoto. |
| Interceptores HTTP | Funcionales (withInterceptors([...])): token de auth, logging, reintentos, 401 → refresh o login, idempotency-key para las integraciones. | — |
Standalone + ApplicationConfig | Sin NgModule, imports en el componente, bootstrapApplication + ApplicationConfig con los provideX(). Mejor tree-shaking, lazy por componente. | SEUS es standalone en todo el repo. |
Control flow nuevo + @defer | @if/@for/@switch (v17); @for exige track; @defer difiere bloques pesados por viewport o interacción. | — |
| Rendimiento | OnPush + inmutabilidad, track en listas, lazy, NgOptimizedImage, no llamar funciones en el template, medir el bundle. | OnPush en las vistas de SEUS con listas grandes. |
| Seguridad frontend | Angular escapa por defecto; [innerHTML] pasa por DomSanitizer; nunca bypassSecurityTrust* sobre input del usuario; CSRF token vía interceptor. | — |
| Estado / por qué no NgRx | Servicio con signal o BehaviorSubject + asObservable(). NgRx solo si el estado compartido y complejo lo justifica. | SEUS no usa NgRx: el estado compartido es mínimo y vive en servicios de la librería. |
| Ciclo de vida | constructor solo DI; ngOnInit arranque; ngOnChanges reacciona a @Input; ngOnDestroy limpia; ngAfterViewInit ya hay @ViewChild. | — |
| Servicios y DI | @Injectable({ providedIn: 'root' }) = singleton tree-shakeable; inyector jerárquico; inject() function-style; InjectionToken para config. | — |
La hoja de datos de SEUS — los hechos verificados de tu repo MicroFrontsSeusWeb (backoffice/POS farmacéutico, más de una docena de microfrontends con Native Federation, Angular 20.3, standalone, Reactive Forms en todo + signals en el código nuevo, mayor contribuidor con 86 commits). Los guiones 8.1–8.6 ya llevan este contexto puesto; esto es lo que repasas mirando tu propio código. Rellena los […] antes del martes.
| Tema | El dato exacto de SEUS |
|---|---|
| Arquitectura frontend | "Un host/shell y cada módulo —compra directa, recepción electrónica, monitor de embarques, consultas de inventario…— es un remoto con su remoteEntry.json, mapeado en federation.manifest.json. Más una librería común: componentes y servicios que todos consumen. Angular 20.3, standalone en todo el repo." |
| Native Federation — tu caso | Aportó: [actualicé el módulo de X y salió a prod sin recompilar el shell ni tocar los otros]. Costó: version skew (NG0203, dos instancias de Angular) · comunicación entre MFEs [cómo lo resolvieron] · dev local con N remotos. |
| Estado — signals | "Compra directa, consulta de facturas, devoluciones a proveedor, inventarios ya usan signals: signal<T>() para el estado, computed() para lo derivado. En confirmación de lote, puedeAgregarLote = computed(() => …) recalcula solo cuando cambian sus dependencias. inject() en vez de constructor. Sin NgRx — el estado compartido es mínimo y vive en servicios de la librería." |
| RxJS concreto (el buscador) | "El buscador de producto de la librería: un Subject con debounceTime(500) + takeUntil(destroy$), a partir de 3 caracteres. La paginación de resultados es por cursor — la API devuelve un Hash, y al hacer scroll al final del autocomplete pido la siguiente página con ese hash. Es keyset, no OFFSET." En módulos: switchMap [para X], combineLatest [para Y]. |
| Arquitectura de un módulo | "Cada feature son 3 capas: assemblyRequest arma el request, .promise hace la llamada HTTP envuelta, .service orquesta (loading, errores, log). El componente solo pinta y llama al service." |
| Formularios | "Reactive Forms en casi todos los módulos: FormBuilder, FormGroup, y todo el set de Validators —required, pattern, minLength, email, y validadores async con composeAsync—. Los formularios de alta [de X] reflejan las mismas reglas que el backend .NET, mismos límites en los dos lados. En el código nuevo, computed() para la validación de conjunto." |
| Testing | "En SEUS es Karma/Jasmine (ng test, cobertura con karma-coverage). TestBed, jasmine.createSpyObj, fixture.detectChanges()." [Jest: la verdad de cuánto lo has tocado — en SEUS no está. Si es poco: "lo conozco, el salto desde Karma es menor"] |
Truco: esta hoja no se memoriza — se vive. Cada fila ya pasó por tus manos; repásala mirando tu propio repo un minuto antes de dormir cada noche de esta semana. Lo que sí ensayas en voz alta son los guiones 8.1–8.6.
Cuando te pregunten "¿tienes dudas?", ten 2–3 listas. Muestran interés real:
Truco: elige tus 2 favoritas de esta lista antes del martes y practícalas en voz alta — no llegues a improvisar la pregunta en el momento, es lo único de todo el guion que sí puedes decir casi textual porque nadie evalúa tu SQL en esta parte.
QueryMultipleAsync o similar; la idea es leer dos result sets." No se mide memoria.Truco: esta lista no se memoriza como texto — se ensaya provocándola a propósito. En tu Corrida 3 (grabándote la voz), mete adrede un error a mitad y practica recuperarte en voz alta. Es la única forma de que estos reflejos existan cuando de verdad tiembles.
Las 3 piezas (SELECT+JOIN → método → endpoint) son invariantes. Lo que cambia es una cláusula o un método. Pero correcto lo entrega cualquiera — lo que te sube al primer puesto es la frase que sueltas sin que te la pidan (el trade-off, el término exacto) y no caer en el error del candidato promedio. Cada variante trae las tres cosas.
| Lo que oyes en el enunciado | Es… | Qué cambia |
|---|---|---|
| "en un rango de fechas", "del último mes" | el caso base | nada — Paso 3–5 tal cual |
| "por estado", "solo los pendientes", "de un producto / SKU" | filtro de igualdad | el WHERE → A |
| "cuánto se vendió", "total por pedido", "un resumen" | agregación | SELECT + GROUP BY → B |
| "buscar por nombre", "que empiece con", "autocomplete" | búsqueda de texto | WHERE … LIKE → C |
| "el detalle de", "un pedido en concreto", "por id" | registro único | quitas la paginación → D |
| "crear", "registrar", "dar de alta" un pedido | escritura, 1 tabla | INSERT + SCOPE_IDENTITY → E |
| "un pedido con sus líneas", "y descontar stock" | escritura, N tablas | transacción → F |
| "cargar más", "scroll infinito", "sin número de página" | keyset | el WHERE de seek → G |
| "sin Dapper", "solo ADO.NET", "a pelo" | driver crudo | SqlCommand + reader → H |
"Antes de teclear repito lo que entendí y confirmo el grano: ¿una fila por línea de pedido o agregado por pedido? ¿la fecha filtra por fecha de pedido? ¿paginación por número de página o por cursor?" — esas tres preguntas te dicen en qué variante estás.
Truco: no memorices las 8 variantes como código — memoriza esta tabla de reconocimiento. En vivo no vas a recordar el WHERE exacto; vas a recordar "esto suena a agregación → GROUP BY y el COUNT cambia" y el resto sale solo del esqueleto.
Cambia: el WHERE → WHERE o.Status = @status (o p.Sku = @sku). Parámetro string status en el método y en el endpoint ([FromQuery]). Nada más.
La frase que te distingue: "Es una igualdad, no un LIKE — = @status usa el índice directo. Si Status fuera un enum del dominio, valido contra los valores permitidos en el endpoint y devuelvo 400 si viene basura, en vez de una lista vacía silenciosa que parece 'no hay datos'."
❌ Error del candidato promedio: usar LIKE @status + '%' "por si acaso" para algo que es igualdad exacta — tira el uso limpio del índice y no aporta nada. O no mencionar que la columna de filtro necesita su índice.
Cambia: el SELECT lleva COUNT/SUM, entra GROUP BY, y el count del paginador cuenta pedidos distintos. Row type nuevo. Método y endpoint: solo el genérico.
SELECT
o.OrderId, o.OrderDate,
COUNT(ol.OrderLineId) AS LineCount,
SUM(ol.Quantity * ol.UnitPrice) AS OrderTotal
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive
GROUP BY o.OrderId, o.OrderDate
ORDER BY o.OrderDate DESC, o.OrderId DESC
OFFSET (@pageIndex * @pageSize) ROWS FETCH NEXT @pageSize ROWS ONLY;
SELECT COUNT(DISTINCT o.OrderId) -- pedidos, no líneas
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive;
Row: OrderSummaryRow(int OrderId, DateTime OrderDate, int LineCount, decimal OrderTotal).
La frase que te distingue: "GROUP BY por todo lo del SELECT que no está agregado — si olvido una columna es error de SQL, no un dato mal. El total del paginador es COUNT(DISTINCT o.OrderId): el COUNT(*) de siempre contaría líneas y el paginador daría páginas fantasma. Y si el join fuera LEFT, cuento COUNT(ol.OrderLineId), no COUNT(*) — * cuenta la fila aunque no haya línea."
❌ Error del candidato promedio: dejar COUNT(*) en el segundo SELECT (cuenta líneas, el paginador miente) · meter en el SELECT una columna que no está en el GROUP BY ni agregada.
Cambia: al WHERE → AND (p.Name LIKE @term + '%' OR p.Sku LIKE @term + '%'), parámetro string term. Si es autocomplete: sin COUNT, FETCH NEXT 10, y en Angular debounceTime + switchMap.
La frase que te distingue: "Prefijo —@term + '%'— puede usar el índice; '%' + @term + '%' (contiene) hace scan, y lo digo: si necesitan 'contiene' de verdad es full-text search, o asumo el scan con un tope de filas. El @term va parametrizado igual que todo — construir el LIKE concatenando es inyección. Y escapo % y _ del input si el usuario los puede escribir, o son comodines."
❌ Error del candidato promedio: armar el LIKE con concatenación de strings (inyección) · prometer que '%x%' "usa el índice" (no es SARGable) · autocomplete sin tope de filas.
Cambia: sin paginación ni ORDER BY, WHERE ol.OrderLineId = @id.
public async Task<OrderLineRow?> GetByIdAsync(int id)
{
using var conn = new SqlConnection(connectionString);
return await conn.QuerySingleOrDefaultAsync<OrderLineRow>(sql, new { id });
}
[HttpGet("order-lines/{id:int}")]
public async Task<ActionResult<OrderLineRow>> GetById(int id)
=> await service.GetByIdAsync(id) is { } row ? Ok(row) : NotFound();
La frase que te distingue: "QuerySingleOrDefault: null si no existe → 404; lanza si hay más de uno, que es lo correcto porque filtro por PK y nunca debería haber dos. First escondería un duplicado, Single sin OrDefault lanzaría en vez de dar 404. La restricción de ruta {id:int} rechaza /order-lines/abc antes de entrar al método."
❌ Error del candidato promedio: FirstOrDefault() por costumbre (oculta datos corruptos) · devolver 200 con null en el body en vez de 404 · no tipar la ruta y dejar que un id no numérico entre como 0.
public async Task<int> CreateOrderAsync(NewOrder input)
{
const string sql = @"
INSERT INTO dbo.[Order] (OrderDate, Status) VALUES (@OrderDate, @Status);
SELECT CAST(SCOPE_IDENTITY() AS int);";
using var conn = new SqlConnection(connectionString);
return await conn.ExecuteScalarAsync<int>(sql, input);
}
[HttpPost("orders")]
public async Task<ActionResult<int>> Create(NewOrder input)
{
var id = await service.CreateOrderAsync(input);
return CreatedAtAction(nameof(GetById), new { id }, id); // 201 + Location
}
La frase que te distingue: "SCOPE_IDENTITY(), no @@IDENTITY — @@IDENTITY te devuelve el id que generó un trigger, no el tuyo. 201 con Location al recurso, no 200. El 400 por body inválido no lo escribo: lo da el binder por los [Required]/[Range] del DTO. Y esto no necesita transacción: un solo INSERT ya es atómico."
❌ Error del candidato promedio: @@IDENTITY en vez de SCOPE_IDENTITY() · devolver 200 en vez de 201 · envolver un INSERT único en BeginTransaction "por si acaso" · validar el body con ifs a mano.
public async Task<int> CreateOrderWithLinesAsync(NewOrder order, IEnumerable<NewLine> lines)
{
using var conn = new SqlConnection(connectionString);
await conn.OpenAsync();
using var tx = conn.BeginTransaction();
try
{
var orderId = await conn.ExecuteScalarAsync<int>(
@"INSERT INTO dbo.[Order] (OrderDate, Status) VALUES (@OrderDate, @Status);
SELECT CAST(SCOPE_IDENTITY() AS int);", order, tx);
foreach (var l in lines)
await conn.ExecuteAsync(
@"INSERT INTO dbo.OrderLine (OrderId, ProductId, Quantity, UnitPrice)
VALUES (@orderId, @ProductId, @Quantity, @UnitPrice);",
new { orderId, l.ProductId, l.Quantity, l.UnitPrice }, tx);
// descuento en UNA sentencia: no leo-y-escribo (lost update)
await conn.ExecuteAsync(
@"UPDATE p SET p.Stock = p.Stock - x.Qty
FROM dbo.Product p
JOIN (SELECT ProductId, SUM(Quantity) AS Qty FROM @lines ...) x ON x.ProductId = p.ProductId",
/* ... */ tx);
tx.Commit();
return orderId;
}
catch { tx.Rollback(); throw; }
}
La frase que te distingue: "Tres escrituras que pasan juntas o ninguna — o el pedido queda sin líneas, o descuento stock de un pedido que no se guardó. Le paso la tx a cada Execute; si me olvido de una, esa corre fuera de la transacción y el rollback no la toca. El descuento de stock lo hago en una sola UPDATE … SET Stock = Stock - @qty, no leo-y-escribo, para no tener un lost update entre dos ventas simultáneas. En SQL Server puro subiría SET XACT_ABORT ON."
❌ Error del candidato promedio: abrir la transacción y olvidar pasar tx a alguna sentencia (queda fuera) · SELECT Stock y luego UPDATE con el valor calculado en C# (lost update) · no relanzar la excepción tras el rollback (te comes el error).
Cambia: el cliente manda (lastDate, lastId) de la última fila vista, no pageIndex.
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive
AND (@lastDate IS NULL
OR o.OrderDate < @lastDate
OR (o.OrderDate = @lastDate AND ol.OrderLineId < @lastId))
ORDER BY o.OrderDate DESC, ol.OrderLineId DESC
OFFSET 0 ROWS FETCH NEXT @pageSize ROWS ONLY;
La frase que te distingue: "OFFSET grande lee y descarta N filas — la página 5.000 es lenta. Con keyset el WHERE salta directo por el índice: la página 1 y la 5.000 cuestan lo mismo. El ORDER BY tiene que ser exactamente las columnas del WHERE de seek, en el mismo orden, y terminar en algo único. El coste: no puedes saltar a 'página 47', solo 'siguiente' — que es justo lo que un scroll infinito necesita."
❌ Error del candidato promedio: comparar solo por fecha (o.OrderDate < @lastDate) y repetir/saltar filas cuando hay fechas iguales — falta el desempate por id · ofrecer keyset cuando piden un paginador con números de página.
using var conn = new SqlConnection(connectionString);
await conn.OpenAsync();
using var cmd = new SqlCommand(sql, conn);
cmd.Parameters.Add("@fromDate", SqlDbType.DateTime2).Value = fromDate; // tipo explícito
cmd.Parameters.Add("@toExclusive", SqlDbType.DateTime2).Value = toExclusive;
cmd.Parameters.Add("@pageIndex", SqlDbType.Int).Value = pageIndex;
cmd.Parameters.Add("@pageSize", SqlDbType.Int).Value = pageSize;
var items = new List<OrderLineRow>();
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
items.Add(new OrderLineRow(
reader.GetInt32(0), reader.GetInt32(1), reader.GetDateTime(2),
reader.GetString(3), reader.GetInt32(4), reader.GetDecimal(5), reader.GetDecimal(6)));
await reader.NextResultAsync(); // pasar al COUNT
await reader.ReadAsync();
var total = reader.GetInt32(0);
La frase que te distingue: "Lo mismo, más verboso. Dos detalles: Parameters.Add con el SqlDbType explícito, no AddWithValue — AddWithValue infiere el tipo y a veces mete un nvarchar donde la columna es varchar, y esa conversión implícita rompe el uso del índice; es el clásico 'va rápida en SSMS y lenta desde la app'. Y mapeo por ordinal con GetInt32(0), que es lo que Dapper hace por debajo. NextResult para el COUNT."
❌ Error del candidato promedio: AddWithValue (conversión implícita → índice no usado) · olvidar NextResultAsync() y leer el count del primer result set · no envolver reader/conn en using.
Cuando terminas la pieza base, te sondean. Aquí no escribes: nombras el concepto y el trade-off en una frase, y sigues. Eso es criterio senior.
| Te preguntan | Tu frase (con el término) |
|---|---|
| "¿y si son 100.000 filas?" | Keyset en vez de OFFSET (variante G). OFFSET en página profunda lee y descarta; keyset salta por índice, coste constante. |
| "¿y si el producto está borrado?" | LEFT JOIN dbo.Product, y lo digo como decisión: incluyo la línea con ProductName en NULL. INNER la escondería. |
| "¿por qué ese índice?" | IX_Order(OrderDate DESC, OrderId DESC) cubre filtro y orden — el motor no ordena aparte. Con INCLUDE de las columnas del SELECT sería un covering index y evitaría el key lookup. |
| "¿y si dos usuarios escriben a la vez?" | Lost update. Descuento de stock en una sola UPDATE … SET Stock = Stock - @q, o concurrencia optimista con rowversion en el WHERE del UPDATE. |
| "¿cómo sabes que la query es lenta y no la red?" | Plan de ejecución: Index Seek vs Table Scan, y estimated vs actual rows. SET STATISTICS IO ON para los reads lógicos. |
| "el reporte lo quieren en Excel, 500k filas" | No cargo todo en memoria: reader en streaming hacia el archivo, sin .ToList(). Sin paginación, un cursor de lectura. |
| "¿esto no es un N+1?" | No — es un JOIN, una query. El N+1 sería traer los pedidos y luego un query por pedido para sus líneas. Se resuelve con JOIN o WHERE OrderId IN (…). |
| "¿cómo lo testeas sin nuestra BD?" | Test del endpoint con la interfaz del servicio mockeada (no toca SQL). Para el SQL en sí, Testcontainers con una imagen de SQL Server o una BD de integración. |
| "¿y si la fecha viene sin hora?" | La columna es datetime2; el cliente manda toExclusive = primer instante del día siguiente. Si mandara solo fecha, la normalizo en el endpoint, nunca en el WHERE (SARGable). |
Truco: ensaya esta tabla con alguien que te dispare las preguntas en desorden. El reflejo de "nombro el término, doy el trade-off en una frase, y sigo" —sin quedarte pensando— es exactamente lo que separa al que sube a Full-Stack #1 del que "lo hizo bien".
Así queda prueba.cs al final. Practica tecleándolo entero, narrando, hasta que salga en ~12–15 min sin trabarte.
Truco final: no lo memorices como un bloque — ya lo memorizaste en 5 piezas (sección "1 · El archivo" de Guion completo). Este código es solo la prueba de que las piezas encajan. Si te sale completo, ya sabes el guion; si se traba en una costura, vuelve a esa pieza sola, no a repasar todo desde cero.
// Prueba tecnica - reporte de lineas de pedido por rango de fechas, paginado
//
// Mis 3 tablas:
// [Order] (OrderId PK, OrderDate, Status)
// Product (ProductId PK, Sku, Name)
// OrderLine (OrderLineId PK, OrderId -> [Order], ProductId -> Product, Quantity, UnitPrice)
//
// Firmas del chat: (aqui va lo que me peguen)
using System.Data;
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.AspNetCore.Mvc;
// ---------- 1) El metodo de servicio ----------
public class OrderReportService(string connectionString)
{
public async Task<PagedResult<OrderLineRow>> GetOrderLinesAsync(
DateTime fromDate, DateTime toExclusive, int pageIndex, int pageSize)
{
const string sql = @"
SELECT ol.OrderLineId, o.OrderId, o.OrderDate,
p.Name AS ProductName, ol.Quantity, ol.UnitPrice,
ol.Quantity * ol.UnitPrice AS LineTotal
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
INNER JOIN dbo.Product AS p ON p.ProductId = ol.ProductId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive
ORDER BY o.OrderDate DESC, ol.OrderLineId DESC
OFFSET (@pageIndex * @pageSize) ROWS FETCH NEXT @pageSize ROWS ONLY;
SELECT COUNT(*)
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive;";
using var conn = new SqlConnection(connectionString);
using var grid = await conn.QueryMultipleAsync(sql,
new { fromDate, toExclusive, pageIndex, pageSize });
var items = (await grid.ReadAsync<OrderLineRow>()).ToList();
var total = await grid.ReadSingleAsync<int>();
return new PagedResult<OrderLineRow>(items, total, pageIndex, pageSize);
}
}
// ---------- 2) El endpoint ----------
[ApiController]
[Route("api/reports")]
public class OrderReportController(OrderReportService service) : ControllerBase
{
[HttpGet("order-lines")]
public async Task<ActionResult<PagedResult<OrderLineRow>>> GetOrderLines(
[FromQuery] DateTime fromDate,
[FromQuery] DateTime toExclusive,
[FromQuery] int pageIndex = 0,
[FromQuery] int pageSize = 20)
{
if (toExclusive <= fromDate)
return BadRequest("El rango de fechas no es valido.");
pageIndex = Math.Max(0, pageIndex);
pageSize = Math.Clamp(pageSize, 1, 100);
var result = await service.GetOrderLinesAsync(fromDate, toExclusive, pageIndex, pageSize);
return Ok(result);
}
}
// ---------- Tipos (si no me los dan, los defino yo) ----------
public record OrderLineRow(
int OrderLineId, int OrderId, DateTime OrderDate,
string ProductName, int Quantity, decimal UnitPrice, decimal LineTotal);
public record PagedResult<T>(IReadOnlyList<T> Items, int Total, int PageIndex, int PageSize);
NEWCO LLC es la razón social; la marca es Vita Tienda de Cocó March (VitaTienda): tienda en línea de salud, bienestar y cuidado personal — suplementos, vitaminas, probióticos, kits, skincare, limpieza natural. Cara de marca: Dra. Cocó March (naturópata; las fórmulas se presentan como diseñadas por ella; claim de marketing "más de 1 millón de mujeres").
Mercado: hispano de EE. UU. + Latinoamérica. Operación bilingüe español/inglés, atención toda la semana. Es una empresa de e-commerce con equipo de desarrollo interno — no una empresa de tecnología. Footprint pequeño (12 seguidores en Computrabajo, 5 vacantes, sin reseñas de empleados).
store.dracocomarch.com/es/… → patrón de URL con id numérico = PrestaShop.tienda.dracocomarch.com/en-us/collections/… → Shopify (mercado /en-us).Lectura: lo más probable es que estén unificando PrestaShop + Shopify + Amazon detrás de un backend .NET propio (catálogo / inventario / pedidos como fuente de verdad), con Stripe para pago. "Integrar nuevos proveedores externos tomando ownership de toda la capa (back + front)" describe exactamente ese trabajo. Confírmalo preguntando (ver Preguntas → tus preguntas).
| Área | Tecnología | Confianza |
|---|---|---|
| Frontend | Angular 20.3, TypeScript, RxJS, Angular Material, Bootstrap | Alta (JD + notas) |
| Frontend arquitectura | Microfrontends con Native Federation, federation.manifest.json | Alta (notas) |
| Testing frontend | Karma/Jasmine y Jest | Alta (notas) |
| Backend | .NET / ASP.NET, REST API | Alta (JD) |
| Acceso a datos | SQL Server (+ MySQL), escrito a mano — probablemente Dapper o ADO.NET, no EF pesado | Media — preguntar cuál |
| Cloud / CI | Azure, Azure DevOps (pipelines + boards) | Alta (JD) |
| Integraciones | Shopify Admin API, Amazon SP-API, Stripe | Alta (JD + notas) |
"Calidad, servicio cercano al cliente, experiencia de compra confiable, innovación en producto, excelencia en servicio." Traducción práctica: equipo chico, customer-obsessed, no romper producción (18 módulos vivos), alto impacto individual, ownership de punta a punta.
Sin datos públicos de LinkedIn (empresa no indexada). Espera 1–2 personas: quien redactó las notas (perfil técnico senior / tech lead, escribe "guion" y "pruebas técnicas" — lleva el proceso) y posiblemente alguien de producto u operación. Trata las notas como si vinieran del tech lead que decidirá.
"Escribe a mano un join de tres tablas con filtro por fecha y paginación, y explica la transacción." Practica hasta que salga sin pensar. Abajo: el modelo, luego cada concepto que te van a picar.
Mismo esquema de datos-prueba.sql — 3 tablas, sin cliente. No lo compliques agregando una tabla que la prueba no pide.
CREATE TABLE dbo.Product (
ProductId INT IDENTITY PRIMARY KEY,
Sku VARCHAR(32) NOT NULL UNIQUE,
Name NVARCHAR(160) NOT NULL
);
CREATE TABLE dbo.[Order] (
OrderId INT IDENTITY PRIMARY KEY,
OrderDate DATETIME2(0) NOT NULL,
Status VARCHAR(20) NOT NULL
);
CREATE TABLE dbo.OrderLine (
OrderLineId INT IDENTITY PRIMARY KEY,
OrderId INT NOT NULL REFERENCES dbo.[Order](OrderId),
ProductId INT NOT NULL REFERENCES dbo.Product(ProductId),
Quantity INT NOT NULL,
UnitPrice DECIMAL(10,2) NOT NULL
);
-- índices que justifican el WHERE y el ORDER BY
CREATE INDEX IX_Order_Date ON dbo.[Order](OrderDate DESC, OrderId DESC) INCLUDE (Status);
CREATE INDEX IX_OrderLine_Order ON dbo.OrderLine(OrderId);
CREATE INDEX IX_OrderLine_Product ON dbo.OrderLine(ProductId);
-- Renglones de pedido de un rango de fechas, con producto, paginado.
SELECT
o.OrderId,
o.OrderDate,
p.Sku,
p.Name AS ProductName,
ol.Quantity,
ol.UnitPrice,
ol.Quantity * ol.UnitPrice AS LineTotal
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
INNER JOIN dbo.Product AS p ON p.ProductId = ol.ProductId
WHERE o.OrderDate >= @FromDate
AND o.OrderDate < @ToDateExclusive -- intervalo semiabierto: NO uses BETWEEN
ORDER BY o.OrderDate DESC, o.OrderId DESC, ol.OrderLineId DESC -- orden determinista
OFFSET (@PageIndex * @PageSize) ROWS
FETCH NEXT @PageSize ROWS ONLY;
>= @From AND < @ToExclusive en vez de BETWEEN: si OrderDate tiene hora, BETWEEN '2026-01-01' AND '2026-01-31' pierde casi todo el día 31. Señal senior inmediata.CAST(o.OrderDate AS date) = …) — mata el uso del índice. Filtra contra la columna cruda.OFFSET/FETCH exige ORDER BY, y si el orden no es único la paginación repite o se salta filas entre páginas. Siempre termina con una columna única de desempate (PK).@PageIndex base 0.@FromDate), nunca concatenar strings: inyección SQL + reutilización de plan en caché.IX_Order_Date (OrderDate DESC, OrderId DESC) cubre filtro + orden; los FK de OrderLine evitan scans en los joins.LEFT JOIN dbo.Product. Decláralo como decisión, no por descuido.-- Opción A: dos queries en el mismo request (recomendado para páginas profundas)
SELECT COUNT(*)
FROM dbo.OrderLine ol
JOIN dbo.[Order] o ON o.OrderId = ol.OrderId
WHERE o.OrderDate >= @FromDate AND o.OrderDate < @ToDateExclusive;
-- Opción B: en la misma query con ventana (cuesta un scan del set filtrado)
SELECT ..., COUNT(*) OVER() AS TotalRows
FROM ... WHERE ... ORDER BY ... OFFSET ... FETCH ...;
OFFSET grande es lento: el motor lee y descarta N filas. Para "cargar más" infinito o páginas profundas, keyset:
-- pasas el último (OrderDate, OrderId) de la página anterior
WHERE o.OrderDate >= @FromDate AND o.OrderDate < @ToDateExclusive
AND (o.OrderDate < @LastDate
OR (o.OrderDate = @LastDate AND o.OrderId < @LastOrderId))
ORDER BY o.OrderDate DESC, o.OrderId DESC
OFFSET 0 ROWS FETCH NEXT @PageSize ROWS ONLY;
Ventaja: coste constante por página, usa el índice directo. Desventaja: no puedes "saltar a la página 47".
Lo más probable: una escritura de varios pasos (crear pedido + renglones + descontar stock), que es justo el tipo de operación de un POS / recepción electrónica. Para este ejemplo le agrego una columna Stock a Product — es lo único que le falta al esquema de arriba para que la transacción tenga algo que proteger.
ALTER TABLE dbo.Product ADD Stock INT NOT NULL CONSTRAINT CK_Product_Stock CHECK (Stock >= 0) DEFAULT 0;
CREATE PROCEDURE dbo.CreateOrder
@Lines dbo.OrderLineTvp READONLY -- table-valued parameter
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- error en runtime => rollback automático, sin transacciones "doomed"
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO dbo.[Order] (OrderDate, Status)
VALUES (SYSUTCDATETIME(), 'Pending');
DECLARE @OrderId INT = SCOPE_IDENTITY();
INSERT INTO dbo.OrderLine (OrderId, ProductId, Quantity, UnitPrice)
SELECT @OrderId, l.ProductId, l.Quantity, l.UnitPrice
FROM @Lines AS l;
-- descuento de stock con bloqueo, en una sola sentencia (evita race read-then-write)
UPDATE p WITH (ROWLOCK)
SET p.Stock = p.Stock - l.Quantity
FROM dbo.Product AS p
JOIN @Lines AS l ON l.ProductId = p.ProductId;
-- el CHECK (Stock >= 0) ya protege; validación explícita para mensaje claro
IF EXISTS (SELECT 1 FROM dbo.Product p JOIN @Lines l ON l.ProductId = p.ProductId WHERE p.Stock < 0)
THROW 50001, 'Stock insuficiente para uno o más productos.', 1;
COMMIT TRANSACTION;
SELECT @OrderId AS OrderId;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
THROW; -- re-lanza; la capa .NET registra y traduce
END CATCH
END
| Concepto | Qué decir |
|---|---|
| ACID | Atomicidad (los 3 pasos se confirman juntos o ninguno), Consistencia (stock nunca negativo — CHECK + validación), Aislamiento (nivel configurable), Durabilidad (una vez COMMIT, sobrevive a caída). |
SET XACT_ABORT ON | Con esto, un error de runtime aborta y revierte toda la transacción automáticamente y evita dejar una transacción abierta/"doomed". Va al inicio del proc. Señal senior. |
XACT_STATE() / @@TRANCOUNT | Comprobar en el CATCH antes de hacer ROLLBACK. XACT_STATE() = -1 = transacción no confirmable, solo rollback. |
| Niveles de aislamiento | Default READ COMMITTED. Para "leer stock y luego actualizar" el riesgo es lost update; se resuelve haciendo el descuento en una sola UPDATE (arriba), o con hints UPDLOCK, HOLDLOCK, o SERIALIZABLE. Menciona READ COMMITTED SNAPSHOT (RCSI) para reducir bloqueos de lectura. |
| Deadlocks | Orden de acceso consistente a las tablas en todos los procs; reintento en error 1205 desde la capa de aplicación (Polly). |
| Concurrencia optimista | Columna rowversion comprobada en el WHERE del UPDATE; si afecta 0 filas, alguien más ya cambió el registro. En EF Core: atributo [Timestamp]. |
| Transacciones cortas | Nada de llamadas HTTP dentro de la transacción. El cobro a Stripe / la llamada a Shopify va fuera; se coordina con idempotency key + patrón outbox / saga. Esto conecta directo con su trabajo de integraciones — dilo tú. |
public async Task<IReadOnlyList<OrderLineRow>> GetLinesAsync(
DateTime fromDate, DateTime toExclusive, int pageIndex, int pageSize, CancellationToken ct)
{
const string sql = @"
SELECT o.OrderId, o.OrderDate,
p.Sku, p.Name AS ProductName, ol.Quantity, ol.UnitPrice
FROM dbo.OrderLine ol
JOIN dbo.[Order] o ON o.OrderId = ol.OrderId
JOIN dbo.Product p ON p.ProductId = ol.ProductId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive
ORDER BY o.OrderDate DESC, o.OrderId DESC, ol.OrderLineId DESC
OFFSET (@pageIndex * @pageSize) ROWS FETCH NEXT @pageSize ROWS ONLY;";
await using var conn = new SqlConnection(_connString);
var cmd = new CommandDefinition(sql,
new { fromDate, toExclusive, pageIndex, pageSize }, cancellationToken: ct);
var rows = await conn.QueryAsync<OrderLineRow>(cmd);
return rows.AsList();
}
// transacción con Dapper
await using var conn = new SqlConnection(_connString);
await conn.OpenAsync(ct);
await using var tx = await conn.BeginTransactionAsync(ct);
try
{
var orderId = await conn.ExecuteScalarAsync<int>(insertOrderSql, param, tx);
await conn.ExecuteAsync(insertLinesSql, lines, tx);
await conn.ExecuteAsync(updateStockSql, lines, tx);
await tx.CommitAsync(ct);
}
catch { await tx.RollbackAsync(ct); throw; }
await using var conn = new SqlConnection(_connString);
await conn.OpenAsync(ct);
await using var tx = (SqlTransaction)await conn.BeginTransactionAsync(ct);
await using var cmd = new SqlCommand(sql, conn, tx);
cmd.Parameters.Add("@fromDate", SqlDbType.DateTime2).Value = fromDate;
cmd.Parameters.Add("@toExclusive", SqlDbType.DateTime2).Value = toExclusive;
cmd.Parameters.Add("@pageIndex", SqlDbType.Int).Value = pageIndex;
cmd.Parameters.Add("@pageSize", SqlDbType.Int).Value = pageSize;
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct)) { /* map por índice ordinal */ }
await tx.CommitAsync(ct);
Puntos: Parameters.Add con tipo explícito (evita conversiones implícitas que rompen el índice), using/await using en todo, CancellationToken, mapear por ordinal para velocidad.
// consulta LINQ equivalente
var q = _db.OrderLines
.Where(ol => ol.Order.OrderDate >= fromDate && ol.Order.OrderDate < toExclusive)
.OrderByDescending(ol => ol.Order.OrderDate).ThenByDescending(ol => ol.OrderLineId)
.Select(ol => new OrderLineRow { /* ... */ })
.Skip(pageIndex * pageSize).Take(pageSize);
// transacción explícita
await using var tx = await _db.Database.BeginTransactionAsync(ct);
// ... SaveChangesAsync varias veces ...
await tx.CommitAsync(ct);
// SQL crudo dentro de EF
_db.Database.SqlQuery<OrderLineRow>($"SELECT ... WHERE OrderDate >= {fromDate}");
Di que EF SaveChanges ya es transaccional por llamada; la transacción explícita es para agrupar varias llamadas o mezclar con SQL crudo. Concurrencia optimista con [Timestamp] rowversion.
GROUP BY + SUM + ORDER BY + TOP.LEFT JOIN … WHERE ol.OrderLineId IS NULL o NOT EXISTS.GROUP BY p.ProductId, DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1).HAVING COUNT(DISTINCT o.OrderId) > 3.CreateOrder completo, probando el rollback (mete un producto con stock 1 y pide 5).La prueba pide exactamente esto en orden: el SQL a mano (ya lo tienes), un método de servicio que lo ejecute, y el endpoint que lo expone. Sin EF, sin procedimientos almacenados. Abajo: cómo debe quedar en un .NET moderno, luego tu patrón real en SEUS como contraste — porque si te preguntan "¿así trabajas normalmente?", la respuesta honesta es "no exactamente, y sé por qué".
Mismas 3 tablas de la pestaña SQL (Order/OrderLine/Product). Tres capas: interfaz del servicio (testeable), implementación con Dapper, controller delgado.
// ---- DTO de salida ----
public record OrderLineRow(
int OrderId, DateTime OrderDate,
string Sku, string ProductName, int Quantity, decimal UnitPrice);
// ---- Interfaz del servicio (esto es lo que inyectas y lo que mockeas en el test) ----
public interface IOrderService
{
Task<IReadOnlyList<OrderLineRow>> GetLinesAsync(
DateTime fromDate, DateTime toExclusive, int pageIndex, int pageSize, CancellationToken ct);
}
// ---- Implementación ----
public class OrderService : IOrderService
{
private readonly string _connString;
public OrderService(IConfiguration config) => _connString = config.GetConnectionString("Default")!;
public async Task<IReadOnlyList<OrderLineRow>> GetLinesAsync(
DateTime fromDate, DateTime toExclusive, int pageIndex, int pageSize, CancellationToken ct)
{
const string sql = @"
SELECT o.OrderId, o.OrderDate,
p.Sku, p.Name AS ProductName, ol.Quantity, ol.UnitPrice
FROM dbo.OrderLine ol
JOIN dbo.[Order] o ON o.OrderId = ol.OrderId
JOIN dbo.Product p ON p.ProductId = ol.ProductId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive
ORDER BY o.OrderDate DESC, o.OrderId DESC, ol.OrderLineId DESC
OFFSET (@pageIndex * @pageSize) ROWS FETCH NEXT @pageSize ROWS ONLY;";
await using var conn = new SqlConnection(_connString);
var cmd = new CommandDefinition(sql,
new { fromDate, toExclusive, pageIndex, pageSize }, cancellationToken: ct);
return (await conn.QueryAsync<OrderLineRow>(cmd)).AsList();
}
}
// ---- Controller ----
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService) => _orderService = orderService;
[HttpGet("lines")]
public async Task<ActionResult<IReadOnlyList<OrderLineRow>>> GetLines(
[FromQuery] DateTime fromDate, [FromQuery] DateTime toExclusive,
[FromQuery] int pageIndex, [FromQuery] int pageSize, CancellationToken ct)
{
if (pageSize is <= 0 or > 200) return BadRequest("pageSize fuera de rango.");
var rows = await _orderService.GetLinesAsync(fromDate, toExclusive, pageIndex, pageSize, ct);
if (rows.Count == 0) return NotFound();
return Ok(rows);
}
}
// ---- Program.cs ----
builder.Services.AddScoped<IOrderService, OrderService>();
ActionResult<T> porque necesito devolver 200 con datos, 404 si no hay filas, o 400 si el paginado viene mal — con un tipo de retorno plano no puedo variar el código de estado. CancellationToken de punta a punta porque si el cliente cierra la petición, no quiero seguir pegándole a la base."
Así se ve un controller real tuyo, CatalogoController — arquitectura Controller → BR (Business Rules) → DTO, con las reglas de negocio inyectadas por interfaz:
[ApiController]
[Route("api/[controller]")]
[EnableCors("_myAllowSpecificOrigins")]
public class CatalogoController : ControllerBase
{
private readonly IBrConfiguration _brConfiguration;
// + otras 3 interfaces de negocio inyectadas por constructor
[HttpGet]
[Route("ConsultarSucursal/")]
public RespuestaGenericaDto<SucursalDto> ConsultarSucursal(int uicodSucursal)
{
return _brConfiguration.ObtenerSucursal(uicodSucursal);
}
}
Y así se ve la capa BR, con reglas de negocio numeradas y sin ActionResult — el patrón es de guardar un pedido manual (BRGuardarPedido), la validación (RN-07 a RN-11) antes de tocar la base:
public class BRGuardarPedido(IBRPedidosManuales bRPedidosManuales, ILambdaManager _lambdaManager,
IBRSQS brSQS, IBRGuardarAdjunto bRGuardarAdjunto) : IBRGuardarPedido
{
// RN-07: Validaciones al Guardar (V1-V4)
public RespuestaGenericaDto<ValidarGuardarPedidoResponse> ValidarGuardarPedido(ValidarGuardarPedidoRequest request)
{
var result = new RespuestaGenericaDto<ValidarGuardarPedidoResponse>();
var erroresValidacion = EjecutarValidacionesGuardar(request);
if (erroresValidacion.Any())
return result.ToError(string.Join("\n", erroresValidacion), 3400);
var fechaHoraPedido = ObtenerFechaHoraServidorAsync(request.UiCodSucursal).Result; // ← ver aviso abajo
if (!fechaHoraPedido.ProcesoCorrecto) return result.ToError(fechaHoraPedido.Descripcion);
// ... clasificación, agrupación, cálculo financiero, persistencia ...
}
}
| En SEUS | Lo que pide la prueba hoy | Por qué existe la diferencia |
|---|---|---|
RespuestaGenericaDto<T> devuelto directo | ActionResult<T> con Ok()/NotFound()/BadRequest() | RespuestaGenericaDto es un contrato interno pre-existente (siempre 200 HTTP, el éxito/error va en el body: ProcesoCorrecto). Estándar y correcto para ese ecosistema; no es lo que un endpoint REST idiomático expone hacia afuera. |
| Métodos síncronos en esa capa | async/await de punta a punta | Legado: gran parte de APIPROCESO sí es async (BrAltaContactos, SOA), pero esta capa concreta no se migró. Lo reconozco como deuda técnica, no como que no sé usar async. |
.Result sobre una llamada async | await | Esto es un antipatrón real que puedo señalar si me preguntan: .Result/.Wait() bloquea el hilo y puede causar deadlock en contextos con SynchronizationContext (clásico en ASP.NET clásico; menos en Core, pero sigue gastando un hilo del pool). La corrección es hacer ValidarGuardarPedido también async Task<...> y usar await. |
ActionResult/async así?", NO lo escondas ni te disculpes de más. Contesta en tres frases: qué patrón usan (RespuestaGenericaDto, éxito/error en el body), por qué existe (contrato interno consistente entre decenas de módulos), y que sabes cuál es el estándar REST y por qué lo vas a usar hoy. Eso es exactamente "usaste algo, pero también tienes el concepto".
| Concepto | Qué decir |
|---|---|
ActionResult<T> vs IActionResult vs T | ActionResult<T>: puedes devolver el objeto tipado o un resultado HTTP (NotFound()), y Swagger infiere el tipo de respuesta. IActionResult: solo resultados HTTP, sin tipo inferido. Devolver T plano (tu patrón SEUS) siempre es 200, la app decide el "estado" en el body. |
[ApiController] | Activa binding inference, valida ModelState automático (400 si el modelo no es válido, sin código extra) y exige rutas con atributos. |
| Inyección de dependencias — ciclos de vida | Scoped (una instancia por request — lo normal para un servicio con DbConnection), Singleton (una para toda la app — cuidado con estado mutable), Transient (nueva cada vez que se pide). |
| Async/await — por qué importa aquí | Libera el hilo mientras espera I/O (la consulta SQL); con miles de requests concurrentes eso es la diferencia entre escalar o agotar el thread pool. Sync-sobre-async (.Result) es el error clásico que hay que saber señalar. |
| DTO vs entidad | El DTO (OrderLineRow) es la forma que necesita el cliente; no expongas la entidad de EF/tabla directo — acopla el contrato público al esquema de base de datos. |
| Manejo de errores | Middleware global (UseExceptionHandler) para errores no esperados + try/catch puntual solo donde puedes dar un mensaje mejor que el genérico. En tu patrón SEUS ese "no esperado" se traduce a result.ToError(). |
| Pregunta | Respuesta corta |
|---|---|
| "¿Por qué una interfaz para el servicio si solo hay una implementación?" | Testeabilidad (mockear en el test del controller) y para poder cambiar la implementación (otro motor, otra librería de acceso a datos) sin tocar el controller. |
"¿Qué pasa si fromDate es mayor que toExclusive?" | Debería validarse y devolver 400 — lo digo en voz alta aunque no lo escriba si el tiempo aprieta: "aquí faltaría validar el rango, en producción lo haría con un FluentValidation o una guarda simple." |
| "¿Cómo lo probarías?" | Un test del controller mockeando IOrderService (no toca SQL Server); opcionalmente un test de integración con una base real o Testcontainers para el SQL en sí. |
| "¿Por qué Dapper y no ADO.NET crudo?" | Dapper es una capa delgada sobre ADO.NET: mapea el resultado a objetos sin el boilerplate de ExecuteReaderAsync + leer por ordinal, pero sigue siendo SQL escrito a mano — no es un ORM que genera SQL por ti como EF. |
| "¿Y si te pido que no uses Dapper, solo ADO.NET puro?" | Ve al details de ADO.NET crudo en la pestaña SQL — mismo query, SqlCommand + Parameters.Add + ExecuteReaderAsync + mapeo manual por índice ordinal. |
| Promise | Observable | |
|---|---|---|
| Valores | uno solo | 0..∞ (un flujo en el tiempo) |
| Ejecución | eager: corre al crearse | lazy: corre al hacer subscribe() |
| Cancelable | no | sí: unsubscribe() aborta (p. ej. cancela el HTTP) |
| Operadores | .then()/.catch() | pipe(map, filter, switchMap, debounceTime, retry, combineLatest…) |
| Sync/async | siempre async (microtask) | puede emitir sync o async |
| Multi-suscriptor | comparte el único resultado | cold: cada subscribe reejecuta; hot/share(): multicast |
En Angular: HttpClient devuelve Observable → al desuscribir cancela la petición (ideal para typeahead con switchMap). Eventos del Router, form.valueChanges, interop con Signals (toSignal(), toObservable()). El pipe async se suscribe y desuscribe solo.
Fugas de memoria = suscripciones sin gestionar. Soluciones: pipe async, takeUntilDestroyed() (Angular 16+), DestroyRef.
| Operador | Comportamiento | Caso real en un POS / backoffice |
|---|---|---|
switchMap | cancela la anterior | búsqueda de producto: solo importa la última tecla |
mergeMap | todas en paralelo | disparar N validaciones independientes a la vez |
concatMap | cola, en orden | guardar renglones de un pedido en secuencia sin pisarse |
exhaustMap | ignora nuevas mientras trabaja | botón "confirmar venta": evita doble submit |
Extra: forkJoin (espera a que todas terminen — cargar datos de arranque), combineLatest (recombina al cambiar cualquiera — filtros), catchError + retry/retryWhen, shareReplay(1) (cachear la respuesta de un GET de catálogo).
Si te preguntan "dame un caso real de forkJoin" o "¿por qué a veces usas Promise y no Observable?", esto es lo que respondes — es código tuyo, no un ejemplo de tutorial.
forkJoin real — cotizador que dispara 7-8 llamadas en paraleloreturn forkJoin(request).pipe( // request: array de 7-8 Observables (existencia, precio, RARF,
// alerta de producto, programa de lealtad, mecánicas, códigos de barras)
map((result: any) => {
const validationResults = validateMultipleResponses(result, this.loadingSpinnerService);
if (validationResults.every(v => v)) {
response.Data = result;
return response;
} else {
const errorResult = result.find((r: any) => (!r?.procesoCorrecto || !r?.respuesta?.procesoCorrecto));
response.Message = errorResult.descripcion ?? errorResult.respuesta.descripcion;
response.Success = false;
return response;
}
}),
catchError((err: any) => {
response.Success = false;
return of(response); // nunca deja el stream en error: siempre resuelve a un response tipado
})
);
forkJoin y no combineLatest aquí: las 7-8 llamadas son peticiones HTTP de una sola respuesta — quiero esperar a que todas terminen una vez y combinar el resultado final, no recalcular cada vez que una emite. combineLatest es para flujos que siguen emitiendo (dos valueChanges de un formulario, por ejemplo) donde sí me importa cada recombinación. Y el catchError al final, no por llamada: si cualquiera falla, todo el cotizador falla junto — es una unidad de negocio, no llamadas independientes.
async-await en vez de Observable — la otra mitad de la respuestaEl flujo de guardar un pedido manual (MasterGuardarPedidoService) está escrito con async/await sobre una capa de Promises, no con Observables encadenados:
async validateAndSendOrder(proveedor, motivo, solicitante, esRepresentanteVentas,
uiCodAdjunto, nombreArchivo, urlDocumento, observaciones,
segmento, productos): Promise<RespuestaGenericaResult<ResponseEnviarGuardarPedido>> {
this.loadingSpinner.show("Validando solicitud...");
const validationResult = await this.pedidoManualPromise.validarGuardarPedido(/* ... */);
if (!validationResult.procesoCorrecto) {
this.errorCommonService.errorPromise(validationResult, 3000, V_TIPO_ENUM.VALIDAR_PEDIDO_MANUAL);
return toError<ResponseEnviarGuardarPedido>(validationResult.descripcion);
}
if (!validationResult.respuesta.bGenerarSolicitud) {
this.loadingSpinner.hide();
return toError<ResponseEnviarGuardarPedido>(validationResult.respuesta.mensajesValidacion);
}
this.loadingSpinner.updateMessage("Enviando solicitud al servidor...");
const saveResult = await this.pedidoManualPromise.enviarGuardarPedido(/* ... */);
if (!saveResult.procesoCorrecto) {
this.errorCommonService.errorPromise(saveResult, 3000, V_TIPO_ENUM.GUARDAR_PEDIDO_MANUAL);
return saveResult;
}
this.loadingSpinner.hide();
return saveResult;
}
async/await sobre Promises el código de dos pasos con dos posibles cortes tempranos (return en cada if) se lee lineal, de arriba a abajo. Encadenar esto con switchMap/concatMap sería forzar un Observable donde no hace falta el poder de un stream: no hay nada que cancelar ni recombinar. Esa es la diferencia real entre "usé Observables" y "entiendo cuándo un Observable no es la herramienta".
Qué es: el concepto de Module Federation reconstruido sobre ESM nativo + import maps, para que funcione con el build moderno de Angular (esbuild/Vite). Webpack Module Federation dejó de encajar cuando Angular migró a esbuild; @angular-architects/native-federation es el reemplazo. La configuración de remotos vive en federation.manifest.json (el que tienes en tu repo).
shared.NG0203 (inject fuera de contexto). Alinear shared con singleton: true y strictVersion.federation.manifest.json por entorno.Prepárate a dibujarlo: shell (host) → lee federation.manifest.json → genera import map → carga remoteEntry de cada microfrontend bajo demanda → monta su ruta. Deps compartidas resueltas una vez.
computed, efectos con effect); RxJS para flujos asíncronos y coordinación de eventos. Interop con toSignal/toObservable. No migrar todo a Signals porque sí.OnPush + inmutabilidad + async pipe; ChangeDetectorRef. Con Signals, CD granular.imports: [...] en el componente.loadComponent / loadChildren con rutas; @defer blocks para diferir render.updateOn: 'blur'.Eres el único del proceso que nombra las dos. Pregunta 3: "el último test de componente que escribiste y qué comprobaba." Necesitas una anécdota concreta y verificable. Rellena la plantilla con tu último test real.
MasterGuardarPedidoServiceMasterGuardarPedidoService — orquesta validar y enviar un pedido manual (dos métodos: validateAndSaveOrder, validateAndSendOrder).if y nadie lo nota sin test.!procesoCorrecto (falla la validación → llama al manejador de errores y no sigue), la rama bGenerarSolicitud = false (el negocio no permite generar la solicitud → oculta el spinner y corta), y el camino feliz completo hasta enviarGuardarPedido.TestBed.configureTestingModule sin componente — solo el servicio y sus 4 dependencias como jasmine.SpyObj (PedidoManualPromise, ErrorHandlerPromiseService, LogSistemaPromise, LoadingService).async/await sobre Promises, no Observables — el test también es async, con .and.resolveTo(mock) en los spies en vez de fakeAsync+tick().showCallSwal (un SweetAlert) no se mockeaba — es un import ESM inmutable que rompía el spy — así que la cobertura de esa rama se lograba dejándolo ejecutar de verdad en vez de interceptarlo. Es el tipo de detalle sucio-pero-real que demuestra que el test es tuyo.describe('MasterGuardarPedidoService', () => {
let service: MasterGuardarPedidoService;
let pedidoManualPromiseSpy: jasmine.SpyObj<PedidoManualPromise>;
let errorCommonServiceSpy: jasmine.SpyObj<ErrorHandlerPromiseService>;
let loadingSpinnerSpy: jasmine.SpyObj<LoadingService>;
beforeEach(() => {
pedidoManualPromiseSpy = jasmine.createSpyObj('PedidoManualPromise',
['validarGuardarPedido', 'enviarGuardarPedido']);
errorCommonServiceSpy = jasmine.createSpyObj('ErrorHandlerPromiseService', ['errorPromise']);
loadingSpinnerSpy = jasmine.createSpyObj('LoadingService', ['show', 'hide', 'updateMessage']);
TestBed.configureTestingModule({
providers: [
MasterGuardarPedidoService,
{ provide: PedidoManualPromise, useValue: pedidoManualPromiseSpy },
{ provide: ErrorHandlerPromiseService, useValue: errorCommonServiceSpy },
{ provide: LoadingService, useValue: loadingSpinnerSpy },
],
});
service = TestBed.inject(MasterGuardarPedidoService);
});
describe('validateAndSaveOrder', () => {
it('should handle validation failure (!procesoCorrecto)', async () => {
const mockResult = { procesoCorrecto: false, descripcion: 'Error' } as any;
pedidoManualPromiseSpy.validarGuardarPedido.and.resolveTo(mockResult);
const result = await service.validateAndSaveOrder(/* ...args */);
expect(loadingSpinnerSpy.show).toHaveBeenCalled();
expect(errorCommonServiceSpy.errorPromise).toHaveBeenCalledWith(mockResult);
expect(result).toEqual(mockResult);
});
it('should handle bGenerarSolicitud = false', async () => {
const mockResult = { procesoCorrecto: true,
respuesta: { bGenerarSolicitud: false, mensajesValidacion: 'No permitido' } } as any;
pedidoManualPromiseSpy.validarGuardarPedido.and.resolveTo(mockResult);
const result = await service.validateAndSaveOrder(/* ...args */);
expect(loadingSpinnerSpy.hide).toHaveBeenCalled();
expect(result.procesoCorrecto).toBeFalse();
});
});
});
resolveTo), y compruebo tanto el valor de retorno como los efectos colaterales — que se llamó al spinner, que se llamó al manejador de errores con el objeto correcto. Eso es lo que separa un test que sube el coverage de un test que realmente protege la regla de negocio."
describe('SaleButtonComponent', () => {
let fixture: ComponentFixture<SaleButtonComponent>;
let sales: jasmine.SpyObj<SalesService>;
beforeEach(async () => {
sales = jasmine.createSpyObj<SalesService>('SalesService', ['confirm']);
await TestBed.configureTestingModule({
imports: [SaleButtonComponent], // standalone
providers: [{ provide: SalesService, useValue: sales }],
}).compileComponents();
fixture = TestBed.createComponent(SaleButtonComponent);
});
it('deshabilita el botón mientras confirma', fakeAsync(() => {
sales.confirm.and.returnValue(timer(100).pipe(map(() => ({ ok: true }))));
fixture.componentInstance.total = 250;
fixture.detectChanges();
const btn = fixture.nativeElement.querySelector('button') as HTMLButtonElement;
btn.click();
fixture.detectChanges();
expect(btn.disabled).toBeTrue(); // durante la operación
tick(100);
fixture.detectChanges();
expect(btn.disabled).toBeFalse();
expect(sales.confirm).toHaveBeenCalledOnceWith(250);
}));
});
TestBed.configureTestingModule({ imports: [HttpClientTestingModule] });
const http = TestBed.inject(HttpTestingController);
service.getCatalog().subscribe(r => expect(r.length).toBe(2));
const req = http.expectOne('/api/catalog');
expect(req.request.method).toBe('GET');
req.flush([{ id: 1 }, { id: 2 }]);
http.verify();
const loader = TestbedHarnessEnvironment.loader(fixture);
const button = await loader.getHarness(MatButtonHarness.with({ text: 'Confirmar' }));
await button.click();
const input = await loader.getHarness(MatInputHarness);
await input.setValue('ABC-123');
Ventaja: el test no depende del DOM interno de Material; sobrevive a upgrades.
| Karma/Jasmine | Jest | |
|---|---|---|
| Entorno | navegador real (Chrome headless) | jsdom (simulado en Node) |
| Velocidad | más lento (arranca navegador) | rápido, paralelo, watch mode ágil |
| Spies/mocks | jasmine.createSpy | jest.fn(), jest.mock() de módulos |
| Snapshots | no nativo | sí |
| Setup en Angular | viene por defecto | jest-preset-angular |
Por qué mantener ambos: Jest para el ciclo rápido de TDD y CI (miles de tests unitarios de lógica y componentes); Karma cuando necesitas fidelidad de navegador real — APIs del DOM que jsdom no implementa bien, layout, algunas cosas de Material/CDK, o pruebas que rozan lo e2e. Si te preguntan "¿los unificarías?": depende del coste de mantener dos configs vs. lo que se perdería; migrar a Jest todo lo unitario y dejar Karma/Playwright para lo que exige navegador es una postura defendible.
Tests de componente aislados: sin cambio. Lo difícil es la integración cruzando la frontera de federación — ahí conviene contract testing entre shell y remoto, o e2e (Playwright/Cypress) sobre el shell ensamblado.
Todas las frases, sin relleno. Una idea por línea, en el orden en que salen.
Las 3 reglas al hablar (feedback de tu ex jefe): 1 Directo — nada de "creo / me parece / no estoy seguro". 2 Con contexto — dónde lo usaste, qué problema, qué decidiste. 3 Término técnico exacto, no rodeos. Detalle y ejemplos en La prueba.
UNIQUE en SKU, CHECK en stock, las FOREIGN KEY — no solo en C#."OrderDate descendente porque el reporte ordena y pagina por fecha."Input lo que entra. Con [Required]/[Range], si el body no cumple ASP.NET devuelve 400 sin un if mío."COUNT(*) en una sola ida con QueryMultiple."Create: INSERT + SELECT CAST(SCOPE_IDENTITY() AS int) para devolver el id nuevo."Update y Delete devuelven filas afectadas: 0 = no existe = 404."DynamicParameters sobre el input para no repetir la lista de campos."Location vía CreatedAtAction, o 400. PUT y DELETE → 204 sin body, o 404."DataAnnotations."ExecuteAsync afecta 0 filas — hace el DELETE idempotente."WebApplicationFactory: levanta la API real en memoria. Cubren 200, 404, 400 y el 201 de punta a punta."Quantity * UnitPrice."OrderLine, la tabla base, el grano del reporte."Order por OrderId y Product por ProductId; INNER porque solo quiero líneas completas.">= @fromDate AND < @toExclusive, nunca BETWEEN — incluiría el límite y se rompe con horas."ORDER BY determinista que termina en columna única — OFFSET/FETCH lo exige o la paginación baila."OFFSET/FETCH; el COUNT con el mismo WHERE en la misma ida."CAST ni CONVERT: SARGable, usa el índice."toExclusive."SELECT: no necesita transacción explícita."Order + sus OrderLine + descontar stock— la envuelvo en una transacción para que sea atómica: o todo o nada."connection.BeginTransaction(), lo paso a cada Execute, Commit() al final, Rollback() en el catch."READ COMMITTED) suele bastar; subiría a SERIALIZABLE solo si necesito que nadie inserte en el rango que estoy leyendo mientras decido."/api/* al backend; el servicio usa rutas relativas, sin líos de CORS."interface son el contrato — mismos nombres que el JSON de .NET, en camelCase."MatSnackBar."HttpTestingController: por cada método verifico URL, verbo y body, sin tocar la red."AddScoped<IDbConnection>; Dapper la usa por request."/api/* al puerto del API."localhost:4200, antes de MapControllers."CHECK de la base, cada capa conectada y probada por separado antes de unirla."dotnet new de equipo. Dejan estructura y wiring; los cuerpos vacíos. El scaffolding con tooling estándar; la lógica la escribo yo."*Repository del ensamblado entra como Scoped."Pregunta 4: "tu nube es AWS Lambda, no Azure: ¿qué esperas que cambie?" Buena señal si nombras lo que NO equivale. No finjas experiencia en Azure que no tienes; demuestra que sabes traducir conceptos y dónde están las trampas.
| AWS | Azure | ¿Equivale limpio? |
|---|---|---|
| Lambda | Azure Functions | Parcial — Functions usa bindings declarativos (input/output por atributos); en Lambda escribes las llamadas al SDK a mano. Modelo isolated worker en .NET. |
| API Gateway | Azure API Management / HTTP trigger + Front Door | No — APIM es más pesado y caro; para casos simples basta el HTTP trigger de la Function. |
| Step Functions | Durable Functions / Logic Apps | No — Step Functions es una máquina de estados en JSON; Durable Functions es orquestación en código C#. Mentalidad distinta. |
| DynamoDB | Cosmos DB | No — Cosmos es multi-modelo, otro modelo de particionado y de costo (Request Units). |
| S3 | Blob Storage | Sí, cercano. |
| SQS / SNS | Storage Queues / Service Bus / Event Grid | No 1:1 — Service Bus ≈ SQS+SNS (colas + topics, sesiones, dead-letter); Storage Queues es lo simple; Event Grid es push de eventos. Son 3 cosas, no 1. |
| CloudWatch | Application Insights / Azure Monitor | App Insights es más rico para .NET (tracing distribuido casi gratis). |
| IAM roles | Managed Identity + RBAC (+ a veces App Registrations en Entra ID) | No — otro modelo mental de identidad y permisos. |
| SAM / CDK / Serverless Framework | Bicep / ARM / azd, y Azure DevOps Pipelines (ellos) | No — IaC y CI distintos; ellos ya usan Azure DevOps. |
"Mi experiencia de nube es AWS Lambda, así que lo que traigo es el modelo serverless: funciones stateless, cold starts, pensar en idempotencia y en límites de ejecución. Al pasar a Azure, lo que sí traslada casi directo es el enfoque event-driven, Blob ≈ S3, y App Insights me daría más de lo que tenía con CloudWatch. Lo que no equivale y tendría que aprender: Azure Functions usa bindings declarativos en vez de llamar al SDK a mano; Durable Functions no es Step Functions —es orquestación en código, no una máquina de estados JSON—; Cosmos no es DynamoDB en particionado ni en costeo; y la identidad pasa de IAM roles a Managed Identity + RBAC. Además, para el grueso de una API .NET probablemente no sería Functions sino App Service o Container Apps — ¿qué usan ustedes?"
Esa última pregunta te posiciona como alguien que piensa en arquitectura, no solo en servicios sueltos.
"Integrar nuevos proveedores externos tomando ownership de toda la capa." Ten estos patrones listos:
payment_intent.succeeded): verificar firma, responder 2xx rápido, procesar async, tolerar entregas duplicadas y fuera de orden.Stripe.net.Cada pregunta: por qué la hacen, la estructura de tu respuesta, un borrador con tu dato marcado, qué recalcar y qué evitar. Practica en voz alta y cronometrada. No memorices palabra por palabra — con la estructura clara improvisas.
Las 3 reglas al hablar (feedback de tu ex jefe, aplican a toda respuesta): 1 · Seguridad — nada de "creo / siento / pienso / me parece"; esas muletillas dicen "no tengo experiencia real". 2 · Contexto — sitúa cada respuesta: dónde usaste esa tecnología, qué problema resolvía, qué decidiste. 3 · Lenguaje técnico — el término exacto, nunca un rodeo. Ejemplos en La prueba.
Lo que necesito de ti para cerrar los borradores (donde falta va [RELLENA]):
| Letra | Qué es | Cuánto |
|---|---|---|
| Situación | 1 frase de contexto: dónde, cuándo, qué sistema. | ~10 s |
| Tarea | qué tenías que lograr y por qué importaba. | ~10 s |
| Acción | lo que hiciste tú, en primera persona, concreto, con la decisión difícil. | ~40 s (el grueso) |
| Resultado | qué pasó, con número si puedes, y qué aprendiste / cambiaste. | ~15 s |
Regla de oro: la A es donde te evalúan. Di "yo diseñé / yo decidí / yo escribí", nunca "el equipo hizo". 60–90 s por historia; si te pasas de 2 min, te cortan.
Por qué: marca el tono y te da el control de la narrativa. La pregunta de SQL viene justo después — este pitch la desactiva si nombras SQL Server tú mismo.
Estructura (4 movimientos, 60–90 s): quién eres + dónde → dónde tienes más profundidad (frontend) → tu otro lado (backend + nube, sin esconder nada) → qué buscas y por qué encajas.
"Soy desarrollador full-stack con 5 años en [SEUS], en un POS y backoffice para retail farmacéutico — hoy 18+ módulos en producción: compra directa, recepción electrónica, monitor de embarques.
Donde tengo más profundidad es en frontend: Angular —ahora en 20—, RxJS, Angular Material, y migramos a microfrontends con Native Federation, donde fui el mayor contribuidor del repo con 86 commits. Testeo con Karma/Jasmine y con Jest.
Del lado del backend trabajo .NET con REST y SQL Server, con el acceso a datos escrito a mano. Mi experiencia de nube es AWS Lambda.
Busco un puesto full-stack de verdad, remoto, con ownership de features completas — y su stack es casi el mismo en el que ya me muevo, así que contribuyo rápido y a la vez profundizo más en .NET y datos."
Recalca: di "5 años" (es lo del CV) · menciona SQL Server tú mismo · no escondas el inglés básico ni el AWS.
Evita: recitarlo monótono · pasar de 90 s · empezar por la vida personal · "soy apasionado de la tecnología".
Por qué: filtran a quien aplica a todo. Quieren 2–3 razones específicas de ellos.
Estructura: el trabajo (encaja con cómo ya trabajas) → el stack (casi idéntico) → el producto (real, equipo chico, impacto) → dónde quieres crecer (su lado fuerte) → remoto.
"El modelo que describen —feature vertical completa, ownership de la capa de integración— es exactamente cómo ya trabajo. El stack coincide casi campo por campo: Angular 20, RxJS, Material, microfrontends; no pierdo tiempo de arranque. Es e-commerce con producto y usuarios reales, y equipo chico, o sea impacto directo. Y me interesa profundizar justo en .NET + SQL Server e integraciones —Stripe, Shopify, SP-API—, que es donde quiero crecer. Más que es remoto, que es lo que busco."
Evita: "me gusta su cultura" / "es una empresa en crecimiento" — vacío. Nada que aplique a cualquier empresa.
Por qué: el puesto es eso: modelo de datos → servicio → endpoint → componente.
S: En el POS/backoffice farmacéutico, el módulo de Pedidos Manuales — generar y aprobar solicitudes de compra fuera del flujo automático (casos como abasto de vacunas o ajustes con proveedor que la reposición automática no cubre).
T: se necesitaba un flujo con reglas de negocio propias (nivel de aprobación según monto y tipo de producto, validaciones antes de dejar generar la solicitud) y nadie más lo tenía asignado end-to-end.
A: Diseñé el modelo de datos en SQL Server, escribí la capa de reglas de negocio en .NET (BRGuardarPedido, cinco reglas numeradas RN-07 a RN-11), expuse el REST y construí el componente Angular con Material. La decisión técnica: separar validar de enviar en dos métodos distintos (ValidarGuardarPedido / EnviarGuardarPedido) — así la UI puede decirle al usuario por qué no puede generar la solicitud (nivel de aprobación, producto no permitido) sin escribir nada en base de datos, y solo el segundo paso persiste de verdad.
R: Salió a producción y [RELLENA: tu métrica real — solicitudes/semana procesadas, horas ahorradas vs. el proceso manual anterior, o simplemente "reemplazó un proceso que antes se hacía por correo/Excel"]. Hoy es uno de los 18+ módulos vivos y le sigo dando mantenimiento.
Recalca: las cuatro capas, en primera persona. Evita: quedarte en "diseñé la arquitectura" sin bajar a una decisión concreta.
Por qué: corren 18 módulos vivos; "experiencia de compra confiable" es un valor declarado.
S/T: [RELLENA: el bug — qué se rompió y a quién afectó].
A: detección ([¿monitoreo? ¿reporte de usuario?]) → aislé la causa raíz ([RELLENA]) → fix inmediato → y la parte que importa: agregué un test de regresión ([Karma / Jest]) y [cambio de proceso: una validación, una alerta, un paso de revisión en PR].
R: no volvió a pasar; el test lo cubre desde entonces.
Recalca: el 70% de la respuesta es la prevención, no el susto. Evita: culpar a otro; contar el bug con dramatismo y saltarte el "qué cambié".
Por qué: el core del puesto es integrar Stripe / Shopify / SP-API.
Si tienes la historia: [RELLENA: qué proveedor, qué falló — timeouts, rate limit, datos inconsistentes, webhooks duplicados] → cómo lo hiciste resiliente: reintentos con backoff, idempotency key, conciliación periódica.
Si NO la tienes: dilo y pivota — "proveedor de pagos como tal no he integrado; así lo abordaría" y explicas idempotency + outbox + webhooks firmados + circuit breaker (pestaña Cloud). Honesto y demuestra criterio.
Evita: inventar una integración que no hiciste — se cae con una repregunta.
Por qué: quieren saber cómo entras a algo desconocido — como su codebase.
Historia natural: Native Federation. Cuando Angular migró su build a esbuild, Webpack Module Federation (lo que usábamos) dejó de encajar con el pipeline — [RELLENA si recuerdas el plazo/contexto real de esa migración] — así que lo estudié desde cero: import maps, federation.manifest.json, negociación de shared deps. Hoy sostengo más de una docena de microfrontends bajo ese esquema, y me tocó resolver los dolores reales: version skew (dos instancias de Angular cargadas a la vez, NG0203 al inyectar fuera de contexto), comunicación entre microfrontends sin bus de estado nativo. R: deploy independiente por equipo, sin recompilar el shell por cada release de un módulo.
Recalca: que nombras dolores concretos — eso prueba que lo hiciste, no que leíste el README.
Por qué: miden si eres rígido o colaborativo, y si cambias de opinión con datos.
S/T: [RELLENA: la decisión — p. ej. Karma vs Jest, estructura de un módulo, EF vs SQL a mano].
A: expuse mi posición con datos ([RELLENA: qué medí o comparé]) → escuché su argumento → [acordamos X / probamos un spike / cedí en Y porque tenía razón en Z].
R: [qué salió], y aprendí [qué de la otra postura].
Recalca: priorizas el resultado del equipo por encima de tener la razón. Evita: una historia donde tú tenías razón y ya.
Por qué: el JD pide "traducir requerimientos de negocio a specs técnicas".
S/T: [RELLENA: un requerimiento ambiguo que te llegó].
A: hice preguntas para acotarlo ([qué preguntaste]) → lo convertí en una spec con casos y criterios de aceptación → lo validé con quien lo pidió antes de construir.
R: se construyó una vez, sin retrabajo.
Recalca: "pregunto antes de asumir" — es literal lo que evalúan en la prueba técnica también.
"Es cierto que no lo destaqué, y debí hacerlo: el acceso a datos en SQL Server es parte de mi día a día en el backend —consultas, joins, transacciones, todo escrito a mano—. Lo dejé implícito bajo '.NET / backend' y fue un error de redacción del CV, no de experiencia. De hecho por eso agradezco la prueba práctica."
Recalca: "error de redacción, no de experiencia" + agradecer la prueba. Evita: justificarte largo o sonar a la defensiva. Dos frases y adelante.
"Mi inglés es [básico / básico-intermedio — sé honesto] de lectura. Consumo documentación técnica en inglés —la de Angular ya así— y me apoyo en traducción con texto denso. No lo hablo con fluidez todavía; lo trabajo activamente [práctica diaria — Anki / una app]. Para comunicación escrita asíncrona me defiendo; para una reunión hablada en inglés todavía no. Sé que SP-API, Stripe y Angular están documentados en inglés y con eso puedo."
Evita: exagerar (lo testean en la misma llamada) · disculparte tres veces. Una respuesta clara + el plan de mejora.
| Lo que dudan | Encuadre (di esto, con tu verdad) |
|---|---|
| Un solo empleador toda la carrera | "No cambié de empresa porque no dejé de crecer dentro: pasé de [rol inicial] a mayor contribuidor del repo y dueño de módulos críticos. Lo que sí cambió constantemente fue el stack debajo de mí — Angular 17→20, adoptar Native Federation, dos frameworks de test. Ahora busco ese cambio de entorno a propósito." |
| Pasante sin titular desde 2021 | Factual y hacia adelante: "[RELLENA tu situación real]. En estos años el trabajo real ha sido mi formación: 18 módulos en producción lo respaldan." No te disculpes, no te extiendas. |
"Entré a un repo grande y en 4 años me volví su mayor contribuidor; sé leer código ajeno y moverme en un dominio complejo — retail farmacéutico con 18 módulos y sus reglas. Un codebase nuevo es la misma habilidad: primero leo el flujo de una feature de punta a punta, luego toco."
Cuatro razones, en positivo, sin quejarte del empleo actual: crecimiento (quiero profundizar en .NET/datos/integraciones) · ownership full-stack real · remoto · el stack encaja. Nunca "estoy harto de…" ni nada negativo de SEUS.
Las de escribir SQL están en SQL y Guion completo. Estas son para decirlas en < 1 min, con estructura.
Estructura: 3 diferencias que importan → dónde aparece en Angular → el riesgo y cómo lo manejas.
"Una promesa es un valor único, eager —corre al crearse— y no cancelable. Un Observable es un flujo perezoso: no hace nada hasta que te suscribes, puede emitir muchos valores en el tiempo, y lo cancelas con unsubscribe. En Angular importa porque HttpClient devuelve Observables: desuscribirte cancela la petición de verdad. Y tengo operadores —switchMap, debounceTime— para coordinar eventos. El riesgo es olvidar desuscribirte; lo resuelvo con el pipe async o takeUntilDestroyed."
Si preguntan por los 4 de aplanado: switchMap cancela el anterior (búsqueda) · mergeMap paraleliza · concatMap encola en orden (guardados) · exhaustMap ignora los nuevos mientras trabaja (botón confirmar, anti doble-submit).
Dibujo: shell (host) → lee federation.manifest.json → genera import map → carga el remoteEntry de cada microfrontend bajo demanda → monta su ruta. Deps compartidas (Angular, RxJS, Material) resueltas una vez.
Aporta: deploy independiente por microfrontend · integración en runtime · deps compartidas · compatible con esbuild (webpack MF ya no).
Cuesta (en 3 cubos): (1) version skew → dos instancias de Angular, NG0203, se mitiga con shared: { singleton: true, strictVersion: true } · (2) comunicación/estado entre microfrontends: sin solución nativa · (3) tooling: routing, dev local con todos los remotos, debugging opaco, testing cruzando la frontera, SSR más débil.
Recalca: nombrar los dolores concretos es lo que prueba que lo usaste de verdad.
Estructura: lo que SÍ traslada → lo que NO equivale (aquí ganas puntos) → una pregunta de arquitectura.
"Lo que traigo es el modelo serverless: funciones stateless, cold starts, idempotencia, límites de ejecución. A Azure traslada casi directo el enfoque event-driven, Blob ≈ S3, y App Insights me daría más que CloudWatch. Lo que no equivale y tendría que aprender: Azure Functions usa bindings declarativos en vez de llamar al SDK a mano; Durable Functions no es Step Functions —es orquestación en código C#, no una máquina de estados JSON—; Cosmos no es DynamoDB en particionado ni en costeo; y la identidad pasa de IAM roles a Managed Identity + RBAC. Además, para una API .NET grande probablemente no sería Functions sino App Service o Container Apps — ¿qué usan ustedes?"
Tu anécdota (rellena con un test real): componente [nombre] → lo testeaba por [regresión / feature] → comprobaba [render condicional / emisión de @Output / que llamó al servicio con los args correctos] → setup con TestBed + mock con jasmine.createSpyObj → async con [fakeAsync+tick / HttpTestingController].
Karma vs Jest: Karma corre en navegador real (Chrome headless), Jest en jsdom (Node, más rápido). Tienen los dos: Jest para el ciclo rápido de TDD/CI, Karma para fidelidad de navegador real. "¿Los unificarías?" → depende del costo de mantener dos configs vs. lo que se perdería; postura defendible: todo lo unitario a Jest, Karma/Playwright solo para lo que exige navegador.
Pregunta su rango primero, y el esquema (nómina / honorarios / contractor) y moneda. Si insisten en que des número: 65,000–85,000 MXN mensuales, ajustable según esquema y prestaciones. El piso que dices (65k) es tu objetivo, nunca menciones tu mínimo. Detalle completo en Negociación.
"Disponible de inmediato" ya lo tienen en el CV. Si sigues en SEUS, aclara tu tiempo de aviso real ([RELLENA: 2 semanas / 1 mes]).
"Más profundidad full-stack —sobre todo del lado de .NET, datos y arquitectura de integraciones—, y con el tiempo, si el equipo lo necesita, tomar decisiones técnicas de más alcance o mentorear." Concreto y alineado a un equipo chico. Nada de "tener tu puesto".
Al tech lead:
Al hiring manager / producto:
La última resuelve el hueco del toolchain y del formato de la prueba — si no te lo han dicho, te lo dicen ahí.
Sobre los datos de banda salarial: Levels.fyi y similares casi no tienen data de e-commerce pequeño en México, y NEWCO es una LLC de EE. UU. contratando remoto en MX — el esquema (nómina local, honorarios, contractor en USD) cambia el número por completo. Lo de abajo es estimación de mercado, no un dato duro. Confírmalo preguntando su rango.
| Escenario | Rango mensual aprox. (MXN) |
|---|---|
| Nómina local, empresa no-tech pequeña | 40,000 – 65,000 |
| Honorarios / contractor, pago influido por USD | 55,000 – 90,000+ |
| Tu objetivo declarado | hasta 80,000 |
Dado que es una LLC de EE. UU. y el puesto es "otro tipo de contrato" (probablemente honorarios/contractor), es razonable anclar en la parte alta.
Cuando pregunten "¿cuál es tu expectativa?"
"Antes de dar un número me ayudaría saber el esquema: ¿es nómina, honorarios, o contractor? ¿el pago es en pesos o en dólares? ¿qué prestaciones incluye? Con eso puedo darte una cifra que tenga sentido. ¿Cuál es el rango que tienen contemplado para la posición?"
Si insisten en que des número primero
"Con base en mi experiencia —5 años full-stack, Angular a nivel senior, microfrontends, y la parte de .NET y SQL Server que es justo lo que buscan— mi expectativa está en el rango de 65,000 a 85,000 MXN mensuales, ajustable según esquema y prestaciones."
El piso que dices (65k) es tu objetivo, no tu mínimo. Nunca ancles en 35k.
Contraoferta si ofrecen bajo
"Agradezco la oferta y el puesto me interesa mucho. El número está por debajo de lo que esperaba para este alcance —soy full-stack con ownership de la capa de integración y Angular senior—. ¿Hay margen para llegar a [tu cifra]? Si el fijo tiene tope, ¿podemos ver bono por desempeño, revisión a los 6 meses, o días de vacaciones?"
Formato confirmado: fichero .cs suelto, 3 piezas (SQL + método + endpoint) + Angular hablado. No compila nada. El guion completo está en la pestaña La prueba; este plan es el calendario para dominarlo.
Bucle de práctica (Escritorio): doble clic en reset-prueba.bat → deja prueba.cs en blanco y lo abre. Tecleas la solución narrando, cronómetro corriendo. Corres verificar.sql en SSMS/Azure Data Studio contra PruebaCocoMarch para comprobar. Repites. reset-bd.bat si ensayaste INSERTs.
reset-prueba.bat, y teclea el esqueleto + la consulta mirando la biblia. Corre verificar.sql.verificar.sql → confirma las 22 líneas de enero, la frontera (pedido 16 entra, 17 no), la página 2 sin repetir.BETWEEN, por qué sin CAST (SARGable), por qué el ORDER BY termina en columna única, OFFSET vs keyset.const string → método (QueryMultiple, leer, devolver) → endpoint (validar, sanear, delegar). Todo en prueba.cs.GROUP BY, uno con objeto de query). Que el read-back y el mapeo salgan solos.READ COMMITTED / el matiz de los dos SELECT / SNAPSHOT / escritura multi-tabla / SERIALIZABLE.LIKE, un INSERT con transacción).reset-prueba.bat deja prueba.cs listo. Ten a mano SSMS/Azure Data Studio conectado (para tu tranquilidad, aunque no lo uses en vivo).prueba.cs (ya reiniciado). Verifica una vez más que Claude Code y Copilot están desactivados.| Componente | Estado |
|---|---|
| .NET SDK | ✅ 9.0.317 |
| SQL Server | ✅ 2022 Express, instancia localhost\SQLEXPRESS, corriendo |
| Clientes SQL | ✅ SSMS 19 · Azure Data Studio · sqlcmd 16 |
| BD de práctica | ✅ PruebaCocoMarch — 3 tablas, poblada (10 productos, 20 pedidos, 37 líneas) |
| VS Code + C# Dev Kit | ✅ instalados (IntelliSense: abrir carpeta, no fichero suelto — poco relevante aquí porque no compila) |
| Scripts | ✅ datos-prueba.sql · verificar.sql · prueba.cs · reset-prueba.bat · reset-bd.bat |
Connection string para ensayar: Server=localhost\SQLEXPRESS;Database=PruebaCocoMarch;Trusted_Connection=True;TrustServerCertificate=True
Ownership de feature vertical = como ya trabajo · stack 90% igual al mío · producto real, equipo chico, impacto directo · quiero profundizar en .NET + integraciones · remoto.
>= @From AND < @ToExclusive — NUNCA BETWEEN. Nada de funciones sobre la columna.ORDER BY determinista + columna única de desempate, luego OFFSET/FETCH. Keyset para páginas profundas.(OrderDate DESC, OrderId DESC).SET XACT_ABORT ON + TRY/CATCH + IF XACT_STATE() <> 0 ROLLBACK + THROW.UPDLOCK · deadlock → orden consistente + retry 1205 · concurrencia optimista con rowversion.conn.QueryAsync<T>(sql, params), transacción con BeginTransactionAsync y pasar tx.Observable: flujo (0..∞) · lazy · cancelable · operadores · cold reejecuta. Promise: uno · eager · no cancelable · .then.
4 aplanados: switch=cancela (búsqueda) · merge=paralelo · concat=cola ordenada · exhaust=ignora mientras trabaja (submit).
Module Federation sobre ESM + import maps para esbuild. Aporta: deploy independiente, integración runtime, deps compartidas. Cuesta: version skew (dos Angular → NG0203), comunicación entre MFEs sin solución nativa, routing, dev local, debugging opaco.
Step Functions ≠ Durable Functions (JSON vs código C#) · DynamoDB ≠ Cosmos (particionado, RU) · IAM roles ≠ Managed Identity + RBAC · API Gateway ≠ APIM · Lambda→Functions con bindings declarativos · una API .NET grande probablemente va en App Service / Container Apps, no Functions.
"Básico de lectura, consumo docs técnicos en inglés, lo trabajo activamente a diario, no conversacional aún. Para SP-API/Stripe/Angular me alcanza."
Un empleador = crecí dentro sin dejar de hacerlo (rol inicial → mayor contribuidor + dueño de módulos). El stack cambió constantemente aunque la empresa no. Titulación: [tu frase real], factual y hacia adelante.
Pregunta su rango + esquema (nómina/honorarios) + moneda PRIMERO. Si insisten: 65,000–85,000 MXN. Nunca menciones 35k.
SQL Server + .NET SDK + Angular CLI ~20 montados y probados · solución scratch pre-creada · BD de práctica sembrada · screen share testeado · correo previo confirmando el entorno.