Biblia de entrevistas — Full Stack Angular + .NET

Candidato: Roy Airy García Beltrán · ~5 años en Seus Web (retail farmacéutico) · Remoto · Español (inglés básico)
v1 · 2026-09-08 Basada en 30+ vacantes postuladas Angular 16–22 · .NET Core · SQL Server Hablado + escrito + comandos

Cómo usar esta biblia

Cada empresa a la que postulaste pide alguna combinación de Angular + .NET + SQL Server + Git/Azure DevOps. Esto cubre lo que preguntan, en los dos formatos: lo que dices (entrevista hablada) y lo que escribes (prueba en vivo o take-home).

Los 4 tipos de entrevista que te van a hacer

TipoQué evalúanPestañas clave
Screening con reclutadorEncaje de perfil, salario, disponibilidad, inglésGuion hablado · Inglés
Técnica hablada (con lead o senior)Profundidad real: por qué, trade-offs, experiencia concretaAngular hablado · .NET hablado · Arquitectura
Prueba en vivo / pair (pantalla compartida)Que resuelvas tecleando y pensando en voz altaAngular escrito · .NET escrito · SQL · Comandos
Take-home (te mandan un repo o enunciado)Estructura, pruebas, README, git limpio.NET escrito · Angular escrito · Comandos · Arquitectura

Cómo es de verdad un proceso de big tech

De un relato de primera mano del proceso de OpenAI — y aplica igual a Meta, Uber, SpaceX y Netflix, porque todas usan la misma plataforma (CodeSignal).

  • Tres rondas técnicas, el mismo día: (1) algoritmo/lógica, (2) ejercicio de frontend —construir algo de cero—, (3) arquitectura: cómo estructuras los componentes, cómo manejas los datos, hasta la base de datos, y luego cómo lo escalas.
  • No es LeetCode duro. Es lógica aplicada. Todo en vivo, con alguien mirando.
  • Las preguntas no tienen final: resuelves, y te dicen "¿y si ahora cambiamos esto?". Se complica a propósito, sin techo. No están buscando dónde te caes — quieren ver cómo razonas cuando te topas con algo nuevo. Que no te resuelvas el último nivel no es reprobar.
  • El ida y vuelta con el entrevistador es parte de la nota. Pensar en voz alta y preguntar da tanta señal como el código.
  • Buscar en Google suele estar permitido (usar IA no). Textual del entrevistador: "googleamos todo el día; lo que me importa es que conozcas los conceptos".
  • Puedes practicar con sus pruebas reales: CodeSignal publica Company challenges gratuitos de SpaceX, Uber y otras.

No sobre-prepares el caso complejo y descuides el simple.

El patrón que más se repite en estas vacantes no es un JOIN de cinco tablas: es una consulta sencilla bien argumentada (a veces sobre una sola tabla) + el método de servicio que la ejecuta + el endpoint que la expone, muchas veces devolviendo un envelope Result<T> (processCorrect, typeError, data). Y la parte de Angular suele ser teclear un componente completo, no solo hablarlo. Todo eso está resuelto en las pestañas escritas.

Esta biblia se lee. Para practicar, está El Dojo Técnico — genera ejercicios de SQL, Angular, .NET, lógica y big tech, te deja escribir la respuesta, la evalúa contra una rúbrica y después te muestra cómo se hacía y por qué.

Semáforo de fit contra tu perfil

Requisito típicoCómo lo manejas
Angular 16–20, RxJS, Material✅ Fuerte5 años, migración a microfrontends, mayor contribuidor del repo
.NET Core Web API, C#, REST✅ SólidoAPIs serverless en AWS Lambda, servicio creado de cero, patrón 4 capas
SQL Server, consultas, JOINs🟡 MedioLo usas vía ORM/servicios; practica consultas a mano — el punto débil del CV
Azure / EF Core🟡 ParcialTu nube es AWS; EF lo conoces pero usas más ADO/Dapper. Di la verdad y traza el paralelo
Inglés avanzado / C1❌ BásicoFiltro duro. Si es "deseable", auto-intro de 40 s preparada (pestaña Inglés)
Título expedido❌ Pasante"Pasante de ITICS, ITTLA. La titulación está en trámite." No mentir
Liderazgo / Tech Lead🟡 De factoLideraste la migración y estandarizaste errores sin el título de lead — hay historia STAR

Los 5 focos de estudio, en orden

  1. SQL a mano — es lo que menos tocas y lo que más cae en prueba en vivo. Pestaña SQL entera.
  2. Componente Angular de cero — buscador/tabla/formulario con RxJS correcto (switchMap, debounceTime). Pestaña Angular escrito.
  3. Endpoint .NET + servicio Dapper + Result<T> — el trío que piden una y otra vez. Pestaña .NET escrito.
  4. Las 3 reglas al hablar — cero "creo/siento", cada respuesta con contexto, término exacto. Pestaña Cómo hablar.
  5. Guion de "cuéntame de ti" + salario + inglés — se repite en todas. Pestaña Guion.

Cómo hablar en la entrevista

Feedback directo de tu ex jefe. Esto pesa tanto como el código: un senior se distingue por cómo explica, no solo por resolver.

Regla 1 — Seguridad

Cero "creo", "siento", "pienso", "me parece", "más o menos", "no sé si...". Delatan falta de experiencia. Si no sabes algo, dilo con firmeza: "Eso no lo he usado en producción. Lo que sí conozco es X, y el concepto equivalente sería Y." Decir "no sé" con seguridad vale más que inventar con duda.

Regla 2 — Contexto

Cada respuesta va situada: dónde lo usaste, qué problema resolvía, qué decidiste. No "sí, sé usar interceptores" sino "En Seus estandaricé el manejo de errores del front con un interceptor HTTP: antes cada módulo capturaba a su manera; lo unifiqué en un flujo único basado en promesas con un guard de sesión y roles."

Regla 3 — Lenguaje técnico

El término exacto, no el rodeo. "Change detection", no "cuando Angular revisa los cambios". "Idempotente", "covering index", "optimistic concurrency", "hot vs cold observable", "tree-shaking". Un término preciso = años de experiencia.

El molde de respuesta

Afirmación directa → caso Seus → término exacto → cierre/matiz.
FlojoCon molde
"Sí, más o menos sé de switchMap, creo que sirve para cancelar cosas." "switchMap cancela el observable interno anterior cuando llega uno nuevo (afirmación). Lo usé en el buscador de productos del POS de Seus (contexto): sin él tenías una race condition — una respuesta lenta pisaba a una rápida (término). Para peticiones que no se deben perder, como guardar, ahí sí uso concatMap o exhaustMap (matiz)."

Errores del candidato promedio (evítalos)

Cuando no sabes algo (plantillas)

"No lo he usado directamente. Por lo que entiendo del concepto es [X]. Donde sí he resuelto algo parecido fue [caso]. ¿Es central para el puesto? Lo puedo tener andando rápido."
"En mi stack el equivalente es [Y]. Los principios se trasladan: [1-2 principios]. La sintaxis la aprendo en días."
"Ahí me estás llevando más allá de mi experiencia real. Prefiero decírtelo a inventarte una respuesta."

Angular — preguntas habladas

Banco de preguntas por tema, con respuesta modelo anclada a tu experiencia en Seus. Aparecen en Hitss, Pyramid, INTERWARE, PROEXCELENCIA, Coppel, BC Tecnología, TESYS21 y prácticamente cualquier vacante de Angular del mercado.

RxJS y asincronía

¿Diferencia entre Observable y Promesa? ¿Cuándo usas cada uno?

La Promesa emite un solo valor, es eager (arranca al crearse) y no se puede cancelar. El Observable emite 0..N valores en el tiempo, es lazy (no hace nada hasta que te suscribes) y cancelable vía unsubscribe.

Contexto Seus: "El HTTP de Angular devuelve Observable, así que en la app casi todo es RxJS. Pero para un flujo transaccional de un solo disparo —como MasterGuardarPedido, guardar un pedido— lo modelé con promesas: no hay stream, es una operación puntual, y el patrón de 4 capas del proyecto se apoya en promesas para la orquestación."

Si profundizan: cold vs hot (HTTP es cold, un Subject/fromEvent es hot); los 4 flatteners (abajo); toSignal/toObservable para el interop con signals; fugas si no te desuscribes.

Explica switchMap, mergeMap, concatMap, exhaustMap.

OperadorQué hace con el internoCaso de uso
switchMapCancela el anterior si llega uno nuevoBuscador / autocomplete / navegación — solo importa el último
mergeMapCorre todos en paraleloPeticiones independientes, sin orden — p. ej. subir varios archivos
concatMapUno tras otro, en orden, encolaOperaciones que deben ir en secuencia — guardados dependientes
exhaustMapIgnora nuevos mientras uno esté activoBotón "Guardar" / login — evita el doble submit
"El error clásico en un buscador es usar mergeMap: te llegan resultados desordenados. Para búsqueda es switchMap. Para un submit, exhaustMap."

¿Cómo evitas fugas de memoria con suscripciones?

  • takeUntilDestroyed() (Angular 16+) — se engancha al DestroyRef, es lo más limpio hoy.
  • AsyncPipe en el template — se desuscribe solo. La regla: si puedes resolverlo con | async, hazlo.
  • toSignal() — maneja la desuscripción por ti.
  • El patrón viejo: takeUntil(this.destroy$) con un Subject en ngOnDestroy.

Contexto: "En el refactor de manejo de errores del front reemplacé suscripciones sueltas dispersas por un flujo único; ahí aproveché para cerrar fugas que venían de años."

Signals y change detection

¿Qué son los signals y qué cambian?

Un signal es un contenedor reactivo de un valor: count = signal(0), lees con count(), escribes con set/update. computed() deriva y memoiza; effect() reacciona a cambios para side-effects.

Lo que cambian: change detection granular. Angular sabe exactamente qué vista depende de qué signal y actualiza solo eso, sin recorrer el árbol. Es el camino a zoneless (sin Zone.js).

Matiz senior: "effect() suele ser un olor a código — si lo estás usando para setear otro estado, casi siempre quieres computed. effect es para logging, analytics, sincronizar con algo externo al framework."

¿Cómo funciona change detection y qué es OnPush?

Por defecto (Zone.js) Angular parcha las APIs async del browser; cuando algo asíncrono termina, corre change detection sobre todo el árbol comparando bindings.

ChangeDetectionStrategy.OnPush: ese componente solo se revisa si (1) cambia una @Input por referencia, (2) dispara un evento del propio componente, (3) un observable ligado con async emite, o (4) lo marcas a mano con markForCheck(). Menos revisiones = más rendimiento. Con signals, el componente se marca solo cuando cambia un signal que usa.

Arquitectura de la app

Cuéntame de la migración a microfrontends con Native Federation.

"El producto era un monolito Angular de 18+ módulos. Migré los de backoffice —compra directa, recepción electrónica, monitor de embarques, pedido manual— a Angular 20 con Native Federation: una arquitectura host + remotos, cada módulo desplegable por separado, con una librería Angular compartida para componentes y servicios comunes. Fui el mayor contribuidor de ese repo, 86 commits."

Qué aportó: despliegues independientes (un fix en compra directa no re-despliega todo), equipos que no se pisan, builds más chicos. Qué costó: versionado de la librería compartida, contrato entre host y remotos, duplicación de dependencias si no afinas los shared, y setup de routing/estado entre remotos. Native Federation vs Module Federation: NF es agnóstico del bundler (usa import maps + esbuild), pensado para el Angular moderno con Vite/esbuild.

¿Cuándo NO usarías microfrontends?

"Si el equipo es chico o el producto no tiene dominios que se puedan cortar limpio, es sobre-ingeniería: pagas toda la complejidad de coordinación sin el beneficio de la independencia. Un monorepo con librerías y lazy loading resuelve el 90% de los casos. Los microfrontends valen cuando tienes varios equipos que necesitan desplegar a ritmos distintos."

¿Cómo organizas los servicios y el consumo de APIs?

"Diseñé un patrón de integración en 4 capas para consumir los microservicios .NET (lealtad, cotizador de venta, clientes, facturación CFDI): Interfaz (contrato) → Service (HttpClient, mapeo) → Assembly (arma el request / desarma la respuesta) → Promise (orquesta y expone al componente). Aísla al componente del transporte y de la forma del backend."

Formularios, routing, DI

¿Reactive Forms o Template-driven? ¿Cómo haces un validador custom?

Reactive para cualquier cosa no trivial: el modelo vive en el TS, es testeable, tipado, y compones validadores. Un validador custom es una función (control: AbstractControl): ValidationErrors | null; si es asíncrono devuelve Observable<ValidationErrors|null>. Validación cruzada entre campos → validador a nivel del FormGroup.

export function rfcValido(): ValidatorFn {
  return (c: AbstractControl): ValidationErrors | null =>
    /^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/.test(c.value ?? '')
      ? null : { rfc: true };
}

¿Qué es la inyección de dependencias en Angular? ¿providedIn: 'root'?

Angular tiene un inyector jerárquico. @Injectable({ providedIn: 'root' }) registra el servicio en el inyector raíz como singleton y además es tree-shakable (si nadie lo inyecta, no entra al bundle). Puedes acotar el scope proveyéndolo a nivel de componente o ruta. inject() es la forma moderna, funciona fuera del constructor (en funciones, guards, computed).

InjectionToken para inyectar cosas que no son clases: config, un valor, una interfaz (como en el buscador sin HttpClient, pestaña Angular escrito).

Lazy loading y @defer.

Lazy loading de rutas: loadComponent / loadChildren — el código de esa ruta se baja cuando navegas. @defer (v17+) hace lazy a nivel de bloque del template, con triggers (on viewport, on idle, on interaction) y bloques @placeholder/@loading/@error. Sirve para bajar cosas pesadas below-the-fold sin partir en rutas.

Testing (frontend)

¿Cómo pruebas un servicio que hace HTTP? ¿Y un componente?

Servicio HTTP: HttpClientTestingModule + HttpTestingControllerexpectOne, flush, y verify() en el afterEach. Nada pega a la red.

Componente: TestBed, ComponentFixture, fixture.detectChanges(). Para código async, fakeAsync + tick() (control determinista del tiempo) o waitForAsync. Para interacción de Material, los component harnesses (MatButtonHarness, etc.) en vez de escarbar el DOM.

Contexto: "El proyecto usa Karma/Jasmine y también Jest. El último spec que escribí fue del MasterGuardarPedidoService, cubriendo las 3 ramas: éxito, error de negocio y error de red."

Cola larga — respuestas de 2 líneas

TypeScript: tipos vs interfaces (interface se puede reabrir/extender, type compone uniones), generics, unknown vs any, keyof, utility types (Partial, Pick, Omit, Record), strict mode.

Standalone: es el default desde v17+; se acabaron los NgModules. Importas lo que usa el componente en su imports.

Control flow: @if / @for / @switch reemplazan *ngIf/*ngFor; @for exige track.

Interceptores: funcionales (HttpInterceptorFn) desde v15; auth token, manejo de error centralizado, loading, retry.

Seguridad front: Angular escapa por defecto (previene XSS); DomSanitizer solo cuando de verdad hace falta; nunca bypassSecurityTrust* con input de usuario.

No usas NgRx: "El estado del proyecto se maneja con servicios + RxJS/signals. NgRx lo conozco de concepto (store, actions, reducers, effects, selectors) pero para el tamaño de nuestros módulos hubiera sido overhead."

Rendimiento: OnPush, trackBy/track, lazy loading, @defer, evitar funciones en el template, NgOptimizedImage, virtual scroll del CDK.

Angular — ejercicios escritos

Cada uno: enunciado, tiempo objetivo, pistas y solución comentada colapsadas, y qué decir en voz alta. Practica tecleando, no leyendo.

E1 · Buscador de productos sin HttpClient 25 min

Componente ProductSearchComponent: input de búsqueda, lista de resultados, estados de carga / vacío / error. La fuente de datos no debe ser HttpClient directamente. Debe hacer debounce y cancelar búsquedas viejas.

"Voy a depender de una abstracción, un ProductSearchService, e inyectarla con un token. Así el componente no conoce el transporte y es testeable sin HttpTestingController. Una implementación puede ser datos en memoria, otra fetch, otra HttpClient — el componente no cambia."
Pistas
  • InjectionToken<ProductSearchService> con el método search(term): Observable<Product[]>.
  • FormControl + valueChangesdebounceTime(300)distinctUntilChanged()switchMap.
  • catchError que devuelve of([]) — un error no debe completar el stream.
  • finalize para apagar el loading (también corre al cancelar).
  • Estado en signals: loading, error, results. Desuscripción con takeUntilDestroyed().
Solución
export interface Product { sku: string; nombre: string; precio: number; }
export interface ProductSearchService { search(term: string): Observable<Product[]>; }
export const PRODUCT_SEARCH = new InjectionToken<ProductSearchService>('PRODUCT_SEARCH');

@Injectable()
export class InMemoryProductSearch implements ProductSearchService {
  private readonly catalogo: Product[] = [
    { sku: 'VT-001', nombre: 'Colágeno Hidrolizado', precio: 599 },
    { sku: 'VT-002', nombre: 'Omega 3', precio: 449 },
  ];
  search(term: string): Observable<Product[]> {
    const q = term.trim().toLowerCase();
    const r = q ? this.catalogo.filter(p =>
      p.nombre.toLowerCase().includes(q) || p.sku.toLowerCase().includes(q)) : [];
    return of(r).pipe(delay(150));
  }
}

@Component({
  selector: 'app-product-search',
  standalone: true,
  imports: [ReactiveFormsModule, CurrencyPipe],
  template: `
    <input type="search" [formControl]="term" placeholder="Buscar producto o SKU…" />
    @if (loading()) { <p>Buscando…</p> }
    @else if (error()) { <p class="error">{{ error() }}</p> }
    @else if (term.value && results().length === 0) { <p>Sin resultados</p> }
    @else {
      <ul>@for (p of results(); track p.sku) {
        <li>{{ p.sku }} — {{ p.nombre }} ({{ p.precio | currency:'MXN' }})</li>
      }</ul>
    }`,
})
export class ProductSearchComponent {
  private readonly api = inject(PRODUCT_SEARCH);
  readonly term = new FormControl('', { nonNullable: true });
  readonly loading = signal(false);
  readonly error = signal<string | null>(null);
  readonly results = signal<Product[]>([]);

  constructor() {
    this.term.valueChanges.pipe(
      debounceTime(300),
      map(t => t.trim()),
      distinctUntilChanged(),
      tap(() => this.error.set(null)),
      switchMap(t => {
        if (!t) { this.results.set([]); return EMPTY; }
        this.loading.set(true);
        return this.api.search(t).pipe(
          catchError(() => { this.error.set('Error al buscar'); return of([] as Product[]); }),
          finalize(() => this.loading.set(false)),
        );
      }),
      takeUntilDestroyed(),
    ).subscribe(list => this.results.set(list));
  }
}

// Provider:  { provide: PRODUCT_SEARCH, useClass: InMemoryProductSearch }

E2 · Tabla paginada con ordenamiento 30 min

Lista de pedidos con paginación del lado servidor (page, pageSize), ordenar por columna, y mostrar el total. El servicio devuelve { data: T[]; totalCount: number }.

Pistas
  • Un signal query = signal({ page: 1, pageSize: 10, sortBy: 'fecha', sortDir: 'desc' }).
  • toObservable(query)switchMap al servicio; o rxResource({ request: () => query(), loader: ... }).
  • Total de páginas = Math.ceil(totalCount / pageSize).
  • Cambiar de orden resetea a página 1.
Solución (núcleo)
type Query = { page: number; pageSize: number; sortBy: string; sortDir: 'asc'|'desc' };

@Component({ /* ... */ })
export class OrdersTableComponent {
  private readonly api = inject(ORDERS_API);
  readonly query = signal<Query>({ page: 1, pageSize: 10, sortBy: 'fecha', sortDir: 'desc' });

  private readonly page$ = toObservable(this.query).pipe(
    switchMap(q => this.api.list(q).pipe(startWith(null))),  // null = loading
  );
  readonly result = toSignal(this.page$, { initialValue: null });

  readonly totalPages = computed(() => {
    const r = this.result(); const q = this.query();
    return r ? Math.ceil(r.totalCount / q.pageSize) : 0;
  });

  sortBy(col: string) {
    this.query.update(q => ({
      ...q, page: 1, sortBy: col,
      sortDir: q.sortBy === col && q.sortDir === 'asc' ? 'desc' : 'asc',
    }));
  }
  goTo(page: number) { this.query.update(q => ({ ...q, page })); }
}
"Paginación del lado servidor porque no quiero traer 50 mil filas al navegador. El estado de la consulta es un solo signal; cualquier cambio dispara una petición nueva con switchMap para cancelar la anterior. Cambiar el orden vuelve a página 1 porque la posición ya no tiene sentido."

E3 · Formulario reactivo con validación cruzada 20 min

Alta de producto: sku (requerido, patrón), precio (> 0), precioOferta (opcional, debe ser < precio). Deshabilitar submit si inválido, mostrar errores por campo.

Solución
const form = this.fb.nonNullable.group({
  sku: ['', [Validators.required, Validators.pattern(/^[A-Z]{2}-\d{3,}$/)]],
  precio: [0, [Validators.required, Validators.min(0.01)]],
  precioOferta: [null as number | null],
}, { validators: ofertaMenorQuePrecio });

function ofertaMenorQuePrecio(g: AbstractControl): ValidationErrors | null {
  const precio = g.get('precio')?.value;
  const oferta = g.get('precioOferta')?.value;
  return oferta != null && oferta >= precio ? { ofertaInvalida: true } : null;
}

Template: @if (form.controls.sku.touched && form.controls.sku.errors?.['required'])… y [disabled]="form.invalid" en el botón.

"La validación entre dos campos va en el FormGroup, no en el control — un control no ve a sus hermanos. Marco los errores solo cuando el campo fue tocado para no gritarle al usuario mientras escribe."

E4 · Interceptor HTTP: token + manejo de error + desempaquetar Result<T> 15 min

Solución
export const apiInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  const token = auth.token();
  const authReq = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req;

  return next(authReq).pipe(
    map(event => {
      if (event instanceof HttpResponse && event.body?.processCorrect === false) {
        throw new HttpErrorResponse({ error: event.body, status: 422, url: req.url });
      }
      return event;
    }),
    catchError((err: HttpErrorResponse) => {
      if (err.status === 401) auth.logout();
      const msg = err.error?.messageError ?? 'Error de red';
      inject(ToastService).error(msg);
      return throwError(() => err);
    }),
  );
};
"El envelope Result<T> lo desempaqueto aquí, en un solo lugar: si processCorrect es false lo convierto en error para que los componentes solo manejen el camino feliz. El 401 dispara logout global."

E5 · Guard de ruta por rol + test 15 min

Solución
export const rolGuard = (rol: string): CanActivateFn => () => {
  const auth = inject(AuthService);
  const router = inject(Router);
  return auth.tieneRol(rol) ? true : router.createUrlTree(['/sin-acceso']);
};

// spec
it('redirige si no tiene el rol', () => {
  TestBed.configureTestingModule({
    providers: [
      { provide: AuthService, useValue: { tieneRol: () => false } },
      { provide: Router, useValue: { createUrlTree: jasmine.createSpy().and.returnValue('URLTREE') } },
    ],
  });
  const result = TestBed.runInInjectionContext(() => rolGuard('admin')(null!, null!));
  expect(result).toBe('URLTREE' as any);
});

E6 · Spec de un servicio con HTTP (3 ramas) 20 min

Solución
describe('DisponibilidadService', () => {
  let svc: DisponibilidadService;
  let http: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [DisponibilidadService],
    });
    svc = TestBed.inject(DisponibilidadService);
    http = TestBed.inject(HttpTestingController);
  });
  afterEach(() => http.verify());

  it('devuelve la lista en éxito', () => {
    let out: any;
    svc.porSku('VT-001').subscribe(r => (out = r));
    http.expectOne('/api/productos/VT-001/disponibilidad')
        .flush([{ almacen: 'CDMX', cantidad: 40 }]);
    expect(out.length).toBe(1);
  });

  it('propaga el error de red', () => {
    let err: any;
    svc.porSku('X').subscribe({ error: e => (err = e) });
    http.expectOne(() => true).flush('boom', { status: 500, statusText: 'Server Error' });
    expect(err.status).toBe(500);
  });

  it('trata processCorrect=false como error', () => {
    let err: any;
    svc.porSku('X').subscribe({ error: e => (err = e) });
    http.expectOne(() => true).flush({ processCorrect: false, messageError: 'no existe' });
    expect(err.message).toContain('no existe');
  });
});

.NET / C# — preguntas habladas

Cae en Wayssen, PROEXCELENCIA, peoplemx, Softtek, Hitss, ProceTI, TESYS21, Endava. Softtek es mantenimiento legacy (ASP.NET, ADO.NET, ODBC) — no todo es .NET moderno.

Lenguaje y runtime

¿async/await? ¿Qué pasa por debajo?

El compilador convierte el método en una máquina de estados. await en una tarea no completada devuelve el control al llamador; cuando la tarea termina, la continuación se reanuda (por defecto en el contexto capturado). No crea hilos — libera el hilo mientras hay I/O pendiente. En una Web API eso significa más throughput: el hilo atiende otra request en vez de bloquearse esperando la BD.

Si profundizan: Task vs ValueTask; ConfigureAwait(false) en librerías (en ASP.NET Core ya no hay SynchronizationContext, así que importa menos); nunca .Result/.Wait() (deadlock); CancellationToken propagado hasta el SqlCommand.

IEnumerable<T> vs IQueryable<T>.

IEnumerable ejecuta en memoria (LINQ to Objects): si filtras después de traer, ya trajiste todo. IQueryable construye un árbol de expresión que el proveedor (EF) traduce a SQL — el filtro llega a la BD. El bug típico: hacer .ToList() antes de .Where() y traer la tabla entera.

¿record vs class? ¿Cuándo cuál?

record: igualdad por valor, inmutable por defecto, with para copias, ToString útil. Ideal para DTOs, requests/responses, value objects. class para entidades con identidad y comportamiento mutable. En APIs uso record para los DTO casi siempre.

Ejecución diferida en LINQ.

Los operadores como Where/Select no ejecutan hasta que enumeras (foreach, ToList, Count, First). Consecuencias: si iteras dos veces, la consulta corre dos veces; si la fuente cambió, el resultado cambia; con EF, materializa con ToListAsync antes de cerrar el contexto.

ASP.NET Core

Ciclos de vida de DI: Singleton, Scoped, Transient.

VidaInstanciaUso
SingletonUna para toda la appConfig, cache, cliente HTTP factory, cosas sin estado por request
ScopedUna por request HTTPDbContext, unit of work, repos, servicios de negocio
TransientUna por cada resoluciónServicios ligeros y sin estado
Trampa clásica (captive dependency): inyectar un Scoped dentro de un Singleton — el Scoped queda "capturado" y vive para siempre, y con DbContext eso truena porque no es thread-safe. El contenedor de .NET lo detecta en Development.

El pipeline de middleware. ¿Orden importa?

Cada middleware puede actuar antes y después del next() — es una cebolla. El orden es crítico: UseRoutingUseAuthenticationUseAuthorization → endpoints. UseExceptionHandler va primero para envolver todo. CORS antes de auth. Un middleware custom típico: logging de request, correlación de IDs, o el manejo global de excepciones que traduce a Result/ProblemDetails.

Minimal APIs vs Controllers.

Minimal APIs: menos ceremonia, rápidas de escribir, buenas para servicios chicos y microservicios. Controllers: convenciones, filtros por atributo, model binding rico, mejor para APIs grandes con muchos endpoints y lógica transversal. Ambas usan el mismo binding y DI. "En Seus las Lambdas eran controllers con arquitectura Controllers–Business–DTO."

Validación y model binding.

Binding: de la ruta, query, body (JSON), form, headers. Validación: DataAnnotations ([Required], [Range]) con [ApiController] que responde 400 automático, o FluentValidation para reglas complejas, o —en el estilo envelope— validar en el servicio y devolver Result.Fail("Validation", ...) sin lanzar excepción.

Datos

EF Core vs Dapper vs ADO.NET puro. ¿Cuándo cada uno?

Para quéCosto
EF CoreCRUD, cambio de esquema frecuente, productividad, trackingCurva de queries traducidas, sorpresas de rendimiento, N+1
DapperConsultas de lectura con SQL a mano, control total, alto rendimientoEscribes el SQL y el mapeo; sin tracking ni migraciones
ADO.NETLegacy, drivers ODBC, control absoluto, sin dependenciasMucho boilerplate: SqlConnection/Command/Reader a mano
"En Seus el acceso a datos iba por servicios .NET; para lectura con SQL afinado prefiero Dapper. Para un proyecto CRUD con esquema móvil, EF Core. Y sé leer y mantener ADO.NET puro con SqlDataReader, que es lo que pide el rol de mantenimiento de Softtek."

¿Qué es una transacción y cuándo la necesitas en el servicio?

Un grupo de operaciones que se aplica todo o nada (atomicidad). La necesitas cuando escribes en varias tablas y un fallo a mitad dejaría datos inconsistentes — p. ej. insertar un pedido y sus líneas. En Dapper: conn.BeginTransaction(), pasas la tx a cada Execute, Commit al final, Rollback en el catch. Niveles de aislamiento: READ COMMITTED (default de SQL Server) evita lecturas sucias; SERIALIZABLE para evitar phantom reads a costa de concurrencia.

Calidad

¿Cómo pruebas una Web API?

Unitarias del servicio con xUnit/NUnit + Moq (mockeas la interfaz del repo). Integración con WebApplicationFactory<Program> — levanta la API en memoria y le pegas con HttpClient; para la BD, Testcontainers (SQL Server real en Docker) o SQLite en memoria. "El equipo usa SonarQube en el pipeline de Azure DevOps para cobertura y code smells."

Cola larga

SOLID — una línea cada uno, ver pestaña Arquitectura.

IDisposable / using — liberar recursos no gestionados (conexiones, archivos); await using para IAsyncDisposable.

Excepciones — no para control de flujo; captura específica; no tragues (catch {}); log con contexto; excepción custom por dominio.

IOptions<T> — binding tipado de appsettings.json; IOptionsSnapshot para recarga por request.

Autenticación — JWT bearer, claims, [Authorize(Roles=...)] o policies; refresh tokens.

Primary constructors (C# 12) — class Svc(IDb db); menos boilerplate para inyección.

span/Memory — solo si preguntan por rendimiento/alocación; probablemente fuera de nivel.

.NET / C# — ejercicios escritos

N1 · Endpoint + servicio Dapper + Result<T> 25 min

Dado un SKU, devolver en qué almacenes hay disponibilidad (tabla única StockPorAlmacen(Sku, Almacen, Cantidad)). El endpoint responde siempre 200 con el envelope.

"Primero aclaro: ¿'disponible' es Cantidad > 0 o hay columna de reservado? ¿typeError es string o enum? Read-back de las firmas antes de teclear."
⚠️ El envelope es UNA convención, no el estándar. Lo usan equipos como el de SeusWeb, pero la mayoría de las vacantes hacen REST puro: status codes reales (400 / 404 / 200) y errores en ProblemDetails (RFC 7807), con el 500 generado por UseExceptionHandler y no por un catch. Practica las dos y pregunta cuál usan antes de teclear. La versión REST está resuelta en el ejercicio «El mismo endpoint, pero en REST puro» del Dojo. El matiz que preguntan ahí: "existe pero sin stock" es 200 con lista vacía, no 404.
Solución completa
// ---- Envelope ----
public class Result<T>
{
    public bool   ProcessCorrect { get; set; }
    public string TypeError      { get; set; } = "";
    public string MessageError   { get; set; } = "";
    public T?     Data           { get; set; }
    public static Result<T> Ok(T data) => new() { ProcessCorrect = true, Data = data };
    public static Result<T> Fail(string type, string msg) =>
        new() { ProcessCorrect = false, TypeError = type, MessageError = msg };
}

// ---- DTO ----
public sealed record DisponibilidadAlmacen(string Almacen, int Cantidad);

// ---- Servicio ----
public interface IInventarioService
{
    Task<Result<IReadOnlyList<DisponibilidadAlmacen>>> PorSkuAsync(string sku, CancellationToken ct = default);
}

public sealed class InventarioService(IDbConnection db, ILogger<InventarioService> log) : IInventarioService
{
    private const string Sql = """
        SELECT   s.Almacen, s.Cantidad
        FROM     dbo.StockPorAlmacen AS s
        WHERE    s.Sku = @Sku AND s.Cantidad > 0
        ORDER BY s.Cantidad DESC;
        """;

    public async Task<Result<IReadOnlyList<DisponibilidadAlmacen>>> PorSkuAsync(
        string sku, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(sku))
            return Result<IReadOnlyList<DisponibilidadAlmacen>>.Fail("Validation", "El SKU es obligatorio.");
        try
        {
            var cmd  = new CommandDefinition(Sql, new { Sku = sku }, cancellationToken: ct);
            var rows = (await db.QueryAsync<DisponibilidadAlmacen>(cmd)).AsList();
            return rows.Count == 0
                ? Result<IReadOnlyList<DisponibilidadAlmacen>>.Fail("NotFound", $"Sin disponibilidad para {sku}.")
                : Result<IReadOnlyList<DisponibilidadAlmacen>>.Ok(rows);
        }
        catch (SqlException ex)
        {
            log.LogError(ex, "Error consultando disponibilidad de {Sku}", sku);
            return Result<IReadOnlyList<DisponibilidadAlmacen>>.Fail("Exception", "Error al consultar la disponibilidad.");
        }
    }
}

// ---- Endpoint (Minimal API) ----
app.MapGet("/api/productos/{sku}/disponibilidad",
    async (string sku, IInventarioService svc, CancellationToken ct) =>
        Results.Ok(await svc.PorSkuAsync(sku, ct)));

// ---- o Controller ----
[ApiController, Route("api/productos")]
public class ProductosController(IInventarioService svc) : ControllerBase
{
    [HttpGet("{sku}/disponibilidad")]
    public async Task<IActionResult> Disponibilidad(string sku, CancellationToken ct)
        => Ok(await svc.PorSkuAsync(sku, ct));
}

N2 · Insertar pedido + líneas en una transacción 20 min

Solución
public async Task<Result<int>> CrearPedidoAsync(NuevoPedido input, CancellationToken ct)
{
    if (input.Lineas is null or { Count: 0 })
        return Result<int>.Fail("Validation", "El pedido necesita al menos una línea.");

    using var conn = new SqlConnection(_cs);
    await conn.OpenAsync(ct);
    using var tx = conn.BeginTransaction();
    try
    {
        var pedidoId = await conn.ExecuteScalarAsync<int>(new CommandDefinition(
            "INSERT INTO dbo.[Order](Fecha, ClienteId) OUTPUT INSERTED.OrderId VALUES(SYSUTCDATETIME(), @ClienteId);",
            new { input.ClienteId }, tx, cancellationToken: ct));

        foreach (var l in input.Lineas)
            await conn.ExecuteAsync(new CommandDefinition(
                "INSERT INTO dbo.OrderLine(OrderId, ProductId, Cantidad, Precio) VALUES(@pedidoId, @ProductId, @Cantidad, @Precio);",
                new { pedidoId, l.ProductId, l.Cantidad, l.Precio }, tx, cancellationToken: ct));

        tx.Commit();
        return Result<int>.Ok(pedidoId);
    }
    catch (Exception ex)
    {
        tx.Rollback();
        _log.LogError(ex, "Rollback creando pedido");
        return Result<int>.Fail("Exception", "No se pudo crear el pedido.");
    }
}
"Atomicidad: o entra el pedido con todas sus líneas o no entra nada. Un fallo en la línea 3 no debe dejar un pedido huérfano. OUTPUT INSERTED.OrderId me da el id sin un segundo round-trip. Batching con una tabla-valued parameter si son muchas líneas."

N3 · Paginación server-side con COUNT 20 min

Solución
public sealed record PageRequest(int Page = 1, int PageSize = 20, string? Search = null);
public sealed record PagedResponse<T>(IReadOnlyList<T> Data, long TotalCount, int Page, int TotalPages);

private const string Sql = """
    SELECT p.Sku, p.Nombre, p.Precio
    FROM   dbo.Product p
    WHERE  (@Search IS NULL OR p.Nombre LIKE '%' + @Search + '%')
    ORDER BY p.Nombre
    OFFSET (@Page - 1) * @PageSize ROWS FETCH NEXT @PageSize ROWS ONLY;

    SELECT COUNT(*)
    FROM   dbo.Product p
    WHERE  (@Search IS NULL OR p.Nombre LIKE '%' + @Search + '%');
    """;

public async Task<PagedResponse<ProductDto>> ListAsync(PageRequest req, CancellationToken ct)
{
    using var multi = await _db.QueryMultipleAsync(new CommandDefinition(Sql, req, cancellationToken: ct));
    var data  = (await multi.ReadAsync<ProductDto>()).AsList();
    var total = await multi.ReadSingleAsync<long>();
    var pages = (int)Math.Ceiling(total / (double)req.PageSize);
    return new(data, total, req.Page, pages);
}
Ejes que pueden cambiar: base 0 vs 1 de la página (cambia el OFFSET); TotalCount int vs long; OFFSET/FETCH vs keyset para tablas enormes; el LIKE '%x%' no usa índice — si lo mencionan, full-text search.

N4 · Manejo global de excepciones → envelope 15 min

Solución
public sealed class ExceptionToResultMiddleware(RequestDelegate next, ILogger<ExceptionToResultMiddleware> log)
{
    public async Task Invoke(HttpContext ctx)
    {
        try { await next(ctx); }
        catch (Exception ex)
        {
            log.LogError(ex, "Excepción no controlada en {Path}", ctx.Request.Path);
            ctx.Response.StatusCode = StatusCodes.Status200OK;   // estilo envelope
            ctx.Response.ContentType = "application/json";
            await ctx.Response.WriteAsJsonAsync(new
            {
                processCorrect = false,
                typeError = "Exception",
                messageError = "Ocurrió un error inesperado.",
            });
        }
    }
}
// app.UseMiddleware<ExceptionToResultMiddleware>();  // lo más arriba posible
"Si el equipo usa REST puro, esto mismo pero con ProblemDetails y el status real (500). La idea es la misma: un solo lugar, nunca filtrar ex.Message ni el stack al cliente."

N5 · Un algoritmo (por si cae) 15 min

"Dada una lista de ventas (sku, monto), devuelve el top 3 de SKUs por monto total."

Solución
public static IReadOnlyList<string> Top3PorMonto(IEnumerable<(string Sku, decimal Monto)> ventas) =>
    ventas.GroupBy(v => v.Sku)
          .Select(g => (Sku: g.Key, Total: g.Sum(x => x.Monto)))
          .OrderByDescending(x => x.Total)
          .Take(3)
          .Select(x => x.Sku)
          .ToList();
"GroupBy + Sum es O(n). Si fuera streaming de millones y solo quiero top 3, un diccionario acumulador + un heap de tamaño 3 → O(n log 3). Para 3 elementos no vale la pena; lo menciono como el paso siguiente si el volumen lo pide."

SQL Server — el punto a reforzar

Tu CV no menciona SQL a fondo y es lo que más cae en prueba en vivo. Objetivo: escribir una consulta con JOIN + filtro + orden + paginación en < 4 min sin titubear, narrando.

Hablado

Tipos de JOIN.

INNER (solo coincidencias), LEFT (todas las de la izquierda + null si no hay match — útil si "el producto pudo ser borrado"), RIGHT (raro, se reescribe como LEFT), FULL (ambos lados), CROSS (producto cartesiano). "Si un pedido puede tener un producto que ya no existe en catálogo, uso LEFT JOIN a Product y manejo el null; con INNER perdería esas filas silenciosamente."

Índices: clustered, nonclustered, covering.

Clustered: ordena físicamente la tabla, uno por tabla (normalmente la PK). Nonclustered: estructura aparte con punteros a la fila. Covering: un nonclustered que con INCLUDE lleva todas las columnas que la consulta necesita, así no va a la tabla ("key lookup"). El WHERE y el JOIN definen qué indexar; el ORDER BY se beneficia si el índice ya viene ordenado así.

"Para WHERE Sku = @Sku con SELECT Almacen, Cantidad pondría CREATE INDEX IX_Stock_Sku ON StockPorAlmacen(Sku) INCLUDE (Almacen, Cantidad) — seek + covering, sin lookups."

¿Cómo diagnosticas una consulta lenta?

SET STATISTICS IO, TIME ON para ver lecturas lógicas y tiempo; el plan de ejecución (estimado y real) para ver scans vs seeks, key lookups, spills, y estimaciones de filas erradas (estadísticas viejas → UPDATE STATISTICS). Señales: table scan donde debería haber seek, warnings de conversión implícita (un parámetro con tipo distinto a la columna mata el índice).

Rango de fechas: ¿por qué no BETWEEN?

BETWEEN '2026-01-01' AND '2026-01-31' se pierde todo lo del 31 después de medianoche si la columna es datetime. Usa semiabierto: Fecha >= @desde AND Fecha < @hasta donde @hasta es el primer día del mes siguiente. Funciona igual con date, datetime o datetime2.

Paginación: OFFSET/FETCH vs keyset.

OFFSET n ROWS FETCH NEXT m es simple pero degrada en páginas altas (SQL Server igual lee y descarta las n filas). Keyset (seek method): WHERE (Fecha, Id) < (@ultimaFecha, @ultimoId) ORDER BY Fecha DESC, Id DESC FETCH NEXT m — usa el índice, constante en cualquier página, pero no permite "ir a la página 500" directo. Para infinite scroll, keyset; para un grid con números de página, OFFSET.

Niveles de aislamiento / concurrencia.

READ COMMITTED (default): no lees cambios no confirmados. REPEATABLE READ: las filas que leíste no cambian en tu transacción. SERIALIZABLE: ni aparecen filas nuevas (no phantoms) — máximo aislamiento, mínima concurrencia. Para el "lost update" (dos usuarios editan lo mismo): optimistic concurrency con una columna rowversion/timestamp y WHERE RowVersion = @original en el UPDATE; si afecta 0 filas, alguien te ganó.

N+1 en SQL.

Traer una lista y luego, por cada elemento, otra consulta. Se resuelve con un JOIN o un IN con todos los ids de golpe. En EF: Include o proyección con Select.

Stored procedures: ¿sí o no?

"Depende del equipo. Ventajas: plan cacheado, permisos granulares, lógica cerca del dato. Desventajas: lógica de negocio partida entre app y BD, versionado y testing más difíciles, se acopla al motor. En Seus la lógica vivía en los servicios .NET con SQL parametrizado; sé escribir y mantener SPs si el proyecto ya va por ahí."
Ojo: varias de estas pruebas piden explícitamente sin stored procedures ni EF — SQL a mano.

Escrito — ejercicios

S1 · Disponibilidad por SKU (tabla única) 4 min

SELECT   s.Almacen, s.Cantidad
FROM     dbo.StockPorAlmacen AS s
WHERE    s.Sku = @Sku
  AND    s.Cantidad > 0
ORDER BY s.Cantidad DESC;

Variantes: solo ubicaciones → SELECT DISTINCT s.Almacen. Total consolidado → SELECT SUM(s.Cantidad) ... (confirma primero). Varios SKUs → WHERE s.Sku IN @Skus.

S2 · Líneas de pedido en un rango de fechas, paginado 6 min

SELECT   ol.OrderLineId, o.OrderId, o.OrderDate,
         p.Sku, p.Nombre, ol.Cantidad, ol.Precio,
         (ol.Cantidad * ol.Precio) AS Importe
FROM     dbo.OrderLine AS ol
JOIN     dbo.[Order]   AS o ON o.OrderId   = ol.OrderId
JOIN     dbo.Product   AS p ON p.ProductId = ol.ProductId
WHERE    o.OrderDate >= @Desde
  AND    o.OrderDate <  @Hasta
ORDER BY o.OrderDate DESC, ol.OrderLineId DESC
OFFSET   (@Page - 1) * @PageSize ROWS FETCH NEXT @PageSize ROWS ONLY;

SELECT COUNT(*)
FROM   dbo.OrderLine ol
JOIN   dbo.[Order] o ON o.OrderId = ol.OrderId
WHERE  o.OrderDate >= @Desde AND o.OrderDate < @Hasta;
"Rango semiabierto, no BETWEEN. Ordeno por fecha y desempato por el id de la línea para que la paginación sea determinista — sin el segundo criterio, dos filas con la misma fecha pueden salir en distinto orden entre páginas y ves duplicados o huecos."

S3 · Top 5 productos más vendidos por importe 5 min

SELECT   TOP (5)
         p.Sku, p.Nombre,
         SUM(ol.Cantidad)              AS Unidades,
         SUM(ol.Cantidad * ol.Precio)  AS Importe
FROM     dbo.OrderLine AS ol
JOIN     dbo.Product   AS p ON p.ProductId = ol.ProductId
GROUP BY p.Sku, p.Nombre
ORDER BY Importe DESC;

Con empate controlado: ROW_NUMBER() OVER (ORDER BY SUM(...) DESC) en una CTE y WHERE rn <= 5.

S4 · Productos sin ventas en el último mes 5 min

SELECT   p.Sku, p.Nombre
FROM     dbo.Product AS p
WHERE    NOT EXISTS (
           SELECT 1
           FROM   dbo.OrderLine ol
           JOIN   dbo.[Order]   o ON o.OrderId = ol.OrderId
           WHERE  ol.ProductId = p.ProductId
             AND  o.OrderDate >= @DesdeMes
         );
"NOT EXISTS en vez de LEFT JOIN ... WHERE x IS NULL: se lee mejor y el optimizador lo resuelve como anti-semi-join, corta en la primera coincidencia."

S5 · Búsqueda por texto en nombre o SKU 3 min

SELECT   p.Sku, p.Nombre, p.Precio
FROM     dbo.Product AS p
WHERE    p.Nombre LIKE '%' + @q + '%'
   OR    p.Sku    LIKE @q + '%'      -- prefijo en SKU sí usa índice
ORDER BY p.Nombre;

"LIKE '%x%' con comodín al inicio no usa índice — scan. Si el volumen lo exige, Full-Text Search con CONTAINS. En SKU el patrón de prefijo 'x%' sí aprovecha el índice."

S6 · Actualizar stock con control de concurrencia 4 min

UPDATE dbo.StockPorAlmacen
SET    Cantidad = Cantidad - @Cantidad
WHERE  Sku = @Sku AND Almacen = @Almacen
  AND  Cantidad >= @Cantidad;      -- no dejar negativo

IF @@ROWCOUNT = 0
    THROW 50001, 'Stock insuficiente o no encontrado', 1;
"El WHERE ... AND Cantidad >= @Cantidad hace el decremento atómico: dos descuentos concurrentes no pueden bajar de cero porque la condición se evalúa en el momento del update. Reviso @@ROWCOUNT para saber si aplicó."
Molde para escribir cualquier consulta, en voz alta: "Primero el SELECT con lo que piden … FROM la tabla base … JOIN las que necesito y por qué … WHERE con parámetros, rango semiabierto … ORDER BY con desempate … paginación al final."

Comandos — lo que se teclea

En prueba en vivo o pair te pueden pedir "crea el proyecto", "haz un branch", "corre los tests". Ten estos en los dedos.

Git

NecesitoComando
Rama nueva desde developgit switch -c feature/DEV-123-descripcion develop
Traer cambios de la base sin merge commitgit pull --rebase origin develop
Guardar sin commiteargit stash / git stash pop
Deshacer el último commit, conservar cambiosgit reset --soft HEAD~1
Descartar cambios locales de un archivogit restore ruta/archivo
Revertir un commit ya publicadogit revert <hash> (crea commit inverso, no reescribe historia)
Traer un commit puntual de otra ramagit cherry-pick <hash>
Encontrar qué commit rompió algogit bisect start / bad / good <hash>
Recuperar algo "perdido"git refloggit reset --hard <hash>
Limpiar commits antes de PRgit rebase -i develop (squash/fixup/reword)
Reset: --soft mueve HEAD (cambios quedan staged) · --mixed (default) los deja sin stage · --hard los borra. Merge vs rebase: merge preserva la historia real (merge commit); rebase la reescribe lineal — nunca rebase de una rama compartida ya publicada.

Angular CLI

ng new mi-app --standalone --routing --style=scss --ssr=false
ng generate component productos/product-search    # o: ng g c ...
ng g service core/product-search
ng g guard core/auth
ng g interceptor core/api
ng add @angular/material
ng serve -o                     # dev server + abre browser
ng build --configuration=production
ng test                         # Karma/Jasmine
ng test --watch=false --code-coverage
ng lint
ng update @angular/core @angular/cli

ng g c x --dry-run para ver qué crearía sin escribir. --inline-template --inline-style para un componente de un archivo (útil en prueba en vivo).

.NET CLI

dotnet new webapi -n Tienda.Api --use-controllers    # o minimal: sin la flag
dotnet new sln -n Tienda && dotnet sln add Tienda.Api
dotnet new xunit -n Tienda.Tests
dotnet add Tienda.Api package Dapper
dotnet add Tienda.Api package Microsoft.Data.SqlClient
dotnet add Tienda.Tests reference Tienda.Api
dotnet build
dotnet run --project Tienda.Api
dotnet test
dotnet watch --project Tienda.Api run     # hot reload
# EF (si el proyecto lo usa)
dotnet tool install --global dotnet-ef
dotnet ef migrations add Inicial
dotnet ef database update

npm / node

npm ci                 # instala exacto del lock (CI); npm install para cambiar deps
npm run start / build / test
npm outdated / npm update
npx <paquete>           # ejecutar sin instalar global
node --version / nvm use 20

SQL desde terminal

sqlcmd -S localhost\SQLEXPRESS -E -d Tienda -Q "SELECT COUNT(*) FROM Product;"
sqlcmd -S localhost\SQLEXPRESS -E -i script.sql       # ejecutar archivo
# -E = Windows auth; -U/-P para usuario/contraseña

Azure DevOps (az cli)

az repos pr create --source-branch feature/x --target-branch develop --title "..." --work-items 123
az pipelines run --branch feature/x
az boards work-item show --id 123

Arquitectura y diseño

SOLID — una frase y un ejemplo cada uno

PrincipioQué esEjemplo tuyo
Single ResponsibilityUna clase, una razón para cambiarEl interceptor hace manejo de error; el guard hace sesión/roles — no lo mismo
Open/ClosedAbierto a extensión, cerrado a modificaciónNueva implementación de ProductSearchService sin tocar el componente
LiskovUn subtipo debe poder sustituir al base sin romperCualquier IInventarioService cumple el contrato: mismos tipos de retorno y errores
Interface SegregationInterfaces chicas y específicas, no una giganteUn repo de lectura y uno de escritura separados en vez de IRepository con 20 métodos
Dependency InversionDepender de abstracciones, no de concretoEl componente inyecta PRODUCT_SEARCH (token), no HttpClient

El patrón de 4 capas (tu diseño en Seus)

Interfaz (contrato tipado) → Service (HttpClient, headers, mapeo transporte) → Assembly (arma request desde el modelo de dominio / desarma la respuesta) → Promise (orquesta varias llamadas, expone al componente una API limpia basada en promesas). "Lo diseñé para consumir 4 microservicios .NET distintos —lealtad, cotizador, clientes, CFDI— sin que cada componente conociera la forma de cada backend. Si un servicio cambia su contrato, el golpe queda contenido en Service+Assembly."

REST — diseño de API

Clean / capas en el backend

Controller (HTTP) → Application/Business (casos de uso, orquestación, validación) → Domain (entidades, reglas) → Infrastructure (repos, EF/Dapper, servicios externos). La dependencia apunta hacia adentro: el dominio no conoce a EF. En Seus: "Controllers–Business–DTO sobre AWS Lambda."

Preguntas de diseño que pueden caer

"Diséñame la API para el catálogo de productos con búsqueda y stock."

  • GET /v1/productos?search=&page=&pageSize=PagedResponse<ProductDto>
  • GET /v1/productos/{sku} → detalle
  • GET /v1/productos/{sku}/disponibilidad → por almacén
  • Capas: Controller → CatalogoService → IProductRepository (Dapper). DTOs de salida. Cache del catálogo (cambia poco) con invalidación por evento o TTL corto.
  • Búsqueda: si crece, mover a un índice (Full-Text o un motor aparte).

"¿Cómo manejas configuración y secretos?"

appsettings.json + appsettings.{Environment}.json, IOptions<T> tipado, User Secrets en local, y variables de entorno / un vault (AWS Secrets Manager, Azure Key Vault) en prod. Nunca secretos en el repo.

Fundamentos de programación

Nivel real del rol: no LeetCode hard. Estructuras básicas, Big-O, y resolver un problema tipo "agrupa / cuenta / encuentra" narrando el enfoque.

Complejidad

NotaciónEjemplo
O(1)Acceso a un Dictionary/Map por clave, push/pop
O(log n)Búsqueda binaria, operaciones en árbol balanceado
O(n)Recorrer una lista, un GroupBy
O(n log n)Ordenar (Array.Sort, .sort())
O(n²)Doble bucle anidado — bandera roja, casi siempre hay un hash que lo baja a O(n)

Estructuras y cuándo

F1 · Primera letra no repetida 8 min

public static char? PrimeraNoRepetida(string s)
{
    var conteo = new Dictionary<char, int>();
    foreach (var c in s) conteo[c] = conteo.GetValueOrDefault(c) + 1;
    foreach (var c in s) if (conteo[c] == 1) return c;
    return null;
}
"Dos pasadas O(n): una para contar en un diccionario, otra para encontrar la primera con conteo 1. Preservo el orden original recorriendo el string, no el diccionario."

F2 · ¿Hay dos números que sumen X? 8 min

public static bool ExistePar(int[] nums, int objetivo)
{
    var vistos = new HashSet<int>();
    foreach (var n in nums)
    {
        if (vistos.Contains(objetivo - n)) return true;
        vistos.Add(n);
    }
    return false;
}
"El ingenuo es doble bucle O(n²). Con un HashSet de lo ya visto, por cada número pregunto si vi su complemento — O(n), O(n) memoria. El trade-off clásico tiempo/espacio."

F3 · Agrupar pedidos por cliente y sumar total 8 min

var porCliente = pedidos
    .GroupBy(p => p.ClienteId)
    .ToDictionary(g => g.Key, g => g.Sum(p => p.Total));

En JS/TS: Object.groupBy(pedidos, p => p.clienteId) o reduce con un Map.

F4 · Invertir una lista enlazada (por si el rol lo pide) 10 min

public static Node? Invertir(Node? head)
{
    Node? prev = null;
    while (head is not null)
    {
        var siguiente = head.Next;
        head.Next = prev;
        prev = head;
        head = siguiente;
    }
    return prev;
}
"Tres punteros: previo, actual, siguiente. Voy dando vuelta a cada enlace. O(n) tiempo, O(1) espacio. La versión recursiva es elegante pero usa la pila."

Recursión vs iteración.

Recursión: cuando el problema se define en términos de sí mismo (árboles, backtracking). Riesgo: stack overflow en profundidades grandes. Iteración con estructura explícita (stack/queue) cuando el volumen lo exige. "En árboles de categorías de producto uso recursión; si el árbol pudiera ser muy profundo, una pila."

Conductuales — historias STAR

Situación · Tarea · Acción · Resultado. Cada respuesta: 60–90 s, con una cifra al final. Rellena los [huecos] con datos reales antes de la entrevista.

Antes de contar: dónde está la línea de confidencialidad

Todo tu material gira en torno a Seus Web, un producto comercial de una empresa real. Contar de más puede violar el NDA que firmaste — y un entrevistador con criterio lo nota y lo penaliza: si aireas lo de tu empleo anterior, airearás lo suyo.

✅ Cuenta sin problema❌ No cuentes
Arquitectura y decisiones técnicas · stack y versiones · el problema en abstracto · tu rol y tus commits · métricas tuyas (86 commits, 18+ módulos) Nombres de clientes o cadenas de farmacias · cifras de negocio (ventas, márgenes, usuarios) · código o capturas · credenciales o endpoints internos · fallos concretos que dejen mal a la empresa

La fórmula segura: describe el problema y tu solución, no los datos del negocio. "Un punto de venta para retail farmacéutico con más de 18 módulos" dice todo lo que necesitan sin nombrar a nadie.

Si insisten en un dato sensible: "Eso ya entra en información de la empresa y preferiría no entrar ahí. Lo que sí te puedo contar es cómo lo resolvimos técnicamente."

Sobre los bancos de preguntas (incluido este): no te aprendas las respuestas de memoria. En un simulacro real, el entrevistador detectó al candidato porque repitió la definición casi palabra por palabra dos veces y le preguntó si estaba leyendo. Entiende qué quieren saber con cada pregunta y construye la respuesta con tus palabras y tu caso.

Historia 1 — Liderar la migración a microfrontends

S: El producto era un monolito Angular de 18+ módulos corriendo en 86 sucursales; los builds y despliegues eran lentos y un cambio en un módulo obligaba a re-desplegar todo — con el riesgo que eso implica en un sistema del que dependen +500 terminales para vender.

T: Migrar los módulos de backoffice a una arquitectura de microfrontends sin frenar el desarrollo de features.

A: Elegí Native Federation por el stack moderno (esbuild). Migré compra directa, recepción electrónica, monitor de embarques y pedido manual a Angular 20; creé una librería Angular compartida para no duplicar; definí el contrato host–remotos y el versionado de la librería.

R: Despliegues independientes por módulo. Fui el mayor contribuidor del repo con 86 commits. [tiempo de build antes/después si lo tienes]

Historia 2 — Estandarizar el manejo de errores (refactor transversal)

S: Cada módulo del front capturaba errores HTTP a su manera; comportamiento inconsistente, sesiones que no se cerraban bien.

T: Unificar el manejo de errores y de sesión en toda la app sin romper los módulos existentes.

A: Interceptor HTTP único + guards de sesión y roles; migré el flujo disperso a uno basado en promesas; fui módulo por módulo validando. Aproveché para cerrar fugas de suscripciones viejas.

R: Un solo punto de manejo de error; [nº de módulos tocados]; base para el cifrado de storage que vino después.

Historia 3 — Bug crítico de negocio bajo presión

S: [elige: folios en compra directa / numerología de piezas en recepción electrónica / CORS en las APIs de facturación] — fallaba en producción y afectaba la operación de farmacias.

T: Encontrar la causa y corregir sin romper el resto del flujo.

A: Reproduje con datos reales, aislé [la causa], corregí y agregué [validación / prueba] para que no volviera.

R: Operación restablecida [en X tiempo]; el defecto no reapareció.

Historia 4 — Iniciativa propia con impacto de seguridad

S: Datos sensibles de operación farmacéutica se guardaban en claro en el localStorage del navegador.

T: No estaba asignado, lo noté yo. Propuse cifrar el almacenamiento del cliente.

A: Diseñé el StorageEncryptionService (work item DEV-18726), lo integré en el flujo de sesión y migré los datos existentes.

R: Los datos sensibles dejaron de estar en claro en el navegador. Riesgo cerrado antes de que fuera un incidente.

Historia 5 — Diseño que dio fruto (el patrón de 4 capas)

S: Había que consumir 4 microservicios .NET distintos desde el front, cada uno con su forma.

T: Que los componentes no se acoplaran a cada backend.

A: Diseñé el patrón Interfaz–Service–Assembly–Promise; lo documenté y lo adoptó el equipo.

R: Cambios de contrato de un servicio quedan contenidos en 2 capas; los componentes no se enteran.

Preguntas y a qué historia van

PreguntaHistoria
"Cuéntame de un proyecto del que estés orgulloso"1 o 5
"Una vez que lideraste sin ser el líder formal"1 o 2
"Un error que cometiste / algo que salió mal"[preparar una honesta: una decisión técnica que reconsideraste]
"Trabajo bajo presión / un deadline difícil"3
"Iniciativa / algo que hiciste sin que te lo pidieran"4
"Un desacuerdo técnico con un compañero"[preparar: cómo lo resolviste con datos, no con ego]
"Por qué dejaste / estás dejando tu empleo"Ver Guion — respuesta neutra, sin quejarse
Regla de oro STAR: el 60% del tiempo en la Acción (qué hiciste TÚ, no "el equipo"), y siempre cierra con la Resultado cuantificada.

Guion hablado — lo que se repite en todas

"Cuéntame de ti" (60–90 s)

"Soy desarrollador full stack con alrededor de cinco años, todos en el mismo producto: Seus Web, un punto de venta y backoffice para retail farmacéutico. Es un sistema de misión crítica: opera en 86 sucursales en toda la República, con más de 500 terminales entre cajas y estaciones de pedidos, y por ahí pasan del orden de millones de pesos en transacciones cada día — si el punto de venta se cae, las sucursales dejan de vender. Primero trabajé sobre el punto de venta, y después lideré la migración del backoffice a microfrontends con Native Federation, en Angular 20; fui el mayor contribuidor de ese repositorio. Del lado del back, APIs REST en .NET Core desplegadas serverless en AWS Lambda, integrando microservicios internos, reportería y facturación electrónica CFDI, con Azure DevOps de punta a punta y SonarQube. Ahora busco un rol remoto donde pueda seguir en full stack Angular + .NET y tomar más responsabilidad de arquitectura."

Por qué este pitch pega más que el anterior

La escala llega en la segunda frase, antes que cualquier tecnología. "86 sucursales y más de 500 terminales" te reclasifica: dejas de ser alguien que sabe Angular y pasas a ser alguien que mantuvo un sistema de misión crítica a escala nacional. Eso es lo que justifica un sueldo de 45–60k.

La frase "si el punto de venta se cae, las sucursales dejan de vender" hace el trabajo de diez adjetivos: comunica la presión real bajo la que trabajabas sin que tengas que decir "trabajo bien bajo presión".

Y el orden punto de venta → backoffice cuenta una progresión: entraste al core transaccional y luego te dieron la migración arquitectónica.

Las 3 correcciones que más repiten los reclutadores

De una sesión de reclutadores evaluando pitches reales de candidatos en vivo:

  1. Vende UNA identidad, no dos. A un candidato que se presentó como frontend y desarrollador iOS: "la empresa va a decidir para cuál de las dos vacantes estás aplicando… la mayoría de las empresas pagan por especialización". → Tú eres full stack Angular + .NET: eso es una identidad coherente. Nunca abras con "puedo hacer front o back, lo que necesiten".
  2. No recites tecnologías. "No hay que apresurarse por mencionar todas… menciona en las que tienes más fortaleza y las que estás queriendo aprender". → Angular y .NET con contexto. El resto solo si viene a cuento.
  3. Di el impacto, no solo el detalle técnico. Textual: "mencionar a cuántos usuarios aproximadamente impactó esta app… ese es un dato que a todas las empresas les interesa".

El punto 3 era tu hueco y ya está resuelto. Tu escala real:

DatoCómo se dice
86 sucursales en toda la República✅ Dilo tal cual. Es alcance de despliegue, es técnico y es impresionante.
+500 terminales (5–6 cajas + 1 estación de pedidos por sucursal)✅ Dilo tal cual. Habla de la escala del sistema, no del negocio.
~3 millones de pesos diarios⚠️ Di "del orden de millones de pesos en transacciones al día". La cifra exacta es la facturación de un tercero.
El nombre de la cadena❌ Nunca. "Retail farmacéutico" basta.
Cuidado con el 86 duplicado. Son 86 sucursales y 86 commits. Decir las dos cifras en el mismo minuto suena a que te trabaste. Quédate con las sucursales —ahora pesan más— y para el repositorio di solo "fui el mayor contribuidor", sin número.

"¿Por qué te interesa esta empresa / puesto?"

Prepara 2–3 frases con algo concreto de la empresa (su producto, su stack, su etapa). Molde:

"Me encaja por [stack: coincide casi campo por campo con lo que hago]. Y me interesa [algo real del producto/empresa]. Vengo de un solo producto muy a fondo; quiero llevar esa profundidad a [su contexto]."

Expectativa salarial

SituaciónQué dices
Te preguntan primero el rango"Mi rango objetivo está entre 35 y 60 mil MXN mensuales, según el alcance del rol y las prestaciones. ¿Cuál es la banda que tienen para la posición?"
100% remoto y todo lo demás encajaPiso real 27 mil, pero no lo ancles ahí: pide 35+.
Insisten en un númeroDa el objetivo (45–50k), no el piso. Siempre puedes bajar, nunca subir.
Ofrecen por debajo de 27k"Está por debajo de lo que puedo considerar para un rol de este nivel."

"¿Por qué dejaste tu empleo?"

"La etapa en Seus se cerró en agosto. Fueron cinco años muy sólidos —llevé el producto de un monolito a microfrontends— y busco un siguiente paso con más alcance de arquitectura y en modalidad remota."

Nunca: quejarte del jefe, del sueldo, de los compañeros o de la empresa. Neutro y hacia adelante.

"¿Tu mayor debilidad?"

"Vengo de un solo producto muy a fondo, así que mi exposición a variedad de arquitecturas es menor que la de alguien que ha rotado por varias empresas. Lo compenso estudiando fuera del trabajo —he montado varios proyectos propios de punta a punta— pero soy consciente de que el primer par de meses en un stack nuevo pido más contexto."

Disponibilidad

"Disponibilidad inmediata." Remoto, con base en [Ciudad de México — según la decisión de las postulaciones].

Inglés — si lo tocan

"Mi inglés es intermedio en lectura —la documentación técnica y los issues los manejo sin problema— y básico en conversación. Si el rol requiere reuniones en inglés a diario, lo digo de frente; si es lectura y algo de escritura, ahí sí estoy cómodo, y lo estoy trabajando activamente."

Auto-intro en inglés lista → pestaña Inglés.

Preguntas que TÚ haces (elige 4–5)

Inglés técnico — kit de rescate

Para procesos donde el inglés es "deseable" o hay un reclutador que suelta un par de preguntas en inglés. Si exigen C1 conversacional, es filtro duro — no fuerces.

Auto-introducción (~40 s) — apréndela de memoria

"Hi, thanks for the time. I'm a full-stack developer with around five years of experience, all on the same product: a point-of-sale and back-office system for pharmacy retail, with more than eighteen modules in production. On the front end I work with Angular, versions seventeen to twenty, and I led the migration to a micro-frontends architecture. On the back end I build REST APIs with .NET Core, running serverless on AWS Lambda. I use Azure DevOps end to end, with SonarQube for code quality. I was the top contributor to the micro-frontends repository. I'm looking for a remote full-stack role with Angular and .NET where I can take on more architecture responsibility."

Frases de rescate

SituaciónFrase
No entendiste"Could you rephrase that, please?" / "Sorry, could you say that more slowly?"
Ganar tiempo"That's a good question. Let me think for a second."
Confirmar que entendiste"So, if I understand correctly, you're asking about [X]. Is that right?"
Ser honesto del nivel"My spoken English is intermediate — I'm more comfortable reading and writing. I can follow technical discussions and I'm improving it actively."
Cerrar una respuesta"Does that answer your question, or would you like me to go deeper?"

Vocabulario del dominio (por si lo dices en inglés)

desplieguedeployment / to ship
rama / fusionarbranch / to merge
pruebas unitariasunit tests · cobertura = coverage
deuda técnicatechnical debt
rendimientoperformance · cuello de botella = bottleneck
mantenibilidadmaintainability
a gran escalaat scale · alta disponibilidad = high availability
requisitorequirement · aclarar alcance = clarify scope

Micro-respuestas preparadas

"Tell me about a challenging project."

"The migration from a monolith to micro-frontends. The product had eighteen modules and every deploy was slow. I moved the back-office modules to Angular twenty with Native Federation and a shared library. The hard part was the contract between the host and the remotes, and versioning the shared library. After that, each module could be deployed on its own."

"Why do you want to work remotely?"

"I've been fully remote for the last few years and I'm productive that way. I communicate clearly in writing, I over-document, and I keep my work visible through pull requests and the board."

Simulacro y checklist

Antes de conectarte (prueba en vivo)

Durante — el ritual de cada ejercicio

  1. Reformula el enunciado con tus palabras. "Entonces necesitas X que devuelva Y…"
  2. Pregunta de aclaración (mínimo una): tipos, casos borde, volumen, formato de salida.
  3. Anuncia el enfoque antes de teclear. "Voy a hacerlo con un servicio abstracto para no acoplar…"
  4. Teclea narrando. "Primero el SELECT… ahora el JOIN porque necesito el nombre del producto…"
  5. Repasa en voz alta al terminar: qué hace, qué asumí, qué haría distinto con más tiempo.
  6. Ofrece el siguiente paso. "Con más volumen esto sería keyset en vez de OFFSET."

Si te trabas

Trampas conocidas (de las postulaciones)

TrampaDefensa
La tarjeta decía "remoto", el rol es híbridoConfirmar modalidad con el reclutador antes de invertir tiempo
"¿Cuántos años de X?" en el formularioAngular=5, C#=5, REST=5, .NET Framework=2, Design Systems=2, inglés=Básico
Piden 8+ años y tú tienes 5La carta lo aclara; en entrevista: "Cinco años pero muy densos, un solo producto a gran escala"
Piden título expedido"Pasante de ITICS, titulación en trámite" — no mentir
"Sin EF / sin stored procedures"SQL a mano + Dapper. Ver pestañas SQL y .NET escrito
Prueba que parece hablada pero piden teclearTen los ejercicios escritos practicados con las manos, no solo leídos

Autoevaluaciones de portal (respuestas consistentes)

AngularAvanzado
APIs RESTAvanzado
GitAvanzado
C# / .NET CoreAvanzado
SQL ServerIntermedio
Docker / OWASPBásico (teórico, no aplicado en el trabajo)
InglésBásico
Ciclo de vida de proyectoAvanzado (Azure DevOps end to end)

Cheat-sheet — leer 5 minutos antes

Pitch en 3 bullets

  • ~5 años, un solo producto a fondo: Seus Web, POS + backoffice farmacéutico, 18+ módulos en prod.
  • Misión crítica a escala nacional: 86 sucursales, +500 terminales, millones de pesos en transacciones al día.
  • Front: Angular 17–20, lideré la migración a microfrontends (Native Federation). Back: .NET Core REST serverless en AWS Lambda.
  • Mayor contribuidor del repo de microfrontends. Azure DevOps + SonarQube.

Las 3 reglas al hablar

1. Sin "creo/siento/pienso" — seguridad. 2. Cada respuesta con contexto de Seus. 3. Término técnico exacto.

Molde de respuesta

Afirmación directa → caso Seus → término exacto → matiz.

Números

Salario: pido 45–50k, objetivo 35–60k, piso 27k solo si 100% remoto. Experiencia: 5 años. Disponibilidad: inmediata.

Reflejos técnicos

  • Buscador → debounceTime + distinctUntilChanged + switchMap.
  • Submit → exhaustMap. Guardados en orden → concatMap.
  • Rango de fechas → semiabierto (>= desde AND < hasta), nunca BETWEEN.
  • Paginación determinista → ORDER BY con desempate por id.
  • DI en API: DbContext/repos = Scoped. Config/cache = Singleton.
  • Envelope Result<T>: processCorrect / typeError / data, HTTP 200 siempre, validación sube al servicio (no excepción).
  • Componente desacoplado → InjectionToken, no HttpClient directo.
  • Índice para WHERE x + SELECT a,bINDEX(x) INCLUDE(a,b) covering.

3 historias listas

1) Migración a microfrontends. 2) Estandarización de manejo de errores. 3) StorageEncryptionService por iniciativa propia.

Mis 3 preguntas top

· ¿Cómo es el ciclo de ramas/PR/despliegue? · ¿El mayor reto técnico del equipo ahora? · ¿Hay camino hacia arquitectura/liderazgo?

No decir

· "Creo que…" · quejas del empleo anterior · que sé algo que no sé · números por debajo de mi objetivo · quedarme callado tecleando.