# Dev .NET y C#: ocho rutas de aprendizaje

Manual detallado · Edición 6 · Revisión editorial y casos de uso: 9 de octubre de 2026

Responsable editorial: Andrez Higuera. Elaboración propia con apoyo de ChatGPT.

Este cuaderno trabaja el caso ficticio de reserva de espacios, con salas, grupos e identidades de ejemplo.

Original: https://briefing.andrezhiguera.cl/academia/rutas/dotnet

Los textos, ejemplos, ejercicios y soluciones propios de este cuaderno se ofrecen bajo CC BY-NC 4.0: https://creativecommons.org/licenses/by-nc/4.0/. Conserva atribución, enlace al original e indica cambios. Los artículos externos y la documentación enlazada conservan sus condiciones y no se incluyen en esta licencia.

## Cómo estudiar

Las ocho rutas contienen 51 lecciones y 26 horas estimadas de lectura y práctica. Las lecturas externas y la implementación completa requieren dedicación adicional.

Cada lección empieza con un caso de uso que sitúa el problema y anticipa lo que aprenderás. Después sigue concepto, ejemplo explicado por pasos, criterios de uso, práctica y comprobación. Dos criterios específicos permiten comparar la evidencia de tu práctica con el objetivo; no certifican un aprendizaje automáticamente. Estudia una lección a la vez. La ampliación opcional puede leerse después de completar la práctica.

Base educativa: .NET 10 / C# 14. El ZIP contiene Laboratorio.csproj y Program.cs, con un núcleo de consola y 15 comprobaciones ejecutadas. Los fragmentos HTTP, EF Core, Docker y CI requieren las dependencias y configuración que indica cada ejemplo; no se presentan como integraciones completas ejecutadas.

Descarga: https://briefing.andrezhiguera.cl/academia/recursos/laboratorio-dotnet.zip

Después de extraer el ZIP, ejecuta:

```shell
dotnet --info
dotnet run --project Laboratorio.csproj --configuration Release
```

El laboratorio no abre una API ni configura una base de datos. Su runner usa dotnet run; una solución con un framework de tests agrega dotnet test. Una práctica solo constituye evidencia propia cuando la ejecutas y registras el resultado.

## Índice de lecciones

- Ruta 01 · Tema 01: [Entorno .NET, proyectos y versiones](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-entorno)
- Ruta 01 · Tema 02: [POO: encapsulación, composición y polimorfismo](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-poo)
- Ruta 01 · Tema 03: [Entidades, objetos de valor e invariantes](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-invariantes)
- Ruta 01 · Tema 04: [SOLID: responsabilidades y contratos útiles](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-solid)
- Ruta 01 · Tema 05: [Colecciones, genéricos y LINQ](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-linq)
- Ruta 01 · Tema 06: [async/await, I/O y CancellationToken](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-asincronia)
- Ruta 01 · Tema 07: [Código legible, decisiones y revisión](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-practicas-codigo)
- Ruta 02 · Tema 01: [REST: recursos, verbos y respuestas HTTP](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-rest)
- Ruta 02 · Tema 02: [Minimal APIs: endpoints y delegación](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-minimal-apis)
- Ruta 02 · Tema 03: [Inyección de dependencias y composición](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-inyeccion)
- Ruta 02 · Tema 04: [Transient, Scoped y Singleton](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-ciclos-vida)
- Ruta 02 · Tema 05: [Middleware y orden del pipeline](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-middleware)
- Ruta 02 · Tema 06: [Ambientes, configuración y secretos](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-configuracion)
- Ruta 02 · Tema 07: [Options Pattern y validación al inicio](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-options)
- Ruta 02 · Tema 08: [Versionado y evolución de contratos](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-versionado)
- Ruta 03 · Tema 01: [EF Core: DbContext, tracking y CRUD](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-ef-crud)
- Ruta 03 · Tema 02: [Fluent API: restricciones y mapeo de entidades](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-fluent-api)
- Ruta 03 · Tema 03: [Relaciones, claves y política de eliminación](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-relaciones)
- Ruta 03 · Tema 04: [DTOs, proyección y mapeo manual](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-dtos-mapeo)
- Ruta 03 · Tema 05: [Migraciones y cambios compatibles de esquema](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-migraciones)
- Ruta 03 · Tema 06: [Transacciones, atomicidad y outbox](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-transacciones)
- Ruta 03 · Tema 07: [Concurrencia optimista y carreras de inserción](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-concurrencia)
- Ruta 03 · Tema 08: [Consultas eficientes, paginación y medición](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-rendimiento)
- Ruta 04 · Tema 01: [FluentValidation y reglas de entrada](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-fluentvalidation)
- Ruta 04 · Tema 02: [ProblemDetails: errores HTTP consistentes](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-problemdetails)
- Ruta 04 · Tema 03: [Manejo global de excepciones y cancelación](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-excepciones)
- Ruta 04 · Tema 04: [Logging estructurado y Serilog](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-logging)
- Ruta 05 · Tema 01: [DDD básico: lenguaje, agregados y límites](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-ddd)
- Ruta 05 · Tema 02: [Clean Architecture y dirección de dependencias](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-clean-architecture)
- Ruta 05 · Tema 03: [Repository Pattern con propósito](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-repository)
- Ruta 05 · Tema 04: [Specification Pattern: criterios y evaluadores](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-specification)
- Ruta 06 · Tema 01: [Result Pattern y rechazos esperados](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-result)
- Ruta 06 · Tema 02: [CQRS: comandos y consultas separados](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-cqrs)
- Ruta 06 · Tema 03: [Mediator y MediatR: despacho y behaviors](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-mediador)
- Ruta 06 · Tema 04: [Idempotencia, reintentos y deduplicación](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-idempotencia)
- Ruta 06 · Tema 05: [Antipatrones: acoplamiento y efectos ocultos](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-antipatrones)
- Ruta 06 · Tema 06: [Monolito modular y decisión de microservicios](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-microservicios)
- Ruta 07 · Tema 01: [Autenticación y validación de JWT](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-autenticacion-jwt)
- Ruta 07 · Tema 02: [Autorización mediante políticas](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-politicas)
- Ruta 07 · Tema 03: [Autorización por recurso y aislamiento de grupos](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-autorizacion-recurso)
- Ruta 07 · Tema 04: [Renovación de sesión y rotación de tokens](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-refresh-tokens)
- Ruta 07 · Tema 05: [CORS y límites de confianza del navegador](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-cors)
- Ruta 07 · Tema 06: [Rate limiting y tratamiento de abuso](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-rate-limiting)
- Ruta 08 · Tema 01: [Pruebas unitarias del comportamiento](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-pruebas-unitarias)
- Ruta 08 · Tema 02: [Pruebas de integración de API y persistencia](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-pruebas-integracion)
- Ruta 08 · Tema 03: [Comprobaciones de arquitectura](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-pruebas-arquitectura)
- Ruta 08 · Tema 04: [Selección y mantenimiento de bibliotecas](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-bibliotecas-dependencias)
- Ruta 08 · Tema 05: [Health checks: vida y preparación](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-health-checks)
- Ruta 08 · Tema 06: [Observabilidad: logs, métricas y trazas](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-observabilidad)
- Ruta 08 · Tema 07: [Contenedores y configuración de ejecución](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-docker)
- Ruta 08 · Tema 08: [Integración continua y entrega verificable](https://briefing.andrezhiguera.cl/academia/lecciones/dotnet-tema-ci-cd)

## Ruta 01

### Tema 01 · Entorno .NET, proyectos y versiones

**Caso de uso · Preparar el primer proyecto**

Una persona recibe el código del sistema de reservas, pero su equipo solo puede ejecutar aplicaciones ya compiladas. Necesita saber qué instalar y qué archivos conservar.

Examinarás el proyecto y los comandos de .NET para separar herramientas de desarrollo, ejecución y resultados generados.

**Objetivo:** Distinguir SDK, runtime y archivos fuente de un proyecto.

**Requisitos:** Instalar el SDK .NET 10 y un editor.

#### 01. Qué es y qué problema resuelve

El SDK reúne compilador, plantillas y herramientas; el runtime ejecuta aplicaciones. Un proyecto declara framework objetivo y dependencias.

Una solución agrupa proyectos, pero no impone una arquitectura. Antes de resolver un fallo del código verifica qué SDK selecciona tu terminal y dónde busca el proyecto: dos carpetas pueden activar configuraciones distintas.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Guarda el XML como Laboratorio.csproj en una carpeta con Program.cs y un punto de entrada. Requiere un SDK compatible con net10.0. El archivo de proyecto no se pega dentro del código C#; dotnet build y dotnet run se ejecutan desde esa carpeta. Proyecto de consola sin dependencias externas. Guarda este XML como Laboratorio.csproj y usa un Program.cs propio. El ZIP ya contiene ambos; no reemplaces archivos sin revisar el destino.

```XML
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
```

**Cómo funciona:**

1. Lee TargetFramework: net10.0 define el entorno al que apunta el proyecto.
2. Nullable activa advertencias sobre referencias nulas; ImplicitUsings importa namespaces habituales.
3. Guarda el archivo como Laboratorio.csproj, agrega Program.cs y ejecuta dotnet build y dotnet run.

**Resultado esperado:** dotnet build compila el proyecto; dotnet run ejecuta su punto de entrada.

#### 03. Cuándo usarlo y qué debes evitar

net10.0 es la base estable de este curso; el SDK selecciona C# 14 de manera compatible. Un global.json permite acordar SDK y roll-forward, mientras que fijar LangVersion=latest puede introducir una diferencia entre equipos.

Versiona el proyecto y los archivos de configuración; mantén bin y obj como resultados de compilación. Nullable detecta posibles referencias nulas durante análisis, sin impedir por sí solo que JSON contenga null.

**Error frecuente:** Si una compilación falla, revisa primero el SDK seleccionado con dotnet --info y la carpeta desde la que ejecutas el comando.

#### 04. Practica lo aprendido

Compara dotnet --info con el target del laboratorio. Crea una carpeta nueva, compila y explica qué archivos son fuente y cuáles son resultados.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Compara dotnet --info con el target del laboratorio. Crea una carpeta nueva, compila y explica qué archivos son fuente y cuáles son resultados.
3. Comprueba y registra el resultado: dotnet build compila el proyecto; dotnet run ejecuta su punto de entrada.

**Solución orientativa:**

El .csproj y Program.cs son fuente; bin y obj se regeneran. Documenta SDK .NET 10, framework net10.0 y el comando ejecutado.

#### 05. Comprueba lo aprendido

**¿Tener un runtime instalado garantiza que puedas compilar?**

No. Compilar requiere un SDK compatible; el runtime solo aporta ejecución.

**¿Qué falta si la consola ejecuta aplicaciones, pero no permite compilar este proyecto?**

Falta un SDK compatible con net10.0 y que la terminal lo seleccione. El runtime ejecuta código ya compilado; no aporta el compilador ni las herramientas del SDK.

**Cómo demostrar el objetivo:**

- Identificas el SDK y el framework objetivo a partir de dotnet --info y el .csproj.
- Clasificas Program.cs y .csproj como fuentes, y bin y obj como resultados regenerables.

**Qué debes recordar:**

- El SDK compila; el runtime ejecuta.
- El proyecto y el código son fuente; bin y obj se regeneran.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Preparar el entorno y leer un programa C#**

La edición de esta ruta usa .NET 10 LTS, ASP.NET Core 10, EF Core 10 y C# 14. Trabaja con el último parche estable compatible del SDK y de los paquetes; no actives versiones preview para resolver un error de configuración. El target net10.0 selecciona la versión de lenguaje correspondiente sin fijar LangVersion a latest. Nullable activa análisis estático: no sustituye validar datos recibidos por HTTP.

Un programa contiene expresiones que producen valores, instrucciones que realizan acciones y tipos que definen contratos. En C#, Guid y DateTimeOffset son tipos de valor; una instancia de clase se accede por referencia. Un record ofrece igualdad basada en sus miembros, útil para mensajes y valores. No vuelve inmutable a un objeto mutable contenido en una propiedad. Usa nombres que representen el negocio antes de discutir carpetas.

Abre el laboratorio descargable, ejecuta dotnet --info y después dotnet run --project Laboratorio.csproj --configuration Release. No necesita paquetes externos. Su objetivo es observar el comportamiento del núcleo; no abre un endpoint ni crea una base de datos. Cada OK es una comprobación ejecutada, no una simulación visual.

**Arranque reproducible** · Comandos ejecutables con SDK .NET 10

```Shell
dotnet --info
dotnet new console --framework net10.0 --name PrimerHorario
cd PrimerHorario
dotnet run
```

El SDK crea un proyecto de consola con un punto de entrada. Usa una carpeta nueva para no sobrescribir archivos. El laboratorio ya trae su proyecto: allí basta ejecutar dotnet run.

**Resultado:** Un proyecto nuevo imprime Hello, World!; el laboratorio imprime 15 comprobaciones aprobadas.

### Tema 02 · POO: encapsulación, composición y polimorfismo

**Caso de uso · Cambiar el canal de avisos**

El sistema escribe avisos de reserva en consola. Para una prueba queremos guardar los identificadores en memoria sin cambiar el código que solicita el aviso.

Verás cómo un contrato y la composición permiten sustituir un colaborador, y qué comportamiento debe conservar la nueva implementación.

**Objetivo:** Separar un contrato de sus implementaciones mediante composición.

**Requisitos:** Entorno .NET, proyectos y versiones.

#### 01. Qué es y qué problema resuelve

Programación orientada a objetos organiza estado y comportamiento mediante contratos. Encapsular consiste en controlar cómo cambia un objeto, no en agregar getters y setters a cada campo.

Un servicio que cualquiera puede modificar después de construirse pierde las garantías que pretendía proteger.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Ejemplo de consola .NET 10. Integra el fragmento en Program.cs o en sus tipos auxiliares según corresponda; no mezcles declaraciones de tipos antes de instrucciones de nivel superior. No requiere una API ni una base. Implementación didáctica: escribe en consola, no envía correo. El consumidor conoce IAvisoReserva y la raíz de composición elige el canal.

```C#
public interface IAvisoReserva
{
    Task NotificarAsync(Guid reservaId, CancellationToken ct);
}
public sealed class AvisoConsola : IAvisoReserva
{
    public Task NotificarAsync(Guid reservaId, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        Console.WriteLine($"Aviso ficticio para {reservaId}");
        return Task.CompletedTask;
    }
}
```

**Cómo funciona:**

1. IAvisoReserva define la operación que necesita el consumidor.
2. AvisoConsola implementa ese contrato sin añadir una dependencia de correo real.
3. La composición elige la implementación; el consumidor sigue trabajando con la interfaz.

**Resultado esperado:** Una llamada escribe un aviso; un token cancelado impide escribirlo.

#### 03. Cuándo usarlo y qué debes evitar

El polimorfismo permite tratar implementaciones diferentes a través de un contrato. Usa composición cuando una reserva necesita una política de aviso; heredar ReservaUrgente puede mezclar identidad, reglas y canal de comunicación.

Una implementación sustituta debe cumplir el mismo significado del resultado. Si un canal devuelve éxito antes de aceptar el envío, documenta esa garantía o cambia el contrato.

**Error frecuente:** Cambiar de implementación debe conservar la garantía del contrato: escribir un aviso no equivale a entregarlo por correo.

#### 04. Practica lo aprendido

Agrega un canal que conserve identificadores en memoria e inyéctalo a un consumidor sin modificar el contrato.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Agrega un canal que conserve identificadores en memoria e inyéctalo a un consumidor sin modificar el contrato.
3. Comprueba y registra el resultado: Una llamada escribe un aviso; un token cancelado impide escribirlo.

**Solución orientativa:**

Ambos canales implementan NotificarAsync. El test verifica el identificador observado, no el tipo interno del canal.

#### 05. Comprueba lo aprendido

**¿Una interfaz asegura por sí sola bajo acoplamiento?**

No. Puede exponer detalles del proveedor o un contrato demasiado grande; revisa significado y consumidores.

**¿Cómo sustituirías la consola por un canal de memoria sin cambiar al consumidor?**

Implementa IAvisoReserva en el canal de memoria e inyéctalo al consumidor. La prueba compara el identificador recibido, no la clase concreta ni su almacenamiento interno.

**Cómo demostrar el objetivo:**

- El canal de memoria implementa el mismo contrato de notificación.
- El consumidor recibe el canal por constructor y la prueba observa el identificador enviado.

**Qué debes recordar:**

- Encapsular protege comportamiento y estado.
- La composición permite cambiar colaboradores sin cambiar al consumidor.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Aplicar los cinco principios SOLID con criterio**

S: responsabilidad única significa una razón coherente de cambio. Reserva cambia por reglas del negocio; un serializador cambia por el formato HTTP. O: abierto/cerrado propone extender comportamiento estable mediante puntos de variación útiles; agrega un segundo canal de aviso sin reescribir CancelarReserva. L: sustitución exige respetar precondiciones, resultados y efectos del contrato; un repositorio alternativo no puede devolver éxito antes de guardar si el contrato promete persistencia.

I: segregación de interfaces evita que una consulta de disponibilidad dependa de métodos para borrar reservas. D: inversión de dependencias hace que el caso de uso conozca un contrato IReservas y que el adaptador de almacenamiento lo implemente. Inyección de dependencias es el mecanismo que entrega el objeto; inversión es la dirección de la dependencia. Se puede inyectar una dependencia concreta y seguir teniendo un acoplamiento incorrecto.

Usa composición para sumar capacidades independientes. Una jerarquía Sala → SalaVirtual puede ser problemática si su contrato exige aforo físico o dirección postal. Una colección de atributos o políticas expresaría mejor diferencias reales. No introduzcas fábricas, interfaces genéricas y herencia para anticipar variantes que todavía no existen. Explica qué cambio previsto justifica cada frontera.

**Un contrato acotado al caso de uso** · Contrato compilado en el laboratorio

```C#
public interface IReservas
{
    Task<Reserva?> BuscarAsync(Guid id, CancellationToken ct);
    Task GuardarAsync(CancellationToken ct);
}
```

La aplicación pide dos capacidades. La implementación puede usar una base de datos; el dominio no importa ese detalle. GuardarAsync representa la confirmación del trabajo pendiente en el ámbito.

**Resultado:** CancelarReserva se prueba con MemoriaReservas sin levantar HTTP.

### Tema 03 · Entidades, objetos de valor e invariantes

**Caso de uso · Rechazar horarios imposibles**

Dos grupos piden la sala consecutivamente: uno termina a las 13:00 y el otro comienza a esa hora. Además llega una solicitud cuyo fin coincide con el inicio.

Definirás qué intervalos son válidos y cómo tratar sus bordes; separarás esa regla local de comprobar disponibilidad en datos compartidos.

**Objetivo:** Proteger reglas que un objeto debe cumplir durante toda su vida.

**Requisitos:** POO: encapsulación, composición y polimorfismo.

#### 01. Qué es y qué problema resuelve

Una entidad conserva identidad a lo largo de cambios; un objeto de valor representa un concepto que se compara por sus valores. Reserva tiene un Id estable, mientras que Intervalo representa inicio y fin.

Dos intervalos equivalentes no necesitan identidades separadas. Esta distinción ayuda a ubicar reglas antes de configurar tablas.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Usa los tipos del núcleo del laboratorio .NET descargable. El fragmento ilustra la regla local; no configura HTTP ni persistencia. Usa Intervalo del laboratorio. El segundo empieza exactamente donde termina el primero; las horas son ficticias y deterministas.

```C#
var inicio = new DateTimeOffset(2026, 10, 20, 12, 0, 0, TimeSpan.Zero);
var primero = new Intervalo(inicio, inicio.AddHours(1));
var segundo = new Intervalo(inicio.AddHours(1), inicio.AddHours(2));
Console.WriteLine(primero.SeSolapaCon(segundo));
```

**Cómo funciona:**

1. Identifica la condición de un intervalo válido: el fin debe ser posterior al inicio.
2. Construye el valor únicamente a través de una operación que compruebe esa condición.
3. Prueba tanto un intervalo válido como uno de duración nula o negativa.

**Resultado esperado:** False: el borde final no está incluido en el primer intervalo.

#### 03. Cuándo usarlo y qué debes evitar

Un objeto válido debe nacer respetando sus invariantes. Definir fin posterior a inicio evita estados incoherentes, pero no prueba que una sala esté libre: esa condición depende de otras reservas.

Usa DateTimeOffset para instantes y conserva la zona si existen recurrencias. La regla de intervalos semiabiertos permite horarios contiguos sin tratarlos como intersecciones.

**Error frecuente:** Un setter público que permita modificar Inicio o Fin sin validar puede romper la regla después de construir el objeto.

#### 04. Practica lo aprendido

Prueba fin igual a inicio, fin anterior y solapamiento de un minuto.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Prueba fin igual a inicio, fin anterior y solapamiento de un minuto.
3. Comprueba y registra el resultado: False: el borde final no está incluido en el primer intervalo.

**Solución orientativa:**

Los dos intervalos inválidos lanzan ArgumentException; el que cruza un minuto devuelve true. Conserva los tres resultados.

#### 05. Comprueba lo aprendido

**¿Validar el horario demuestra disponibilidad?**

No. Disponibilidad requiere datos compartidos y una estrategia de consistencia al escribir.

**¿Por qué dos reservas consecutivas pueden ser válidas y una de duración cero debe rechazarse?**

Un intervalo semiabierto excluye su fin: terminar a las 13:00 no ocupa el comienzo de la siguiente reserva. Fin igual a inicio viola la regla de duración positiva.

**Cómo demostrar el objetivo:**

- Los intervalos de duración cero o negativa se rechazan.
- La prueba distingue contacto exacto en el borde y solapamiento de un minuto.

**Qué debes recordar:**

- Una invariante debe cumplirse siempre.
- Un objeto de valor representa un significado y valida sus límites.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Un valor de dominio y una entidad con invariantes**

Una invariante es una condición que siempre debe ser verdadera para un objeto válido. El intervalo exige fin posterior al inicio. Se modela como intervalo semiabierto [inicio, fin): una reserva que termina a las 13:00 permite otra que comienza a las 13:00. Dos intervalos se cruzan si cada inicio es anterior al fin del otro. Especificar esta convención evita errores de frontera.

Reserva posee identidad y controla la transición a cancelada. El método devuelve si hubo cambio: una segunda cancelación es aceptable y no duplica efectos. Esa decisión hace idempotente la transición local. El caso de uso deberá impedir que un reintento envíe dos notificaciones; la entidad por sí sola no coordina efectos externos. DateTimeOffset representa un instante con offset, pero no contiene las reglas futuras de una zona horaria: para reservas recurrentes conserva también la zona y la política ante cambios de hora.

**Intersección sin errores en el borde** · Tipo completo; variante del laboratorio

```C#
public sealed record Intervalo
{
    public DateTimeOffset Inicio { get; }
    public DateTimeOffset Fin { get; }
    public Intervalo(DateTimeOffset inicio, DateTimeOffset fin)
    {
        if (fin <= inicio) throw new ArgumentException("Horario vacío o invertido");
        Inicio = inicio;
        Fin = fin;
    }
    public bool SeSolapaCon(Intervalo otro) =>
        Inicio < otro.Fin && otro.Inicio < Fin;
}
```

El constructor evita crear intervalos inválidos. El método consulta estado sin modificarlo. El ejemplo coincide con el concepto implementado en el laboratorio.

**Resultado:** [12:00,13:00) y [13:00,14:00) no se solapan; [12:30,14:00) sí se solapa con el primero.

### Tema 04 · SOLID: responsabilidades y contratos útiles

**Caso de uso · Cambiar persistencia sin reescribir reglas**

La clase que crea reservas recibe HTTP, ejecuta SQL y envía avisos. Cambiar el almacenamiento obliga a tocar las reglas de negocio.

Separarás razones de cambio y revisarás la dirección de las dependencias usando un contrato pequeño para consultar salas.

**Objetivo:** Asignar responsabilidades y diseñar contratos con un propósito claro.

**Requisitos:** Entidades, objetos de valor e invariantes.

#### 01. Qué es y qué problema resuelve

SOLID ofrece cinco preguntas para evaluar diseño: qué cambia una clase, cómo extender una variación, qué promete un subtipo, qué capacidades necesita un consumidor y hacia qué dependencia apunta el caso de uso. No es una puntuación ni una obligación de crear una interfaz para cada tipo.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Contrato pequeño para ilustrar segregación e inversión. Su implementación exige una decisión de persistencia que no forma parte de esta declaración.

```C#
public interface IConsultaSala
{
    Task<bool> ExisteAsync(string salaId, CancellationToken ct);
}
// CrearReserva depende de IConsultaSala.
// El adaptador de datos implementa la consulta.
// El consumidor no conoce SQL ni configuración de conexión.
```

**Cómo funciona:**

1. Identifica la necesidad del consumidor: consultar una sala.
2. Declara un contrato pequeño que represente esa necesidad.
3. Deja al adaptador la consulta concreta y al caso de uso la coordinación.

**Resultado esperado:** Puedes probar el caso de uso con una sala presente o ausente sin cambiar su dependencia.

#### 03. Cuándo usarlo y qué debes evitar

En Reserva de espacios, el caso de uso cambia por coordinación del negocio y el adaptador por el motor de datos. IReservas separa esas razones.

Una consulta no debe depender de métodos de administración que nunca usa. El contrato de GuardarAsync debe describir qué queda confirmado; un mock que siempre devuelve éxito no verifica esa garantía.

**Error frecuente:** Crear una interfaz por cada clase no mejora el diseño si no protege una responsabilidad o una frontera útil.

#### 04. Practica lo aprendido

Separa una clase que recibe HTTP, consulta SQL y envía avisos en tres responsabilidades.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Separa una clase que recibe HTTP, consulta SQL y envía avisos en tres responsabilidades.
3. Comprueba y registra el resultado: Puedes probar el caso de uso con una sala presente o ausente sin cambiar su dependencia.

**Solución orientativa:**

HTTP traduce el contrato; el caso de uso coordina; adaptadores consultan datos y entregan avisos. Explica una razón de cambio por componente.

#### 05. Comprueba lo aprendido

**¿DIP equivale a registrar AddScoped?**

No. DIP describe la dirección de dependencias; DI entrega instancias y puede inyectar un diseño mal acoplado.

**¿Qué responsabilidades separarías antes de sustituir el almacenamiento?**

Separa traducción HTTP, coordinación de la reserva, consulta de datos y entrega de avisos según sus razones de cambio. El contrato del caso de uso no debe exponer SQL ni respuestas HTTP.

**Cómo demostrar el objetivo:**

- Asignas la traducción HTTP, la coordinación y el acceso a datos a responsabilidades explícitas.
- Justificas una razón de cambio por componente y evitas tipos de SQL en el contrato del caso de uso.

**Qué debes recordar:**

- Una responsabilidad tiene una razón de cambio.
- Una abstracción debe servir a su consumidor.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Aplicar los cinco principios SOLID con criterio**

S: responsabilidad única significa una razón coherente de cambio. Reserva cambia por reglas del negocio; un serializador cambia por el formato HTTP. O: abierto/cerrado propone extender comportamiento estable mediante puntos de variación útiles; agrega un segundo canal de aviso sin reescribir CancelarReserva. L: sustitución exige respetar precondiciones, resultados y efectos del contrato; un repositorio alternativo no puede devolver éxito antes de guardar si el contrato promete persistencia.

I: segregación de interfaces evita que una consulta de disponibilidad dependa de métodos para borrar reservas. D: inversión de dependencias hace que el caso de uso conozca un contrato IReservas y que el adaptador de almacenamiento lo implemente. Inyección de dependencias es el mecanismo que entrega el objeto; inversión es la dirección de la dependencia. Se puede inyectar una dependencia concreta y seguir teniendo un acoplamiento incorrecto.

Usa composición para sumar capacidades independientes. Una jerarquía Sala → SalaVirtual puede ser problemática si su contrato exige aforo físico o dirección postal. Una colección de atributos o políticas expresaría mejor diferencias reales. No introduzcas fábricas, interfaces genéricas y herencia para anticipar variantes que todavía no existen. Explica qué cambio previsto justifica cada frontera.

**Un contrato acotado al caso de uso** · Contrato compilado en el laboratorio

```C#
public interface IReservas
{
    Task<Reserva?> BuscarAsync(Guid id, CancellationToken ct);
    Task GuardarAsync(CancellationToken ct);
}
```

La aplicación pide dos capacidades. La implementación puede usar una base de datos; el dominio no importa ese detalle. GuardarAsync representa la confirmación del trabajo pendiente en el ámbito.

**Resultado:** CancelarReserva se prueba con MemoriaReservas sin levantar HTTP.

### Tema 05 · Colecciones, genéricos y LINQ

**Caso de uso · Mostrar una lista sin duplicados**

El catálogo de salas combina dos fuentes y contiene sala-sur dos veces. El portal necesita nombres únicos en un orden predecible.

Recorrerás Distinct, OrderBy y la materialización; después aplicarás filtro y proyección a una colección de reservas preparada en memoria.

**Objetivo:** Filtrar y transformar colecciones con LINQ de forma predecible.

**Requisitos:** SOLID: responsabilidades y contratos útiles.

#### 01. Qué es y qué problema resuelve

Una colección expresa un conjunto de elementos; el tipo genérico indica qué operaciones y valores acepta. List<Reserva> es modificable; IReadOnlyList<Reserva> limita la interfaz de acceso, aunque no garantiza inmutabilidad profunda de sus elementos.

Decide si entregas una instantánea o una vista de datos que aún pueden cambiar.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Ejemplo de consola .NET 10. Integra el fragmento en Program.cs o en sus tipos auxiliares según corresponda; no mezcles declaraciones de tipos antes de instrucciones de nivel superior. No requiere una API ni una base. Fragmento de consola sin base de datos. El comparador ordinal hace explícita la comparación de identificadores.

```C#
var nombres = new[] { "sala-sur", "sala-norte", "sala-sur" };
var unicas = nombres.Distinct()
    .OrderBy(nombre => nombre, StringComparer.Ordinal)
    .ToArray();
Console.WriteLine(string.Join(", ", unicas));
```

**Cómo funciona:**

1. Localiza la colección de entrada y el tipo de cada elemento.
2. Sigue el filtro, la eliminación de duplicados y el orden en la secuencia escrita.
3. Observa dónde se materializa el resultado y qué elementos quedan.

**Resultado esperado:** sala-norte, sala-sur: duplicados eliminados y orden estable.

#### 03. Cuándo usarlo y qué debes evitar

LINQ separa filtro, orden y proyección. IEnumerable puede evaluar objetos; IQueryable puede representar una consulta traducible.

La ejecución diferida permite componer, pero enumerar dos veces puede repetir trabajo o observar cambios distintos. Materializa en el límite donde necesitas una instantánea y nunca cargues todas las filas antes de aplicar una restricción de acceso.

**Error frecuente:** LINQ sobre objetos y una consulta traducida por EF Core no tienen las mismas capacidades ni el mismo costo.

#### 04. Practica lo aprendido

Ejecuta el catálogo de nombres. Agrega un duplicado y cambia el orden de entrada; explica cuándo se obtiene el array. Como ampliación, filtra, proyecta y limita una colección de reservas en memoria.

1. Prepara un programa de consola con el fragmento y nombres duplicados.
2. Ejecuta con dos órdenes de entrada y compara la salida.
3. Relaciona cada operador con deduplicación, orden o materialización.

**Solución orientativa:**

La salida es sala-norte, sala-sur en ambos órdenes. Distinct elimina duplicados, OrderBy fija el orden y ToArray recorre la secuencia. La consulta de reservas es una ampliación distinta.

#### 05. Comprueba lo aprendido

**¿ToList convierte una consulta ilimitada en eficiente?**

No. Materializa lo que hayas definido; limita y proyecta antes de ejecutar.

**¿Cuándo se calcula la lista y qué ocurriría si quitaras Distinct?**

ToArray enumera y produce el resultado; Distinct y OrderBy componen la secuencia antes. Sin Distinct, sala-sur aparece dos veces aunque esté ordenada.

**Cómo demostrar el objetivo:**

- Obtienes sala-norte y sala-sur una sola vez y en ese orden.
- Explicas qué operadores preparan la secuencia y qué operación la recorre.

**Qué debes recordar:**

- LINQ expresa transformaciones sobre secuencias.
- Materializar una consulta determina cuándo se obtiene su resultado.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**LINQ, ejecución diferida y cancelación**

LINQ compone filtrado, ordenamiento y proyección. IEnumerable trabaja con objetos enumerables; IQueryable conserva una expresión que el proveedor puede traducir. Where no garantiza que ya se haya ejecutado una consulta. ToList materializa en memoria; ToListAsync materializa una consulta compatible de EF Core. Agrega un orden estable antes de paginar, porque sin orden el resultado puede cambiar entre consultas.

async permite suspender una operación mientras espera I/O. No convierte automáticamente un cálculo de CPU en trabajo paralelo. Transmite el CancellationToken hasta el adaptador y usa await; bloquear con .Result puede desperdiciar hilos y complicar el diagnóstico. Una cancelación posterior al commit no deshace la operación: diseña reintentos e idempotencia para ese escenario. Task.WhenAll requiere operaciones independientes; no ejecutes consultas concurrentes sobre el mismo DbContext.

**Consulta de objetos, sin base de datos** · Fragmento sobre tipos del laboratorio

```C#
var activas = reservas
    .Where(r => r.GrupoId == grupo && !r.Cancelada)
    .OrderBy(r => r.Id)
    .Select(r => new { r.Id, r.SalaId })
    .Take(20)
    .ToList();
```

reservas es IEnumerable<Reserva> y grupo es un identificador validado. La proyección reduce los datos entregados; filtrar por grupo aquí no reemplaza autorización.

**Resultado:** Hasta 20 reservas activas del grupo, materializadas una vez.

### Tema 06 · async/await, I/O y CancellationToken

**Caso de uso · Cancelar una espera de aviso**

La persona abandona una solicitud mientras se prepara un aviso. El proceso debe observar la cancelación sin bloquear el hilo que atiende la operación.

Compararás await con una espera bloqueante y propagarás CancellationToken; distinguirás cancelar trabajo pendiente de deshacer cambios confirmados.

**Objetivo:** Esperar operaciones de I/O y propagar cancelación sin bloquear.

**Requisitos:** Colecciones, genéricos y LINQ.

#### 01. Qué es y qué problema resuelve

Una operación asíncrona devuelve una promesa de finalización. await permite esperar I/O sin bloquear el hilo durante la espera; no reduce por sí mismo el costo de CPU. La firma Task comunica que el resultado llega después y CancellationToken comunica una solicitud cooperativa de cancelación.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Ejemplo de consola .NET 10. Integra el fragmento en Program.cs o en sus tipos auxiliares según corresponda; no mezcles declaraciones de tipos antes de instrucciones de nivel superior. No requiere una API ni una base. Task.Delay simula una espera didáctica; no es un envío de aviso real. Ejecuta el fragmento en un programa de consola.

```C#
static async Task<string> PrepararAvisoAsync(CancellationToken ct)
{
    await Task.Delay(TimeSpan.FromMilliseconds(20), ct);
    return "Aviso preparado";
}
using var fuente = new CancellationTokenSource();
fuente.Cancel();
try { await PrepararAvisoAsync(fuente.Token); }
catch (OperationCanceledException) { Console.WriteLine("Cancelado"); }
```

**Cómo funciona:**

1. Observa el método que devuelve Task y recibe CancellationToken.
2. await permite esperar Task.Delay sin bloquear explícitamente el hilo.
3. Cancela desde el consumidor y comprueba cómo termina la operación.

**Resultado esperado:** Cancelado; no se obtiene el mensaje de éxito.

#### 03. Cuándo usarlo y qué debes evitar

Propaga el token desde HTTP hasta persistencia y servicios externos. Cancelar después de confirmar datos no revierte el commit; el cliente puede necesitar reconciliar o reintentar con idempotencia.

No uses async void para casos de uso. Task.WhenAll requiere independencia y recursos compatibles con concurrencia; un DbContext compartido no ofrece esa garantía.

**Error frecuente:** Cancelar es una solicitud cooperativa; no revierte cambios que la base de datos ya confirmó.

#### 04. Practica lo aprendido

Repite sin Cancel y compara. Evita .Result en ambos recorridos.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Repite sin Cancel y compara. Evita .Result en ambos recorridos.
3. Comprueba y registra el resultado: Cancelado; no se obtiene el mensaje de éxito.

**Solución orientativa:**

Sin cancelación, await obtiene Aviso preparado. Con token cancelado la operación lanza OperationCanceledException.

#### 05. Comprueba lo aprendido

**¿La cancelación es un rollback?**

No. Es una solicitud que cada operación debe observar; lo ya confirmado requiere otra política.

**¿Qué resultado esperas con el token cancelado y qué cambio confirmado no se deshace por ello?**

La operación observa el token y lanza OperationCanceledException en lugar de devolver el aviso. Cancelar la espera no revierte una reserva que ya se haya confirmado en la base.

**Cómo demostrar el objetivo:**

- Sin cancelar obtienes el mensaje de éxito; con el token cancelado observas OperationCanceledException.
- El recorrido usa await y propaga el token sin .Result ni .Wait().

**Qué debes recordar:**

- await espera sin usar .Result.
- La cancelación debe propagarse hasta las dependencias que la admiten.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**LINQ, ejecución diferida y cancelación**

LINQ compone filtrado, ordenamiento y proyección. IEnumerable trabaja con objetos enumerables; IQueryable conserva una expresión que el proveedor puede traducir. Where no garantiza que ya se haya ejecutado una consulta. ToList materializa en memoria; ToListAsync materializa una consulta compatible de EF Core. Agrega un orden estable antes de paginar, porque sin orden el resultado puede cambiar entre consultas.

async permite suspender una operación mientras espera I/O. No convierte automáticamente un cálculo de CPU en trabajo paralelo. Transmite el CancellationToken hasta el adaptador y usa await; bloquear con .Result puede desperdiciar hilos y complicar el diagnóstico. Una cancelación posterior al commit no deshace la operación: diseña reintentos e idempotencia para ese escenario. Task.WhenAll requiere operaciones independientes; no ejecutes consultas concurrentes sobre el mismo DbContext.

**Consulta de objetos, sin base de datos** · Fragmento sobre tipos del laboratorio

```C#
var activas = reservas
    .Where(r => r.GrupoId == grupo && !r.Cancelada)
    .OrderBy(r => r.Id)
    .Select(r => new { r.Id, r.SalaId })
    .Take(20)
    .ToList();
```

reservas es IEnumerable<Reserva> y grupo es un identificador validado. La proyección reduce los datos entregados; filtrar por grupo aquí no reemplaza autorización.

**Resultado:** Hasta 20 reservas activas del grupo, materializadas una vez.

### Tema 07 · Código legible, decisiones y revisión

**Caso de uso · Entender una regla de aforo**

Una función usa números y nombres ambiguos para decidir cuántas personas admite una reserva. Quien la revisa no sabe si el límite está incluido.

Convertirás la intención en nombres explícitos y revisarás los bordes con ejemplos pequeños y reproducibles.

**Objetivo:** Expresar una regla con nombres claros y resultados comprobables.

**Requisitos:** async/await, I/O y CancellationToken.

#### 01. Qué es y qué problema resuelve

La mantenibilidad empieza por poder comprender intención, efectos y límites. Nombres concretos y funciones con una responsabilidad coherente reducen el tiempo de revisión.

Una función corta todavía puede ser confusa si esconde una modificación o utiliza números sin significado.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Ejemplo de consola .NET 10. Integra el fragmento en Program.cs o en sus tipos auxiliares según corresponda; no mezcles declaraciones de tipos antes de instrucciones de nivel superior. No requiere una API ni una base. Una función pura expresa el límite; no conoce HTTP ni base de datos. El máximo es didáctico y necesita una fuente de configuración en una aplicación real.

```C#
const int MaximoAsistentesPorReserva = 12;
static bool AforoPermitido(int asistentes, int maximo) =>
    asistentes > 0 && asistentes <= maximo;
Console.WriteLine(AforoPermitido(13, MaximoAsistentesPorReserva));
```

**Cómo funciona:**

1. Lee el nombre de la función y qué significan sus parámetros.
2. Identifica los límites permitidos y las condiciones que rechazan la entrada.
3. Comprueba cero, el límite exacto y un valor superior.

**Resultado esperado:** False para 13; true para 1 y 12; false para 0.

#### 03. Cuándo usarlo y qué debes evitar

Antes de optimizar, conserva un escenario reproducible y una medición. Revisa entradas inválidas, reintentos y permisos junto al camino feliz.

Una regla que aparece en tres endpoints debería tener un propietario único. Registra por qué eliges un patrón y en qué condición dejaría de ayudar; esa decisión evita que el siguiente cambio copie complejidad innecesaria.

**Error frecuente:** Un número fijo didáctico no sustituye la fuente de configuración o regla de negocio del sistema real.

#### 04. Practica lo aprendido

Revisa una función de reserva y escribe una observación sobre intención, otra sobre efecto y otra sobre evidencia.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Revisa una función de reserva y escribe una observación sobre intención, otra sobre efecto y otra sobre evidencia.
3. Comprueba y registra el resultado: False para 13; true para 1 y 12; false para 0.

**Solución orientativa:**

Comprueba nombre, estado modificado y prueba del borde. Evita comentarios que repitan exactamente una línea de código.

#### 05. Comprueba lo aprendido

**¿Agregar comentarios compensa un contrato ambiguo?**

No. Primero corrige nombres, responsabilidades y garantías; documenta luego el razonamiento.

**¿Qué casos te permiten decidir si el límite de aforo está incluido?**

Prueba 0, 1, 12 y 13 con capacidad 12. Solo 1 y 12 deben aceptarse: ese contraste documenta el límite inclusivo y rechaza el grupo vacío.

**Cómo demostrar el objetivo:**

- La regla admite 1 y 12 asistentes, y rechaza 0 y 13 para capacidad 12.
- La observación editorial explica intención, efecto y evidencia sin repetir el código.

**Qué debes recordar:**

- Un nombre claro explica la intención.
- Los casos límite revelan errores que una entrada habitual no muestra.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Aplicar los cinco principios SOLID con criterio**

S: responsabilidad única significa una razón coherente de cambio. Reserva cambia por reglas del negocio; un serializador cambia por el formato HTTP. O: abierto/cerrado propone extender comportamiento estable mediante puntos de variación útiles; agrega un segundo canal de aviso sin reescribir CancelarReserva. L: sustitución exige respetar precondiciones, resultados y efectos del contrato; un repositorio alternativo no puede devolver éxito antes de guardar si el contrato promete persistencia.

I: segregación de interfaces evita que una consulta de disponibilidad dependa de métodos para borrar reservas. D: inversión de dependencias hace que el caso de uso conozca un contrato IReservas y que el adaptador de almacenamiento lo implemente. Inyección de dependencias es el mecanismo que entrega el objeto; inversión es la dirección de la dependencia. Se puede inyectar una dependencia concreta y seguir teniendo un acoplamiento incorrecto.

Usa composición para sumar capacidades independientes. Una jerarquía Sala → SalaVirtual puede ser problemática si su contrato exige aforo físico o dirección postal. Una colección de atributos o políticas expresaría mejor diferencias reales. No introduzcas fábricas, interfaces genéricas y herencia para anticipar variantes que todavía no existen. Explica qué cambio previsto justifica cada frontera.

**Un contrato acotado al caso de uso** · Contrato compilado en el laboratorio

```C#
public interface IReservas
{
    Task<Reserva?> BuscarAsync(Guid id, CancellationToken ct);
    Task GuardarAsync(CancellationToken ct);
}
```

La aplicación pide dos capacidades. La implementación puede usar una base de datos; el dominio no importa ese detalle. GuardarAsync representa la confirmación del trabajo pendiente en el ámbito.

**Resultado:** CancelarReserva se prueba con MemoriaReservas sin levantar HTTP.

## Ruta 02

### Tema 01 · REST: recursos, verbos y respuestas HTTP

**Caso de uso · Crear y consultar una reserva**

El portal necesita crear una reserva y luego consultar su resultado. Si la respuesta no identifica el recurso, el consumidor debe adivinar dónde encontrarlo.

Diseñarás métodos, rutas, estados y Location a partir del comportamiento; separarás el contrato HTTP de las tablas de almacenamiento.

**Objetivo:** Diseñar operaciones HTTP a partir de recursos y comportamientos.

**Requisitos:** Fundamentos de la ruta anterior.

#### 01. Qué es y qué problema resuelve

Una API modela recursos y ofrece operaciones con semántica predecible. La URI identifica una reserva; el verbo comunica intención.

GET no debería cambiarla. Crear mediante POST necesita explicar entrada, ubicación del resultado y efectos posibles.

La forma de una tabla no debe dictar los campos públicos.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Contrato o esquema de diseño para analizar; no es una aplicación ejecutable completa. La evidencia principal es la decisión y su justificación. Una comprobación automatizada requiere implementar el control en una solución preparada. Intercambio ilustrativo. La petición no permite escoger grupo, permisos ni estado de aprobación; el identificador de la respuesta se genera en servidor.

```HTTP
POST /api/v1/reservas
Content-Type: application/json

{"salaId":"sala-norte","inicio":"2026-10-20T12:00:00Z",
 "fin":"2026-10-20T13:00:00Z"}

HTTP/1.1 201 Created
Location: /api/v1/reservas/identificador-generado
```

**Cómo funciona:**

1. Lee el verbo y la URL para reconocer qué solicita el cliente.
2. Compara la respuesta 201 con el recurso que acaba de crearse.
3. Observa Location: permite consultar la nueva representación.

**Resultado esperado:** El consumidor conoce dónde consultar la reserva creada; el grupo se deriva de identidad validada.

#### 03. Cuándo usarlo y qué debes evitar

Diseña éxito y rechazo como parte del mismo contrato: 201 con Location al crear, 400 para entrada inválida, 401 para identidad no válida y 409 para conflicto con estado actual. Idempotencia describe efectos al repetir una intención; no significa que cualquier POST pueda reintentarse sin una clave.

Establece además qué recursos se ocultan mediante 404.

**Error frecuente:** Exponer cada tabla como endpoint puede producir un contrato que filtra detalles internos y no responde a las necesidades del cliente.

#### 04. Practica lo aprendido

Define contrato para consultar y cancelar, incluyendo ausencia y acceso ajeno.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Define contrato para consultar y cancelar, incluyendo ausencia y acceso ajeno.
3. Comprueba y registra el resultado: El consumidor conoce dónde consultar la reserva creada; el grupo se deriva de identidad validada.

**Solución orientativa:**

GET consulta sin efectos; cancelación especifica idempotencia. Autenticación y permiso se verifican antes de tocar estado.

#### 05. Comprueba lo aprendido

**¿REST exige exponer cada tabla como recurso?**

No. El contrato responde a necesidades del consumidor y al dominio.

**¿Cómo sabe el portal dónde consultar lo que acaba de crear?**

La respuesta de creación incluye Location con la URI de la reserva. El cliente consulta esa URI con GET; no necesita deducir una ruta a partir de tablas internas.

**Cómo demostrar el objetivo:**

- El contrato de creación devuelve 201 con una ubicación del recurso creado.
- Defines consulta y cancelación con sus resultados de ausencia y acceso denegado, sin efectos en GET.

**Qué debes recordar:**

- GET consulta sin efectos de negocio.
- Una creación debe comunicar su resultado y cómo localizar el recurso.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Contrato HTTP y DTOs explícitos**

Un contrato describe entradas, respuestas, errores y permisos. Para crear una reserva acepta sala e intervalo; obtiene el grupo del contexto autenticado. No aceptes un objeto completo Reserva con Cancelada, propietario o auditoría modificables desde el cliente. Usa DTOs de entrada y salida pequeños; el DTO es el límite de confianza del transporte.

POST /api/v1/reservas crea y responde 201 con Location; GET de una reserva devuelve 200 o 404 según la política de visibilidad. 400 expresa entrada mal formada, 401 ausencia de autenticación válida y 403 acceso no autorizado. 409 puede expresar conflicto con estado actual. Un 500 corresponde a un fallo inesperado. Elegir 404 para ocultar recursos ajenos es una política consistente que debes aplicar y probar, no un reemplazo de comprobar permisos.

La ruta v1 es una estrategia de versionado visible. Agregar un campo opcional puede ser compatible; renombrar salaId o cambiar unidades de duración puede romper clientes. Conserva contratos de ejemplo en OpenAPI, acuerda deprecaciones y verifica consumidores. Un DTO compartido entre versiones deja de ser conveniente si ambas necesitan evolucionar de manera diferente.

**Representar una petición sin privilegios** · Tipos completos para el adaptador HTTP

```C#
public sealed record CrearReservaDto(
    string SalaId,
    DateTimeOffset Inicio,
    DateTimeOffset Fin);
public sealed record ReservaCreadaDto(Guid Id);
```

Estos mensajes usan tipos conocidos por el serializador. El grupo se resuelve después de autenticar; no es un campo que el cliente pueda elegir.

**Resultado:** La entrada no permite asignar un grupo ajeno ni marcar la reserva como aprobada.

### Tema 02 · Minimal APIs: endpoints y delegación

**Caso de uso · Sacar una consulta del endpoint**

El endpoint de salas compara un código fijo. Al crecer el catálogo, mantener los datos dentro del handler hace difícil probar y cambiar la consulta.

Leerás un endpoint pequeño y extraerás su colaborador conservando la traducción de resultados a HTTP.

**Objetivo:** Delegar reglas de negocio desde un endpoint conciso.

**Requisitos:** REST: recursos, verbos y respuestas HTTP.

#### 01. Qué es y qué problema resuelve

Minimal APIs reduce la ceremonia para definir entrada HTTP. Esa concisión no justifica concentrar todas las reglas en una lambda.

Un endpoint puede recibir un DTO, extraer contexto confiable, delegar y traducir resultados. Mantén casos de uso accesibles sin necesidad de una petición real.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Los adaptadores y servicios nombrados son tipos del ejercicio que debes implementar. Fragmento en Program.cs de una API .NET 10. Catálogo ficticio fijo para estudiar routing; no representa autorización ni persistencia de producción.

```C#
app.MapGet("/api/v1/salas/{codigo}", (string codigo) =>
{
    if (codigo != "sala-norte") return Results.NotFound();
    return Results.Ok(new { codigo, aforo = 12 });
});
```

**Cómo funciona:**

1. MapGet vincula una URL con su manejador.
2. El manejador produce una representación y una respuesta HTTP.
3. Cuando la consulta crezca, extráela a un servicio sin trasladar allí los detalles de HTTP.

**Resultado esperado:** sala-norte obtiene 200 con aforo; otro código obtiene 404.

#### 03. Cuándo usarlo y qué debes evitar

La validación del transporte distingue JSON mal formado de una regla del negocio. Los servicios enlazados se resuelven por DI; sus ciclos de vida siguen importando aunque no haya un controlador.

Anota respuestas y contratos para documentación OpenAPI y verifica su comportamiento mediante pruebas del host, no solo mediante llamadas directas al handler.

**Error frecuente:** Una lambda corta puede seguir mezclando SQL, permisos y negocio; evalúa responsabilidades, no cantidad de líneas.

#### 04. Practica lo aprendido

Extrae el catálogo a un servicio e inyéctalo al endpoint.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Extrae el catálogo a un servicio e inyéctalo al endpoint.
3. Comprueba y registra el resultado: sala-norte obtiene 200 con aforo; otro código obtiene 404.

**Solución orientativa:**

El endpoint conserva traducción HTTP; la clase de catálogo conserva la consulta. Prueba ambas fronteras.

#### 05. Comprueba lo aprendido

**¿Usar Minimal APIs elimina la necesidad de capas?**

No. La entrada sigue necesitando límites de responsabilidades cuando la aplicación crece.

**¿Qué parte conservarías en el endpoint y qué parte moverías al catálogo?**

El endpoint conserva lectura del parámetro y traducción a 200 o 404. El catálogo decide si la sala existe y devuelve datos o ausencia sin utilizar Results.

**Cómo demostrar el objetivo:**

- El catálogo devuelve la sala conocida o ausencia sin depender de Results.
- El endpoint conserva 200 y 404 al delegar la consulta al servicio.

**Qué debes recordar:**

- El endpoint traduce entrada y salida HTTP.
- El caso de uso conserva la coordinación del negocio.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Composición, servicios y Minimal APIs**

Program.cs es la raíz de composición: registra servicios y conecta endpoints. El endpoint recibe una petición, resuelve identidad, invoca el caso de uso y traduce el resultado a HTTP. Evita consultas y reglas repetidas en lambdas extensas. Controladores son una alternativa válida cuando sus convenciones y filtros ayudan; Minimal APIs son útiles para entradas concisas. Ambos pueden respetar las mismas fronteras.

Singleton dura toda la aplicación y requiere estado seguro ante concurrencia; Scoped se comparte dentro de un ámbito; Transient crea una instancia en cada resolución. DbContext suele ser Scoped. Un servicio hospedado es Singleton: crea un ámbito por trabajo mediante IServiceScopeFactory y resuelve dependencias dentro de él. Nunca almacenes un DbContext en un singleton ni reutilices un ámbito indefinidamente.

**Cancelar desde una identidad validada** · Integración guiada, no incluida en la consola

```C#
builder.Services.AddScoped<CancelarReserva>();
// Registrar IReservas con un adaptador persistente por ámbito.

app.MapPost("/api/v1/reservas/{id:guid}/cancelacion",
    async (Guid id, ClaimsPrincipal usuario,
           CancelarReserva caso, CancellationToken ct) =>
    {
        var grupo = usuario.FindFirst("grupo_id")?.Value;
        var resultado = await caso.EjecutarAsync(id, grupo, ct);
        return resultado.Codigo switch
        {
            null => Results.NoContent(),
            "no_encontrada" => Results.NotFound(),
            "sin_permiso" => Results.Forbid(),
            _ => Results.Problem(statusCode: 500)
        };
    }).RequireAuthorization();
```

Fragmento para Program.cs con using System.Security.Claims y ASP.NET Core. Requiere configurar autenticación, autorización e IReservas. El claim solo es confiable después de validar la identidad del emisor.

**Resultado:** 204 al cancelar o repetir una cancelación propia; 403 para otro grupo; 404 para ausencia. No es una API completa.

### Tema 03 · Inyección de dependencias y composición

**Caso de uso · Construir un caso de uso con otro adaptador**

CancelarReserva necesita acceder a reservas. Queremos probar su regla con memoria y usar otro almacenamiento al iniciar la aplicación.

Harás explícita la dependencia del constructor y distinguirás la composición manual del registro en el contenedor.

**Objetivo:** Componer colaboradores y hacer explícitas las dependencias.

**Requisitos:** Minimal APIs: endpoints y delegación.

#### 01. Qué es y qué problema resuelve

La inyección entrega colaboradores desde fuera en lugar de construirlos dentro de cada consumidor. La raíz de composición conecta contratos con implementaciones.

El constructor hace visibles requisitos y facilita probar variantes sin alterar el caso de uso.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Los adaptadores y servicios nombrados son tipos del ejercicio que debes implementar. Fragmento de composición ASP.NET Core. No declares que funciona antes de implementar y registrar las dependencias del repositorio.

```C#
builder.Services.AddScoped<IReservas, RepositorioReservas>();
builder.Services.AddScoped<CancelarReserva>();
// CancelarReserva recibe IReservas por constructor.
// RepositorioReservas es el adaptador que debes implementar.
```

**Cómo funciona:**

1. Registra el contrato y la implementación en el contenedor.
2. El consumidor declara lo que necesita en su constructor o parámetro.
3. El contenedor entrega esa implementación dentro del ámbito configurado.

**Resultado esperado:** El contenedor puede crear el caso de uso dentro de un ámbito de petición.

#### 03. Cuándo usarlo y qué debes evitar

El contenedor no verifica que una interfaz tenga buen significado. Tampoco autoriza recursos ni crea automáticamente una transacción.

Registra dependencias con ámbito apropiado, evita resolver servicios arbitrariamente desde todo el código y falla al inicio cuando una dependencia obligatoria falta. Service locator suele ocultar lo que el consumidor necesita.

**Error frecuente:** Inyectar una dependencia no corrige una dirección de referencias equivocada entre capas.

#### 04. Practica lo aprendido

Construye CancelarReserva directamente con MemoriaReservas y luego compón un host con un adaptador por ámbito.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Construye CancelarReserva directamente con MemoriaReservas y luego compón un host con un adaptador por ámbito.
3. Comprueba y registra el resultado: El contenedor puede crear el caso de uso dentro de un ámbito de petición.

**Solución orientativa:**

La prueba de dominio usa el constructor; la prueba de host confirma registro y resolución. Son garantías distintas.

#### 05. Comprueba lo aprendido

**¿La inyección corrige dependencia de infraestructura en dominio?**

No. Corrige la dirección de referencias en el diseño, además de entregar instancias.

**¿Qué demuestra construir el objeto a mano y qué demuestra resolverlo desde el host?**

La construcción manual comprueba que el caso de uso recibe un contrato explícito y funciona con el adaptador elegido. Resolverlo en el host comprueba además registros y ámbitos del contenedor.

**Cómo demostrar el objetivo:**

- Construyes el caso de uso con un adaptador de memoria sin iniciar HTTP.
- En un host preparado, el contenedor resuelve el contrato y el caso de uso dentro de un ámbito.

**Qué debes recordar:**

- DI entrega instancias.
- El diseño determina qué dependencia debe conocer cada componente.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Composición, servicios y Minimal APIs**

Program.cs es la raíz de composición: registra servicios y conecta endpoints. El endpoint recibe una petición, resuelve identidad, invoca el caso de uso y traduce el resultado a HTTP. Evita consultas y reglas repetidas en lambdas extensas. Controladores son una alternativa válida cuando sus convenciones y filtros ayudan; Minimal APIs son útiles para entradas concisas. Ambos pueden respetar las mismas fronteras.

Singleton dura toda la aplicación y requiere estado seguro ante concurrencia; Scoped se comparte dentro de un ámbito; Transient crea una instancia en cada resolución. DbContext suele ser Scoped. Un servicio hospedado es Singleton: crea un ámbito por trabajo mediante IServiceScopeFactory y resuelve dependencias dentro de él. Nunca almacenes un DbContext en un singleton ni reutilices un ámbito indefinidamente.

**Cancelar desde una identidad validada** · Integración guiada, no incluida en la consola

```C#
builder.Services.AddScoped<CancelarReserva>();
// Registrar IReservas con un adaptador persistente por ámbito.

app.MapPost("/api/v1/reservas/{id:guid}/cancelacion",
    async (Guid id, ClaimsPrincipal usuario,
           CancelarReserva caso, CancellationToken ct) =>
    {
        var grupo = usuario.FindFirst("grupo_id")?.Value;
        var resultado = await caso.EjecutarAsync(id, grupo, ct);
        return resultado.Codigo switch
        {
            null => Results.NoContent(),
            "no_encontrada" => Results.NotFound(),
            "sin_permiso" => Results.Forbid(),
            _ => Results.Problem(statusCode: 500)
        };
    }).RequireAuthorization();
```

Fragmento para Program.cs con using System.Security.Claims y ASP.NET Core. Requiere configurar autenticación, autorización e IReservas. El claim solo es confiable después de validar la identidad del emisor.

**Resultado:** 204 al cancelar o repetir una cancelación propia; 403 para otro grupo; 404 para ausencia. No es una API completa.

### Tema 04 · Transient, Scoped y Singleton

**Caso de uso · Evitar compartir estado entre solicitudes**

El catálogo de salas es inmutable, pero la unidad de trabajo acumula cambios de una operación. Compartir ambos de la misma forma puede mezclar solicitudes.

Compararás Transient, Scoped y Singleton mediante identidad de instancias y revisarás qué estado puede conservar cada servicio.

**Objetivo:** Elegir el tiempo de vida de un servicio según su estado y uso.

**Requisitos:** Inyección de dependencias y composición.

#### 01. Qué es y qué problema resuelve

El ciclo de vida define cuándo se crea y comparte una instancia. Transient se resuelve de nuevo, Scoped se comparte dentro de un ámbito y Singleton dura el contenedor.

Es una decisión de propiedad del estado, no una etiqueta de rendimiento.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Los adaptadores y servicios nombrados son tipos del ejercicio que debes implementar. Clases representativas que debes declarar. El catálogo singleton debe ser inmutable o proteger sus cambios; UnidadReservas puede coordinar un contexto por ámbito.

```C#
builder.Services.AddTransient<FormateadorReserva>();
builder.Services.AddScoped<UnidadReservas>();
builder.Services.AddSingleton<CatalogoInmutable>();
```

**Cómo funciona:**

1. Compara los registros Transient, Scoped y Singleton.
2. Resuelve dos instancias dentro del mismo ámbito y compara su identidad.
3. Crea un segundo ámbito para observar qué instancias se comparten y cuáles cambian.

**Resultado esperado:** Dos resoluciones scoped en el mismo ámbito comparten instancia; en ámbitos distintos no.

#### 03. Cuándo usarlo y qué debes evitar

Una dependencia capturada por un singleton puede durar más de lo previsto. No guardes un DbContext de petición en un worker singleton; crea un ámbito por trabajo.

Un servicio transient tampoco se vuelve seguro para varios hilos si sus dependencias tienen estado compartido. Prueba identidad de instancias dentro y fuera de ámbitos.

**Error frecuente:** Un singleton que captura un servicio scoped puede conservar estado que debía vivir solo durante un ámbito.

#### 04. Practica lo aprendido

Predice y comprueba la identidad de dos resoluciones por ámbito para cada registro.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Predice y comprueba la identidad de dos resoluciones por ámbito para cada registro.
3. Comprueba y registra el resultado: Dos resoluciones scoped en el mismo ámbito comparten instancia; en ámbitos distintos no.

**Solución orientativa:**

Transient cambia en cada resolución; Scoped cambia entre ámbitos; Singleton se mantiene. Comprueba también que no captura Scoped.

#### 05. Comprueba lo aprendido

**¿Scoped significa necesariamente una petición HTTP?**

No. Significa un ámbito; HTTP suele crear uno por petición, y un worker puede crear otro explícitamente.

**¿Por qué el catálogo y la unidad de trabajo necesitan decisiones de vida distintas?**

Un catálogo verdaderamente inmutable puede compartirse; la unidad de trabajo tiene estado de una operación y necesita un ámbito propio. Un Singleton no debe capturar esa unidad Scoped.

**Cómo demostrar el objetivo:**

- Predices y registras dos resoluciones dentro de un ámbito y en otro ámbito.
- Detectas el riesgo de capturar un servicio Scoped desde un Singleton.

**Qué debes recordar:**

- Transient se crea por resolución; Scoped se comparte por ámbito.
- Singleton se comparte durante la vida del contenedor.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Composición, servicios y Minimal APIs**

Program.cs es la raíz de composición: registra servicios y conecta endpoints. El endpoint recibe una petición, resuelve identidad, invoca el caso de uso y traduce el resultado a HTTP. Evita consultas y reglas repetidas en lambdas extensas. Controladores son una alternativa válida cuando sus convenciones y filtros ayudan; Minimal APIs son útiles para entradas concisas. Ambos pueden respetar las mismas fronteras.

Singleton dura toda la aplicación y requiere estado seguro ante concurrencia; Scoped se comparte dentro de un ámbito; Transient crea una instancia en cada resolución. DbContext suele ser Scoped. Un servicio hospedado es Singleton: crea un ámbito por trabajo mediante IServiceScopeFactory y resuelve dependencias dentro de él. Nunca almacenes un DbContext en un singleton ni reutilices un ámbito indefinidamente.

**Cancelar desde una identidad validada** · Integración guiada, no incluida en la consola

```C#
builder.Services.AddScoped<CancelarReserva>();
// Registrar IReservas con un adaptador persistente por ámbito.

app.MapPost("/api/v1/reservas/{id:guid}/cancelacion",
    async (Guid id, ClaimsPrincipal usuario,
           CancelarReserva caso, CancellationToken ct) =>
    {
        var grupo = usuario.FindFirst("grupo_id")?.Value;
        var resultado = await caso.EjecutarAsync(id, grupo, ct);
        return resultado.Codigo switch
        {
            null => Results.NoContent(),
            "no_encontrada" => Results.NotFound(),
            "sin_permiso" => Results.Forbid(),
            _ => Results.Problem(statusCode: 500)
        };
    }).RequireAuthorization();
```

Fragmento para Program.cs con using System.Security.Claims y ASP.NET Core. Requiere configurar autenticación, autorización e IReservas. El claim solo es confiable después de validar la identidad del emisor.

**Resultado:** 204 al cancelar o repetir una cancelación propia; 403 para otro grupo; 404 para ausencia. No es una API completa.

### Tema 05 · Middleware y orden del pipeline

**Caso de uso · Autorizar después de autenticar**

Una solicitud de cancelación llega al pipeline. El control de permisos necesita una identidad validada, y un fallo posterior necesita un manejador que lo observe.

Seguirás el recorrido de ida y vuelta para comprender cómo el orden determina controles y manejo de errores.

**Objetivo:** Explicar cómo el orden del pipeline afecta una solicitud.

**Requisitos:** Transient, Scoped y Singleton.

#### 01. Qué es y qué problema resuelve

Cada middleware puede trabajar antes y después del siguiente componente, o detener el recorrido. La respuesta vuelve por la cadena en orden inverso.

Ese modelo explica por qué el manejo de excepciones debe envolver los componentes que pretende atender.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Registra ProblemDetails, autenticación, autorización y CORS con la política Portal antes de usar este orden de middleware. Requiere servicios de errores, CORS e identidad registrados. Es un orden ilustrativo; configura el limitador según su estrategia de partición.

```C#
app.UseExceptionHandler();
app.UseRouting();
app.UseCors("Portal");
app.UseAuthentication();
app.UseAuthorization();
// Mapear endpoints después de configurar el pipeline.
```

**Cómo funciona:**

1. Lee los middleware de arriba hacia abajo: ese es el recorrido de entrada.
2. Identifica dónde se establece la identidad y dónde se decide el acceso.
3. Sigue el retorno de la respuesta y comprueba qué middleware puede tratar errores.

**Resultado esperado:** La identidad existe al evaluar permisos; el manejo de errores envuelve componentes posteriores.

#### 03. Cuándo usarlo y qué debes evitar

Autenticación establece identidad y autorización usa esa identidad. Un limitador por usuario necesita ejecutarse después de autenticar.

CORS debe ubicarse conforme al routing y la autorización. Un middleware que registra cuerpos completos para depurar puede exponer credenciales; decide propiedades concretas y tiempo de retención.

**Error frecuente:** Colocar autorización antes de autenticación impide evaluar correctamente una identidad todavía no establecida.

#### 04. Practica lo aprendido

Dibuja el recorrido de una petición válida y otra que falla en el caso de uso.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Dibuja el recorrido de una petición válida y otra que falla en el caso de uso.
3. Comprueba y registra el resultado: La identidad existe al evaluar permisos; el manejo de errores envuelve componentes posteriores.

**Solución orientativa:**

La válida cruza controles y vuelve; la inesperada alcanza el manejador superior. Una denegación prevista no necesita una excepción.

#### 05. Comprueba lo aprendido

**¿El orden de registro es solo una preferencia de estilo?**

No. Cambia los controles y qué fallos puede observar cada componente.

**¿Qué se pierde al evaluar permisos antes de establecer la identidad?**

La autorización no dispone todavía de una identidad autenticada para evaluar permisos. Establece la identidad antes y conserva el manejador de errores envolviendo los componentes posteriores.

**Cómo demostrar el objetivo:**

- El diagrama coloca autenticación antes de autorización.
- Distingues una denegación prevista de una excepción posterior capturada por el manejador.

**Qué debes recordar:**

- El orden de middleware tiene efectos funcionales.
- La respuesta recorre el pipeline en sentido inverso.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Pipeline y configuración validada al inicio**

El middleware procesa la petición en orden y la respuesta de vuelta. El manejador de errores debe envolver componentes posteriores; autenticación identifica al actor y autorización evalúa acceso. Configura CORS antes de autorización cuando haya clientes de navegador. El limitador por usuario necesita que autenticación haya establecido la identidad. HTTPS y cabeceras de proxy requieren conocer la topología: confía únicamente en proxies configurados.

Options agrupa configuración tipada. Valida valores imprescindibles al inicio para evitar que una política defectuosa aparezca en mitad de una solicitud. IOptions sirve para valores estables, IOptionsSnapshot permite una vista por ámbito e IOptionsMonitor cambios observables. No asumas que todos los proveedores recargan valores. Variables de entorno, archivos y gestores de secretos tienen precedencia: documenta cuál domina cada configuración.

**Límite de duración verificable** · Integración con Options de ASP.NET Core

```C#
public sealed class ReservaOpciones
{
    public int MaximoMinutos { get; init; } = 180;
}
// En Program.cs, antes de Build:
builder.Services.AddOptions<ReservaOpciones>()
    .BindConfiguration("Reservas")
    .Validate(o => o.MaximoMinutos is >= 15 and <= 480,
        "MaximoMinutos fuera del rango permitido")
    .ValidateOnStart();
```

El fallo ocurre al arrancar, antes de aceptar tráfico. Para secretos usa un proveedor apropiado al ambiente; el archivo de configuración no es un lugar para claves o tokens.

**Resultado:** La aplicación rechaza MaximoMinutos=0 y acepta 180.

### Tema 06 · Ambientes, configuración y secretos

**Caso de uso · Cambiar el límite según el ambiente**

En desarrollo una reserva puede durar 180 minutos; en otro ambiente el límite es 120. No queremos recompilar ni guardar credenciales en el repositorio.

Resolverás un valor desde proveedores de configuración y documentarás su precedencia, separando parámetros de secretos.

**Objetivo:** Separar configuración de código y mantener secretos fuera del repositorio.

**Requisitos:** Middleware y orden del pipeline.

#### 01. Qué es y qué problema resuelve

La configuración combina proveedores con precedencia. Un archivo puede definir valores por defecto y una variable de entorno sustituirlos.

Development y Production son nombres de ambiente, no fronteras de seguridad. Un secreto no queda protegido porque esté en un archivo con un nombre distinto.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. JSON ilustrativo con un emisor ficticio. El doble guion bajo expresa un nivel de configuración en variables de entorno; no incluye secretos.

```JSON
{
  "Reservas": { "MaximoMinutos": 180 },
  "Identidad": { "Emisor": "https://identidad.example" }
}
// Variable de entorno equivalente al límite:
// Reservas__MaximoMinutos=120
```

**Cómo funciona:**

1. Localiza la configuración de ejemplo y las claves de cada sección.
2. Identifica qué proveedor puede reemplazar un valor según el ambiente.
3. Comprueba que una credencial se obtiene de un mecanismo de secretos, sin imprimirla.

**Resultado esperado:** Si el proveedor de entorno domina el archivo, el límite efectivo es 120.

#### 03. Cuándo usarlo y qué debes evitar

Usa valores ficticios en ejemplos y suministra credenciales por un mecanismo apropiado al entorno. User Secrets sirve para desarrollo local y no cifra por sí mismo un almacén productivo.

Documenta qué proveedor domina cada valor, valida configuración crítica y evita escribir la configuración completa en logs.

**Error frecuente:** Una variable de ambiente puede contener un secreto: mostrar toda la configuración en logs puede divulgarlo.

#### 04. Practica lo aprendido

Compara el límite 180 del archivo con 120 del entorno. Documenta la precedencia y el valor final. La validación tipada se implementará en el siguiente tema.

1. Identifica los proveedores de un host de prueba y su orden.
2. Define Reservas__MaximoMinutos=120 sin incluir credenciales.
3. Lee el valor efectivo y registra qué proveedor lo determinó.

**Solución orientativa:**

Con la configuración habitual del host, el entorno sobrescribe el archivo y se obtiene 120. No se ha implementado todavía validación del rango.

#### 05. Comprueba lo aprendido

**¿Un archivo appsettings.Production.json es un gestor de secretos?**

No. Sigue siendo un archivo; requiere controles y una estrategia separada para credenciales.

**¿Qué valor usaría la aplicación con el archivo en 180 y el entorno en 120?**

Con el proveedor de entorno posterior al archivo, el resultado es 120. La precedencia pertenece a la configuración del host; comprueba el orden real si lo modificaste.

**Cómo demostrar el objetivo:**

- Identificas 120 como valor efectivo cuando el entorno tiene precedencia sobre el archivo.
- Documentas dónde se define el límite y cómo protegerías las credenciales sin imprimirlas.

**Qué debes recordar:**

- El ambiente modifica configuración, no el código de negocio.
- Los secretos requieren almacenamiento y acceso adecuados.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Pipeline y configuración validada al inicio**

El middleware procesa la petición en orden y la respuesta de vuelta. El manejador de errores debe envolver componentes posteriores; autenticación identifica al actor y autorización evalúa acceso. Configura CORS antes de autorización cuando haya clientes de navegador. El limitador por usuario necesita que autenticación haya establecido la identidad. HTTPS y cabeceras de proxy requieren conocer la topología: confía únicamente en proxies configurados.

Options agrupa configuración tipada. Valida valores imprescindibles al inicio para evitar que una política defectuosa aparezca en mitad de una solicitud. IOptions sirve para valores estables, IOptionsSnapshot permite una vista por ámbito e IOptionsMonitor cambios observables. No asumas que todos los proveedores recargan valores. Variables de entorno, archivos y gestores de secretos tienen precedencia: documenta cuál domina cada configuración.

**Límite de duración verificable** · Integración con Options de ASP.NET Core

```C#
public sealed class ReservaOpciones
{
    public int MaximoMinutos { get; init; } = 180;
}
// En Program.cs, antes de Build:
builder.Services.AddOptions<ReservaOpciones>()
    .BindConfiguration("Reservas")
    .Validate(o => o.MaximoMinutos is >= 15 and <= 480,
        "MaximoMinutos fuera del rango permitido")
    .ValidateOnStart();
```

El fallo ocurre al arrancar, antes de aceptar tráfico. Para secretos usa un proveedor apropiado al ambiente; el archivo de configuración no es un lugar para claves o tokens.

**Resultado:** La aplicación rechaza MaximoMinutos=0 y acepta 180.

### Tema 07 · Options Pattern y validación al inicio

**Caso de uso · Detectar un límite inválido al arrancar**

Un cambio de configuración deja el máximo de reserva en 14 minutos, fuera del rango admitido. Descubrirlo con tráfico real produciría fallos difíciles de explicar.

Vincularás configuración a opciones tipadas y usarás validación al inicio para detectar valores fuera de 15 a 480.

**Objetivo:** Validar configuración tipada antes de atender solicitudes.

**Requisitos:** Ambientes, configuración y secretos.

#### 01. Qué es y qué problema resuelve

Options agrupa valores relacionados en un contrato tipado. El consumidor puede conocer una política de reservas en lugar de buscar claves arbitrarias por toda la aplicación.

Valida rangos y relaciones entre valores para rechazar una configuración incoherente antes de aceptar tráfico.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Define ReservaOpciones con MaximoMinutos y su sección Reservas. ReservaOpciones tiene MaximoMinutos. Este registro tipado necesita la sección de configuración y el consumidor correspondiente.

```C#
builder.Services.AddOptions<ReservaOpciones>()
    .BindConfiguration("Reservas")
    .Validate(o => o.MaximoMinutos >= 15 && o.MaximoMinutos <= 480)
    .ValidateOnStart();
```

**Cómo funciona:**

1. Relaciona la sección de configuración con el tipo de Options.
2. Lee la regla de validación y qué entrada considera inválida.
3. ValidateOnStart permite detectar ese fallo al iniciar la aplicación.

**Resultado esperado:** Un límite fuera de rango impide un arranque válido.

#### 03. Cuándo usarlo y qué debes evitar

IOptions representa valores estables; IOptionsSnapshot resuelve una vista por ámbito; IOptionsMonitor permite observar cambios. Recarga depende del proveedor y de cómo se consuma el valor.

Si una política cambia en mitad de una operación, conserva una instantánea coherente para esa operación y registra la versión de política si la decisión necesita auditoría.

**Error frecuente:** Registrar Options sin validar puede trasladar un error de configuración hasta la primera solicitud real.

#### 04. Practica lo aprendido

Prueba 14, 15, 480 y 481 como valores de configuración.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Prueba 14, 15, 480 y 481 como valores de configuración.
3. Comprueba y registra el resultado: Un límite fuera de rango impide un arranque válido.

**Solución orientativa:**

14 y 481 se rechazan; 15 y 480 se aceptan. La evidencia pertenece al arranque, no solo a una función aislada.

#### 05. Comprueba lo aprendido

**¿IOptionsMonitor garantiza recarga de cualquier archivo o secreto?**

No. El proveedor y la política de actualización determinan cuándo se observan cambios.

**¿Por qué probar la expresión no demuestra que el host valida sus opciones al arrancar?**

La expresión solo prueba el rango. ValidateOnStart debe estar registrado y ejecutarse durante el arranque del host; ese recorrido puede faltar aunque la expresión sea correcta.

**Cómo demostrar el objetivo:**

- 14 y 481 fallan; 15 y 480 pasan la regla de configuración.
- Compruebas el arranque del host preparado y distingues ese resultado de probar solo una expresión.

**Qué debes recordar:**

- Options agrupa configuración con un contrato.
- La validación al inicio descubre fallos antes de servir tráfico.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Pipeline y configuración validada al inicio**

El middleware procesa la petición en orden y la respuesta de vuelta. El manejador de errores debe envolver componentes posteriores; autenticación identifica al actor y autorización evalúa acceso. Configura CORS antes de autorización cuando haya clientes de navegador. El limitador por usuario necesita que autenticación haya establecido la identidad. HTTPS y cabeceras de proxy requieren conocer la topología: confía únicamente en proxies configurados.

Options agrupa configuración tipada. Valida valores imprescindibles al inicio para evitar que una política defectuosa aparezca en mitad de una solicitud. IOptions sirve para valores estables, IOptionsSnapshot permite una vista por ámbito e IOptionsMonitor cambios observables. No asumas que todos los proveedores recargan valores. Variables de entorno, archivos y gestores de secretos tienen precedencia: documenta cuál domina cada configuración.

**Límite de duración verificable** · Integración con Options de ASP.NET Core

```C#
public sealed class ReservaOpciones
{
    public int MaximoMinutos { get; init; } = 180;
}
// En Program.cs, antes de Build:
builder.Services.AddOptions<ReservaOpciones>()
    .BindConfiguration("Reservas")
    .Validate(o => o.MaximoMinutos is >= 15 and <= 480,
        "MaximoMinutos fuera del rango permitido")
    .ValidateOnStart();
```

El fallo ocurre al arrancar, antes de aceptar tráfico. Para secretos usa un proveedor apropiado al ambiente; el archivo de configuración no es un lugar para claves o tokens.

**Resultado:** La aplicación rechaza MaximoMinutos=0 y acepta 180.

### Tema 08 · Versionado y evolución de contratos

**Caso de uso · Evolucionar el campo de capacidad**

Un consumidor ya lee aforo. El equipo quiere reemplazarlo por capacidad.personas, pero todavía no puede actualizar todos los clientes.

Compararás dos contratos y clasificarás cambios según lo que esperan sus consumidores, definiendo convivencia y retiro de versiones.

**Objetivo:** Evolucionar un contrato sin romper consumidores existentes.

**Requisitos:** Options Pattern y validación al inicio.

#### 01. Qué es y qué problema resuelve

Versionar hace visible que dos consumidores pueden depender de contratos diferentes. Un cambio incompatible exige una transición deliberada: renombrar una propiedad, cambiar su significado o volver obligatorio un campo afecta clientes aunque la base de datos siga funcionando.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Endpoints ilustrativos de un catálogo fijo. v2 cambia la estructura y conserva v1 durante la transición; no usa una biblioteca de versionado.

```C#
app.MapGet("/api/v1/salas/{codigo}", (string codigo) =>
    Results.Ok(new { codigo, aforo = 12 }));
app.MapGet("/api/v2/salas/{codigo}", (string codigo) =>
    Results.Ok(new { codigo, capacidad = new { personas = 12 } }));
```

**Cómo funciona:**

1. Compara los contratos de la versión inicial y la nueva.
2. Identifica qué campos o comportamientos cambian para el consumidor.
3. Mantén pruebas de ambos contratos durante el período de transición.

**Resultado esperado:** Un cliente de v1 sigue recibiendo aforo; v2 obtiene capacidad.personas.

#### 03. Cuándo usarlo y qué debes evitar

Usa una estrategia consistente, como versión en la ruta. No prometas compatibilidad solo porque JSON acepta campos adicionales: algunos clientes validan esquemas o enums estrictamente.

Publica fecha y condiciones de retiro, conserva pruebas con consumidores representativos y distingue la versión HTTP de la versión del paquete interno.

**Error frecuente:** Añadir un campo no siempre es compatible: algunos clientes validan esquemas o valores de forma estricta.

#### 04. Practica lo aprendido

Clasifica agregar un campo opcional, renombrar aforo y cambiar minutos por horas.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Clasifica agregar un campo opcional, renombrar aforo y cambiar minutos por horas.
3. Comprueba y registra el resultado: Un cliente de v1 sigue recibiendo aforo; v2 obtiene capacidad.personas.

**Solución orientativa:**

Evalúa consumidores: renombrar y cambiar unidades son incompatibles. Un campo adicional depende de sus reglas de aceptación.

#### 05. Comprueba lo aprendido

**¿La versión de EF Core es la versión de tu API?**

No. Son contratos distintos con consumidores y políticas de evolución diferentes.

**¿Cómo introducirías capacidad.personas mientras existe un cliente que lee aforo?**

Conserva el contrato v1 mientras introduces v2. Prueba ambos con consumidores representativos y acuerda el retiro de v1; renombrar el campo en la misma versión rompe al cliente antiguo.

**Cómo demostrar el objetivo:**

- El consumidor de v1 conserva aforo y el de v2 recibe capacidad.personas.
- Justificas por qué renombrar o cambiar unidades puede romper un consumidor.

**Qué debes recordar:**

- Versiona el contrato observable.
- Define transición y retiro con evidencia de compatibilidad.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Contrato HTTP y DTOs explícitos**

Un contrato describe entradas, respuestas, errores y permisos. Para crear una reserva acepta sala e intervalo; obtiene el grupo del contexto autenticado. No aceptes un objeto completo Reserva con Cancelada, propietario o auditoría modificables desde el cliente. Usa DTOs de entrada y salida pequeños; el DTO es el límite de confianza del transporte.

POST /api/v1/reservas crea y responde 201 con Location; GET de una reserva devuelve 200 o 404 según la política de visibilidad. 400 expresa entrada mal formada, 401 ausencia de autenticación válida y 403 acceso no autorizado. 409 puede expresar conflicto con estado actual. Un 500 corresponde a un fallo inesperado. Elegir 404 para ocultar recursos ajenos es una política consistente que debes aplicar y probar, no un reemplazo de comprobar permisos.

La ruta v1 es una estrategia de versionado visible. Agregar un campo opcional puede ser compatible; renombrar salaId o cambiar unidades de duración puede romper clientes. Conserva contratos de ejemplo en OpenAPI, acuerda deprecaciones y verifica consumidores. Un DTO compartido entre versiones deja de ser conveniente si ambas necesitan evolucionar de manera diferente.

**Representar una petición sin privilegios** · Tipos completos para el adaptador HTTP

```C#
public sealed record CrearReservaDto(
    string SalaId,
    DateTimeOffset Inicio,
    DateTimeOffset Fin);
public sealed record ReservaCreadaDto(Guid Id);
```

Estos mensajes usan tipos conocidos por el serializador. El grupo se resuelve después de autenticar; no es un campo que el cliente pueda elegir.

**Resultado:** La entrada no permite asignar un grupo ajeno ni marcar la reserva como aprobada.

## Ruta 03

### Tema 01 · EF Core: DbContext, tracking y CRUD

**Caso de uso · Confirmar una cancelación en la base**

La API marca una reserva como cancelada en memoria. El portal parece correcto, pero necesitamos saber si el cambio quedó persistido y pertenece al grupo autorizado.

Relacionarás consulta, seguimiento y SaveChangesAsync; comprobarás el resultado leyendo con otro contexto.

**Objetivo:** Reconocer el papel del contexto, el seguimiento y la confirmación de cambios.

**Requisitos:** Fundamentos de la ruta anterior.

#### 01. Qué es y qué problema resuelve

DbContext coordina entidades rastreadas y cambios pendientes. Agregar una entidad no confirma datos hasta guardar; consultar con seguimiento permite detectar modificaciones posteriores.

Ese ámbito no debe durar indefinidamente: el contexto acumula estado y no es seguro para operaciones concurrentes.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un proyecto con EF Core 10, proveedor elegido, DbContext y entidades configurados. db, ct e identificadores representan dependencias del caso. El ZIP de consola no contiene esta integración. Verifica el esquema y las garantías con el proveedor. Fragmento EF con DbSet<ReservaFila>. grupo es contexto validado; un adaptador real define la política de ausencia y conflicto.

```C#
var fila = await db.Reservas
    .SingleOrDefaultAsync(r => r.Id == id && r.GrupoId == grupo, ct);
if (fila is null) return Results.NotFound();
fila.Cancelada = true;
await db.SaveChangesAsync(ct);
```

**Cómo funciona:**

1. Obtén la entidad mediante el contexto usado por el caso de uso.
2. Modifica su estado: el seguimiento conserva esa intención de cambio.
3. SaveChangesAsync intenta persistirla y debe propagar la cancelación.

**Resultado esperado:** Se modifica una fila autorizada o no se modifica ninguna.

#### 03. Cuándo usarlo y qué debes evitar

CRUD describe operaciones básicas, pero cada escritura sigue siendo un caso de uso con permisos y reglas. Comprueba que la fila pertenece al ámbito autorizado antes de modificar.

SaveChangesAsync confirma el conjunto pendiente; no lo ejecutes por cada propiedad. Para leer sin cambiar, usa proyección y seguimiento apropiado.

**Error frecuente:** Modificar un objeto no significa que el cambio se haya confirmado; comprueba el resultado de persistencia.

#### 04. Practica lo aprendido

En una API de pruebas con contexto, modelo y proveedor preparados, cancela una reserva del grupo autorizado y consulta su estado con un contexto nuevo. Incluye identificador ausente o grupo ajeno.

1. Prepara datos de dos grupos en una base aislada; el ZIP de consola no incluye este contexto.
2. Ejecuta el fragmento para una reserva propia y para una que no coincide con el grupo.
3. Con otro contexto verifica Cancelada y la ausencia de cambios en la reserva ajena.

**Solución orientativa:**

SaveChangesAsync confirma la modificación autorizada. El objeto rastreado no prueba persistencia; la lectura con un contexto nuevo aporta esa evidencia. Sin coincidencia, el fragmento devuelve ausencia y no guarda.

#### 05. Comprueba lo aprendido

**¿AddAsync equivale a insertar y confirmar?**

No. Agrega al trabajo pendiente; la confirmación ocurre al guardar.

**¿Por qué inspeccionar el objeto modificado no demuestra persistencia?**

El seguimiento conserva cambios pendientes en memoria. Guardar y leer con un contexto nuevo evita confundir ese estado local con una escritura realmente confirmada.

**Cómo demostrar el objetivo:**

- La consulta filtra identificador y grupo, y la ausencia no produce escritura.
- Un contexto nuevo confirma Cancelada después de guardar en la base de pruebas.

**Qué debes recordar:**

- DbContext coordina consultas y cambios.
- SaveChanges confirma el trabajo pendiente en su alcance.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Modelar persistencia, relaciones y migraciones**

EF Core rastrea cambios de entidades y traduce expresiones de consulta. El modelo persistido puede diferir del dominio: este curso usa una fila ReservaFila con propiedades simples para explicar SQL sin comprometer Intervalo. Un adaptador traduce entre ambos. Sala y Grupo tienen relación uno a muchos con Reserva; elige claves y comportamiento de borrado explícitos. Borrar una sala no debería borrar silenciosamente la historia de sus reservas.

Una migración contiene cambios de esquema y puede afectar datos previos. Revisa columnas obligatorias, valores iniciales, índices y operaciones destructivas; ensaya con una copia representativa y mide duración. Generar una migración no demuestra que sea segura. Aplica cambios compatibles antes de desplegar consumidores nuevos cuando convivan versiones. La reversión de código no revierte automáticamente un cambio de datos.

Usa una clave primaria para identidad, índices para consultas reales y restricciones para reglas expresables en el motor. Un índice único (SalaId, Inicio) solo impide dos inicios idénticos: no evita solapamientos arbitrarios. Un token de concurrencia solo protege modificaciones de una fila conocida. Explica esa diferencia antes de afirmar que la agenda es consistente.

**Fila explícita y versión de aplicación** · Integración EF Core 10; requiere DbContext y proveedor

```C#
public sealed class ReservaFila
{
    public Guid Id { get; set; }
    public string GrupoId { get; set; } = "";
    public string SalaId { get; set; } = "";
    public DateTimeOffset Inicio { get; set; }
    public DateTimeOffset Fin { get; set; }
    public bool Cancelada { get; set; }
    public Guid Version { get; set; }
}
// En OnModelCreating:
var reserva = modelBuilder.Entity<ReservaFila>();
reserva.HasKey(r => r.Id);
reserva.Property(r => r.GrupoId).HasMaxLength(80).IsRequired();
reserva.Property(r => r.Version).IsConcurrencyToken();
reserva.HasIndex(r => new { r.GrupoId, r.Inicio, r.Id });
```

ReservaFila es un modelo pedagógico independiente de Reserva. Los tiempos, índices y SQL deben verificarse con el proveedor elegido; no todos traducen DateTimeOffset de la misma forma.

**Resultado:** El modelo trata Version como token; la aplicación debe renovarlo al actualizar.

### Tema 02 · Fluent API: restricciones y mapeo de entidades

**Caso de uso · Expresar restricciones del almacenamiento**

SalaId no puede estar vacío ni superar 80 caracteres. Además queremos consultar por grupo y horario y detectar actualizaciones con una versión antigua.

Traducirás decisiones a claves, propiedades, índices y tokens en Fluent API, y revisarás qué produce el proveedor.

**Objetivo:** Definir claves, límites e índices con intención explícita.

**Requisitos:** EF Core: DbContext, tracking y CRUD.

#### 01. Qué es y qué problema resuelve

El modelo de EF define cómo se mapea un objeto a persistencia. Fluent API permite separar esa configuración del contrato HTTP y del dominio.

Una longitud máxima, una clave y un índice tienen propósitos distintos: evitar valores incompatibles, identificar filas y facilitar consultas.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un proyecto con EF Core 10, proveedor elegido, DbContext y entidades configurados. db, ct e identificadores representan dependencias del caso. El ZIP de consola no contiene esta integración. Verifica el esquema y las garantías con el proveedor. Fragmento dentro de OnModelCreating. Usa ReservaFila del material; no equivale a una migración aplicada.

```C#
var entidad = modelBuilder.Entity<ReservaFila>();
entidad.HasKey(r => r.Id);
entidad.Property(r => r.SalaId).HasMaxLength(80).IsRequired();
entidad.Property(r => r.Version).IsConcurrencyToken();
entidad.HasIndex(r => new { r.GrupoId, r.Inicio });
```

**Cómo funciona:**

1. Localiza la entidad que configura ModelBuilder.
2. Distingue clave, longitud máxima e índice: cada uno protege o facilita algo distinto.
3. Revisa la migración generada para confirmar cómo se expresa cada decisión en la base.

**Resultado esperado:** El modelo expresa longitud, obligatoriedad, token e índice; valida el esquema generado.

#### 03. Cuándo usarlo y qué debes evitar

No delegues todas las invariantes al mapeo. Una restricción puede proteger la base frente a escritores distintos, pero la aplicación debe ofrecer errores comprensibles.

Revisa las migraciones que genera el modelo: un cambio pequeño en C# puede generar una operación destructiva sobre datos existentes.

**Error frecuente:** Un índice no sustituye una validación de negocio, y una configuración de modelo no cambia por sí sola una base ya desplegada.

#### 04. Practica lo aprendido

Separa el mapeo en una configuración y revisa el SQL de su migración.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Separa el mapeo en una configuración y revisa el SQL de su migración.
3. Comprueba y registra el resultado: El modelo expresa longitud, obligatoriedad, token e índice; valida el esquema generado.

**Solución orientativa:**

Comprueba columnas, restricciones e índices; el proveedor determina detalles finales.

#### 05. Comprueba lo aprendido

**¿HasMaxLength sustituye validar el DTO?**

No. El DTO necesita rechazo temprano y el modelo protege persistencia.

**¿Qué configuración limita SalaId y cuál ayuda a buscar por grupo y horario?**

HasMaxLength e IsRequired expresan límites de SalaId; el índice compuesto ayuda a consultas por grupo e inicio. Ninguna de esas líneas garantiza por sí sola horarios no solapados.

**Cómo demostrar el objetivo:**

- Relacionas cada configuración con una restricción o una intención concreta.
- Revisas el esquema generado sin atribuir al índice una garantía de no solapamiento.

**Qué debes recordar:**

- Fluent API describe el mapeo de persistencia.
- Una restricción y un índice tienen responsabilidades distintas.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Modelar persistencia, relaciones y migraciones**

EF Core rastrea cambios de entidades y traduce expresiones de consulta. El modelo persistido puede diferir del dominio: este curso usa una fila ReservaFila con propiedades simples para explicar SQL sin comprometer Intervalo. Un adaptador traduce entre ambos. Sala y Grupo tienen relación uno a muchos con Reserva; elige claves y comportamiento de borrado explícitos. Borrar una sala no debería borrar silenciosamente la historia de sus reservas.

Una migración contiene cambios de esquema y puede afectar datos previos. Revisa columnas obligatorias, valores iniciales, índices y operaciones destructivas; ensaya con una copia representativa y mide duración. Generar una migración no demuestra que sea segura. Aplica cambios compatibles antes de desplegar consumidores nuevos cuando convivan versiones. La reversión de código no revierte automáticamente un cambio de datos.

Usa una clave primaria para identidad, índices para consultas reales y restricciones para reglas expresables en el motor. Un índice único (SalaId, Inicio) solo impide dos inicios idénticos: no evita solapamientos arbitrarios. Un token de concurrencia solo protege modificaciones de una fila conocida. Explica esa diferencia antes de afirmar que la agenda es consistente.

**Fila explícita y versión de aplicación** · Integración EF Core 10; requiere DbContext y proveedor

```C#
public sealed class ReservaFila
{
    public Guid Id { get; set; }
    public string GrupoId { get; set; } = "";
    public string SalaId { get; set; } = "";
    public DateTimeOffset Inicio { get; set; }
    public DateTimeOffset Fin { get; set; }
    public bool Cancelada { get; set; }
    public Guid Version { get; set; }
}
// En OnModelCreating:
var reserva = modelBuilder.Entity<ReservaFila>();
reserva.HasKey(r => r.Id);
reserva.Property(r => r.GrupoId).HasMaxLength(80).IsRequired();
reserva.Property(r => r.Version).IsConcurrencyToken();
reserva.HasIndex(r => new { r.GrupoId, r.Inicio, r.Id });
```

ReservaFila es un modelo pedagógico independiente de Reserva. Los tiempos, índices y SQL deben verificarse con el proveedor elegido; no todos traducen DateTimeOffset de la misma forma.

**Resultado:** El modelo trata Version como token; la aplicación debe renovarlo al actualizar.

### Tema 03 · Relaciones, claves y política de eliminación

**Caso de uso · Conservar reservas históricas**

Una sala tiene reservas antiguas. El equipo quiere retirarla del catálogo sin borrar la historia ni dejar referencias inválidas.

Diseñarás la clave foránea y el comportamiento de borrado, separando la relación de datos de una política de desactivación.

**Objetivo:** Diseñar relaciones y decidir qué ocurre al eliminar una entidad.

**Requisitos:** Fluent API: restricciones y mapeo de entidades.

#### 01. Qué es y qué problema resuelve

Una relación vincula una clave foránea con una entidad principal. Una sala puede tener muchas reservas; cada reserva referencia una sala.

La navegación de C# facilita recorrer objetos, mientras que la clave foránea expresa la vinculación persistida.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un proyecto con EF Core 10, proveedor elegido, DbContext y entidades configurados. db, ct e identificadores representan dependencias del caso. El ZIP de consola no contiene esta integración. Verifica el esquema y las garantías con el proveedor. Requiere declarar SalaFila con una clave compatible. Restrict solicita una política que impida el borrado dependiente según el proveedor.

```C#
modelBuilder.Entity<ReservaFila>()
    .HasOne<SalaFila>()
    .WithMany()
    .HasForeignKey(r => r.SalaId)
    .OnDelete(DeleteBehavior.Restrict);
```

**Cómo funciona:**

1. Identifica entidad principal, dependiente y clave foránea.
2. Lee la política de eliminación configurada para esa relación.
3. Intenta eliminar una sala con reservas y comprueba la garantía esperada.

**Resultado esperado:** Borrar una sala referenciada debe rechazarse; confirma la garantía con el motor.

#### 03. Cuándo usarlo y qué debes evitar

Elige cardinalidad y eliminación según el negocio. Borrar una sala histórica no debería borrar silenciosamente todas sus reservas.

Una relación muchos a muchos con atributos como rol o fecha de alta necesita una entidad de asociación explícita. Cargar relaciones también tiene costo: no conviertas todo listado en un grafo completo.

**Error frecuente:** Elegir eliminación en cascada sin revisar el negocio puede borrar registros que debían conservarse.

#### 04. Practica lo aprendido

Diseña una relación grupo-reserva y un borrado de sala con historia.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Diseña una relación grupo-reserva y un borrado de sala con historia.
3. Comprueba y registra el resultado: Borrar una sala referenciada debe rechazarse; confirma la garantía con el motor.

**Solución orientativa:**

El modelo conserva claves; la aplicación ofrece desactivar la sala si el negocio requiere mantener auditoría.

#### 05. Comprueba lo aprendido

**¿WithMany obliga a cargar todas las reservas?**

No. Describe cardinalidad; la consulta define qué datos se cargan.

**¿Qué alternativa al borrado físico preservaría el historial de reservas?**

Desactivar la sala conserva su identidad y referencias históricas mientras impide nuevas reservas. Define esa transición en aplicación; Restrict protege el borrado físico, no implementa desactivación.

**Cómo demostrar el objetivo:**

- El diseño conserva la referencia sala-reserva y justifica Restrict.
- Una prueba con el proveedor verifica el rechazo del borrado de la sala referenciada.

**Qué debes recordar:**

- Una clave foránea protege integridad referencial.
- La eliminación debe responder a una política de negocio.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Modelar persistencia, relaciones y migraciones**

EF Core rastrea cambios de entidades y traduce expresiones de consulta. El modelo persistido puede diferir del dominio: este curso usa una fila ReservaFila con propiedades simples para explicar SQL sin comprometer Intervalo. Un adaptador traduce entre ambos. Sala y Grupo tienen relación uno a muchos con Reserva; elige claves y comportamiento de borrado explícitos. Borrar una sala no debería borrar silenciosamente la historia de sus reservas.

Una migración contiene cambios de esquema y puede afectar datos previos. Revisa columnas obligatorias, valores iniciales, índices y operaciones destructivas; ensaya con una copia representativa y mide duración. Generar una migración no demuestra que sea segura. Aplica cambios compatibles antes de desplegar consumidores nuevos cuando convivan versiones. La reversión de código no revierte automáticamente un cambio de datos.

Usa una clave primaria para identidad, índices para consultas reales y restricciones para reglas expresables en el motor. Un índice único (SalaId, Inicio) solo impide dos inicios idénticos: no evita solapamientos arbitrarios. Un token de concurrencia solo protege modificaciones de una fila conocida. Explica esa diferencia antes de afirmar que la agenda es consistente.

**Fila explícita y versión de aplicación** · Integración EF Core 10; requiere DbContext y proveedor

```C#
public sealed class ReservaFila
{
    public Guid Id { get; set; }
    public string GrupoId { get; set; } = "";
    public string SalaId { get; set; } = "";
    public DateTimeOffset Inicio { get; set; }
    public DateTimeOffset Fin { get; set; }
    public bool Cancelada { get; set; }
    public Guid Version { get; set; }
}
// En OnModelCreating:
var reserva = modelBuilder.Entity<ReservaFila>();
reserva.HasKey(r => r.Id);
reserva.Property(r => r.GrupoId).HasMaxLength(80).IsRequired();
reserva.Property(r => r.Version).IsConcurrencyToken();
reserva.HasIndex(r => new { r.GrupoId, r.Inicio, r.Id });
```

ReservaFila es un modelo pedagógico independiente de Reserva. Los tiempos, índices y SQL deben verificarse con el proveedor elegido; no todos traducen DateTimeOffset de la misma forma.

**Resultado:** El modelo trata Version como token; la aplicación debe renovarlo al actualizar.

### Tema 04 · DTOs, proyección y mapeo manual

**Caso de uso · Responder sin exponer campos internos**

La entidad de persistencia contiene versión y datos administrativos. El portal solo necesita el identificador de reserva y el de sala.

Construirás un DTO mediante proyección y revisarás tanto selección de campos como alcance y orden de la consulta.

**Objetivo:** Publicar solo los datos que necesita un consumidor.

**Requisitos:** Relaciones, claves y política de eliminación.

#### 01. Qué es y qué problema resuelve

Un DTO es un contrato de entrada o salida, no una entidad persistida. Permite limitar lo que un consumidor escribe y observa.

CrearReservaDto no debería contener GrupoId manipulable, privilegios o campos de auditoría; ReservaResumen omite propiedades internas.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un proyecto con EF Core 10, proveedor elegido, DbContext y entidades configurados. db, ct e identificadores representan dependencias del caso. El ZIP de consola no contiene esta integración. Verifica el esquema y las garantías con el proveedor. Consulta EF ilustrativa. Agrega un orden estable al implementar paginación; el ejemplo se concentra en la proyección de campos.

```C#
public sealed record ReservaVista(Guid Id, string SalaId);
var salida = await db.Reservas.AsNoTracking()
    .Where(r => r.GrupoId == grupoAutorizado)
    .OrderBy(r => r.Id)
    .Select(r => new ReservaVista(r.Id, r.SalaId))
    .Take(20).ToListAsync(ct);
```

**Cómo funciona:**

1. Localiza los campos internos y los campos del DTO de salida.
2. Observa Select: construye la representación que necesita la consulta.
3. Comprueba que datos internos no aparecen en la respuesta y que el orden es estable.

**Resultado esperado:** El resultado no expone Version, propietario interno ni campos de administración.

#### 03. Cuándo usarlo y qué debes evitar

El mapeo manual resulta legible para contratos pequeños. Una biblioteca puede reducir repetición, pero debe tener configuración revisada y pruebas; no infiere qué campos son seguros.

Al consultar EF proyecta antes de materializar para obtener solo lo necesario. Cambiar el modelo de tabla no debe cambiar el JSON accidentalmente.

**Error frecuente:** Serializar una entidad directamente puede publicar propiedades que nunca formaron parte del contrato HTTP.

#### 04. Practica lo aprendido

Compara serializar ReservaFila y proyectar ReservaVista.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Compara serializar ReservaFila y proyectar ReservaVista.
3. Comprueba y registra el resultado: El resultado no expone Version, propietario interno ni campos de administración.

**Solución orientativa:**

La proyección define explícitamente el contrato; verifica que campos sensibles no aparecen en JSON.

#### 05. Comprueba lo aprendido

**¿Un mapper autoriza los campos que copia?**

No. La selección de campos y las reglas de acceso son decisiones del sistema.

**¿Por qué serializar la entidad completa puede exponer más datos que el contrato del portal?**

La entidad puede incluir versión, datos administrativos o futuras propiedades que el consumidor no necesita. Una proyección explícita conserva el contrato de salida aunque cambie persistencia.

**Cómo demostrar el objetivo:**

- La respuesta solo incluye los campos definidos por ReservaVista.
- La consulta filtra el grupo autorizado y limita resultados con un orden estable.

**Qué debes recordar:**

- El DTO define la representación pública.
- Una proyección puede evitar cargar columnas innecesarias.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Proyectar, paginar y medir la consulta**

Para leer, proyecta los campos necesarios antes de materializar. AsNoTracking evita mantener seguimiento cuando no vas a modificar las entidades; una proyección a escalares ya reduce la carga de seguimiento. Include carga relaciones: úsalo para el resultado que necesitas, no para todas las navegaciones. Una consulta por cada fila suele causar N+1: cuenta viajes y examina el SQL con datos representativos.

La paginación por offset es sencilla pero puede degradarse con páginas profundas. Un cursor basado en (Inicio, Id) permite continuar con orden estable si ambos valores se incluyen en el contrato. El máximo de página se valida en servidor; no basta confiar en un número enviado por el cliente. Para tareas pesadas define límites y presupuesto de tiempo antes de agregar caché. Una caché multiusuario debe incluir el grupo en la clave y tener una política de invalidación.

**Lista acotada de reservas autorizadas** · Consulta ilustrativa con proveedor relacional

```C#
var limite = Math.Clamp(tamanoSolicitado, 1, 50);
var lista = await db.Reservas
    .AsNoTracking()
    .Where(r => r.GrupoId == grupoAutorizado && !r.Cancelada)
    .OrderBy(r => r.Inicio).ThenBy(r => r.Id)
    .Select(r => new { r.Id, r.SalaId, r.Inicio, r.Fin })
    .Take(limite)
    .ToListAsync(ct);
```

db.Reservas es DbSet<ReservaFila>; grupoAutorizado proviene de una identidad comprobada. Con EF Core agrega using Microsoft.EntityFrameworkCore. Es una primera página; para páginas posteriores implementa un cursor.

**Resultado:** Una consulta obtiene como máximo 50 filas con orden determinista y sin campos internos.

### Tema 05 · Migraciones y cambios compatibles de esquema

**Caso de uso · Añadir un campo a datos existentes**

Necesitamos incorporar una versión a reservas que ya están almacenadas. Un cambio válido en una base vacía puede fallar o perder datos sobre las filas actuales.

Prepararás la migración, revisarás su SQL y planificarás una transición y recuperación con datos representativos.

**Objetivo:** Preparar cambios de esquema compatibles y revisables.

**Requisitos:** DTOs, proyección y mapeo manual.

#### 01. Qué es y qué problema resuelve

Una migración captura cambios entre modelos y produce operaciones para evolucionar la base. Versionarla permite revisar intención y orden.

Aplicarla sobre una base vacía solo demuestra instalación inicial; no prueba conservación de datos ni tiempo de bloqueo.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un proyecto con EF Core 10, proveedor elegido, DbContext y entidades configurados. db, ct e identificadores representan dependencias del caso. El ZIP de consola no contiene esta integración. Verifica el esquema y las garantías con el proveedor. Comandos de integración para una solución que debes crear. Requieren dotnet-ef, paquete de diseño y proyecto con DbContext compatibles. No se ejecutan sobre la consola del ZIP.

```Shell
dotnet ef migrations add AgregarVersionReserva --project Reserva.Infraestructura
dotnet ef migrations script --idempotent --project Reserva.Infraestructura
# Revisar el script antes de aplicarlo al entorno elegido.
```

**Cómo funciona:**

1. Genera una migración a partir del cambio de modelo.
2. Inspecciona el SQL antes de aplicarlo y distingue operaciones destructivas.
3. Prueba el cambio en una base aislada y documenta compatibilidad con la versión anterior.

**Resultado esperado:** Una migración revisable y un script; no una modificación automática de producción.

#### 03. Cuándo usarlo y qué debes evitar

Para cambios con consumidores simultáneos usa etapas compatibles: agregar, poblar, cambiar consumidores y retirar después. Si conviertes una columna opcional en obligatoria, prepara valores anteriores y revisa qué harán los escritores antiguos.

Una reversión de binario no restaura datos eliminados.

**Error frecuente:** Aplicar una migración automáticamente al arrancar varias instancias puede introducir carreras y fallos difíciles de recuperar.

#### 04. Practica lo aprendido

Ensaya añadir un campo obligatorio a filas ya existentes.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Ensaya añadir un campo obligatorio a filas ya existentes.
3. Comprueba y registra el resultado: Una migración revisable y un script; no una modificación automática de producción.

**Solución orientativa:**

Define valor inicial, ejecuta en una copia representativa y verifica datos y duración. Prepara recuperación antes de aplicar.

#### 05. Comprueba lo aprendido

**¿Down siempre recupera los datos previos?**

No. Revertir esquema puede perder información; conserva una estrategia de datos.

**¿Qué debes resolver antes de añadir una columna obligatoria a filas existentes?**

Debes definir cómo poblar filas anteriores, qué harán los escritores antiguos y cómo recuperar los datos si falla el cambio. Generar un script no verifica esos escenarios.

**Cómo demostrar el objetivo:**

- El plan define cómo completar el nuevo campo antes de exigir su obligatoriedad.
- Distingues generar el script de aplicarlo y describes una verificación de datos y recuperación.

**Qué debes recordar:**

- La migración describe una transición de esquema.
- La entrega debe considerar datos existentes y versiones coexistentes.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Modelar persistencia, relaciones y migraciones**

EF Core rastrea cambios de entidades y traduce expresiones de consulta. El modelo persistido puede diferir del dominio: este curso usa una fila ReservaFila con propiedades simples para explicar SQL sin comprometer Intervalo. Un adaptador traduce entre ambos. Sala y Grupo tienen relación uno a muchos con Reserva; elige claves y comportamiento de borrado explícitos. Borrar una sala no debería borrar silenciosamente la historia de sus reservas.

Una migración contiene cambios de esquema y puede afectar datos previos. Revisa columnas obligatorias, valores iniciales, índices y operaciones destructivas; ensaya con una copia representativa y mide duración. Generar una migración no demuestra que sea segura. Aplica cambios compatibles antes de desplegar consumidores nuevos cuando convivan versiones. La reversión de código no revierte automáticamente un cambio de datos.

Usa una clave primaria para identidad, índices para consultas reales y restricciones para reglas expresables en el motor. Un índice único (SalaId, Inicio) solo impide dos inicios idénticos: no evita solapamientos arbitrarios. Un token de concurrencia solo protege modificaciones de una fila conocida. Explica esa diferencia antes de afirmar que la agenda es consistente.

**Fila explícita y versión de aplicación** · Integración EF Core 10; requiere DbContext y proveedor

```C#
public sealed class ReservaFila
{
    public Guid Id { get; set; }
    public string GrupoId { get; set; } = "";
    public string SalaId { get; set; } = "";
    public DateTimeOffset Inicio { get; set; }
    public DateTimeOffset Fin { get; set; }
    public bool Cancelada { get; set; }
    public Guid Version { get; set; }
}
// En OnModelCreating:
var reserva = modelBuilder.Entity<ReservaFila>();
reserva.HasKey(r => r.Id);
reserva.Property(r => r.GrupoId).HasMaxLength(80).IsRequired();
reserva.Property(r => r.Version).IsConcurrencyToken();
reserva.HasIndex(r => new { r.GrupoId, r.Inicio, r.Id });
```

ReservaFila es un modelo pedagógico independiente de Reserva. Los tiempos, índices y SQL deben verificarse con el proveedor elegido; no todos traducen DateTimeOffset de la misma forma.

**Resultado:** El modelo trata Version como token; la aplicación debe renovarlo al actualizar.

### Tema 06 · Transacciones, atomicidad y outbox

**Caso de uso · Guardar una reserva y su aviso pendiente**

La reserva se confirma, pero el envío del aviso puede fallar. Queremos conservar la intención de notificar sin afirmar que una transacción SQL incluye la entrega externa.

Examinarás la confirmación conjunta de reserva y mensaje de salida, y separarás esa garantía de su procesamiento posterior.

**Objetivo:** Delimitar cambios atómicos y separar efectos externos.

**Requisitos:** Migraciones y cambios compatibles de esquema.

#### 01. Qué es y qué problema resuelve

Una transacción agrupa cambios para que se confirmen juntos o se descarten. Atomicidad local no incluye automáticamente llamadas HTTP, correo o colas externas.

Un caso de uso que guarda una reserva y luego envía un aviso debe definir qué ocurre si la segunda acción falla.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un proyecto con EF Core 10, proveedor elegido, DbContext y entidades configurados. db, ct e identificadores representan dependencias del caso. El ZIP de consola no contiene esta integración. Verifica el esquema y las garantías con el proveedor. Requiere entidades y proveedor transaccional. MensajesSalida representa un outbox; no se envía ningún aviso dentro de este fragmento.

```C#
await using var tx = await db.Database.BeginTransactionAsync(ct);
db.Reservas.Add(nuevaReserva);
db.MensajesSalida.Add(avisoPendiente);
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
```

**Cómo funciona:**

1. Identifica las escrituras que deben confirmarse juntas.
2. Observa el inicio de transacción, la persistencia y el commit.
3. Separa la entrega de un aviso: un outbox permite guardar su intención junto con el cambio local.

**Resultado esperado:** Reserva y mensaje pendiente se confirman juntos o ninguno se confirma.

#### 03. Cuándo usarlo y qué debes evitar

Una llamada SaveChanges relacional normalmente usa una transacción para sus cambios. Si necesitas varias confirmaciones coordinadas, delimita una transacción explícita y mantén breve el trabajo dentro.

Un outbox guarda un mensaje pendiente junto con el cambio; su envío posterior requiere reintentos y deduplicación, no una promesa de entrega única.

**Error frecuente:** Una transacción de base de datos no convierte una llamada HTTP o un correo en parte de su atomicidad.

#### 04. Practica lo aprendido

Simula un fallo antes del commit y otro después, durante el envío.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Simula un fallo antes del commit y otro después, durante el envío.
3. Comprueba y registra el resultado: Reserva y mensaje pendiente se confirman juntos o ninguno se confirma.

**Solución orientativa:**

Antes del commit no quedan cambios; después queda un mensaje pendiente que puede reintentarse sin recrear la reserva.

#### 05. Comprueba lo aprendido

**¿Una transacción SQL vuelve atómico un envío de correo?**

No. Diseña consistencia entre sistemas mediante mensajes persistidos y reconciliación.

**¿Qué debe permanecer si el envío falla después de confirmar la reserva?**

Quedan la reserva confirmada y el mensaje de salida pendiente. Un worker puede reintentar el aviso; su entrega debe controlar duplicados y no volver a crear la reserva.

**Cómo demostrar el objetivo:**

- Reserva y mensaje pendiente se confirman juntos en la misma base y transacción.
- Explicas el fallo antes del commit y el reintento del aviso después del commit.

**Qué debes recordar:**

- La atomicidad se aplica dentro de una frontera concreta.
- El outbox conserva una intención de entrega; requiere un procesador.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Conflictos de actualización y transacciones**

Cuando dos operadores leen la misma versión y ambos cancelan o modifican una reserva, un token de concurrencia permite detectar que el segundo escribe sobre estado obsoleto. EF Core incluye el valor original en la condición de actualización. Si ninguna fila coincide, lanza DbUpdateConcurrencyException. No la conviertas en éxito ni reintentes indefinidamente: vuelve a leer y decide si el nuevo estado satisface la intención.

Una llamada a SaveChanges de un proveedor relacional suele confirmar sus cambios en una transacción. Necesitas una transacción explícita para coordinar varias confirmaciones locales, no para rodear cada operación por costumbre. Ninguna transacción de base de datos vuelve atómico un envío de correo o un HTTP externo. Para ese caso, guarda un evento de salida en la misma transacción y procesa un outbox con reintentos y deduplicación.

**Detectar una versión obsoleta** · Actualización EF Core; token administrado por aplicación

```C#
var fila = await db.Reservas.SingleAsync(r => r.Id == id, ct);
// versiónCliente procede del contrato de actualización validado.
db.Entry(fila).Property(r => r.Version).OriginalValue = versionCliente;
fila.Cancelada = true;
fila.Version = Guid.NewGuid();
try
{
    await db.SaveChangesAsync(ct);
}
catch (DbUpdateConcurrencyException)
{
    return Results.Conflict(new { codigo = "version_obsoleta" });
}
```

Este fragmento presupone que ya se verificó acceso al recurso. Cambiar Version sin configurar IsConcurrencyToken no protege la escritura. El ejemplo devuelve un conflicto esperado, no una traza de excepción.

**Resultado:** La segunda escritura con una versión antigua se rechaza. Verifícalo con dos contextos y el motor real.

### Tema 07 · Concurrencia optimista y carreras de inserción

**Caso de uso · Resolver dos cambios sobre la misma reserva**

Dos personas leen la misma versión de una reserva y luego intentan modificarla. También pueden llegar dos solicitudes nuevas para el mismo horario.

Usarás un token para detectar una actualización obsoleta y delimitarás por qué esa garantía no evita por sí sola inserciones solapadas.

**Objetivo:** Distinguir conflictos de actualización de carreras de inserción.

**Requisitos:** Transacciones, atomicidad y outbox.

#### 01. Qué es y qué problema resuelve

La concurrencia optimista compara una versión original con la actual al escribir. Si alguien cambió la misma fila, la aplicación detecta un conflicto.

Es útil para edición simultánea y evita sobrescribir silenciosamente una intención ajena.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un proyecto con EF Core 10, proveedor elegido, DbContext y entidades configurados. db, ct e identificadores representan dependencias del caso. El ZIP de consola no contiene esta integración. Verifica el esquema y las garantías con el proveedor. La propiedad debe estar configurada como token. El fragmento presupone autorización y modificación de una fila conocida.

```C#
db.Entry(fila).Property(r => r.Version).OriginalValue = versionCliente;
fila.Version = Guid.NewGuid();
try { await db.SaveChangesAsync(ct); }
catch (DbUpdateConcurrencyException)
{
    return Results.Conflict(new { codigo = "version_obsoleta" });
}
```

**Cómo funciona:**

1. Localiza la versión que se leyó antes de modificar la entidad.
2. Observa cómo una actualización compara esa versión con la persistida.
3. Prueba dos escrituras y después dos inserciones simultáneas: necesitan garantías diferentes.

**Resultado esperado:** Un cambio con versión antigua se rechaza; dos inserciones requieren otra prueba.

#### 03. Cuándo usarlo y qué debes evitar

Dos reservas nuevas que se cruzan son filas diferentes: un token por fila no impide esa carrera. Define además una restricción o serialización por sala apropiada al motor.

Un lock en un proceso no coordina varias instancias. El resultado debe probarse con dos conexiones independientes y un estado final verificable.

**Error frecuente:** Un token de concurrencia en una fila existente no evita por sí solo que dos filas nuevas se solapen.

#### 04. Practica lo aprendido

Ejecuta dos actualizaciones de la misma fila y dos inserciones solapadas.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Ejecuta dos actualizaciones de la misma fila y dos inserciones solapadas.
3. Comprueba y registra el resultado: Un cambio con versión antigua se rechaza; dos inserciones requieren otra prueba.

**Solución orientativa:**

La actualización usa token; la inserción necesita otra garantía del motor. Documenta exactamente qué controla cada mecanismo.

#### 05. Comprueba lo aprendido

**¿Reintentar una escritura obsoleta sin volver a leer es correcto?**

No en general. La intención debe evaluarse contra el estado actual.

**¿Por qué un token de versión de una fila no basta para dos reservas nuevas que se solapan?**

No hay una fila existente cuya versión enfrente ambas inserciones. La exclusividad del horario necesita una garantía adicional por sala, mediante restricción o serialización apropiada al motor.

**Cómo demostrar el objetivo:**

- Dos contextos con la misma versión producen un éxito y un conflicto al guardar la segunda actualización.
- Nombras una garantía adicional del proveedor o del diseño para impedir inserciones incompatibles.

**Qué debes recordar:**

- La concurrencia optimista detecta cambios sobre una versión.
- Los conflictos de inserción requieren una garantía adicional.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Conflictos de actualización y transacciones**

Cuando dos operadores leen la misma versión y ambos cancelan o modifican una reserva, un token de concurrencia permite detectar que el segundo escribe sobre estado obsoleto. EF Core incluye el valor original en la condición de actualización. Si ninguna fila coincide, lanza DbUpdateConcurrencyException. No la conviertas en éxito ni reintentes indefinidamente: vuelve a leer y decide si el nuevo estado satisface la intención.

Una llamada a SaveChanges de un proveedor relacional suele confirmar sus cambios en una transacción. Necesitas una transacción explícita para coordinar varias confirmaciones locales, no para rodear cada operación por costumbre. Ninguna transacción de base de datos vuelve atómico un envío de correo o un HTTP externo. Para ese caso, guarda un evento de salida en la misma transacción y procesa un outbox con reintentos y deduplicación.

**Detectar una versión obsoleta** · Actualización EF Core; token administrado por aplicación

```C#
var fila = await db.Reservas.SingleAsync(r => r.Id == id, ct);
// versiónCliente procede del contrato de actualización validado.
db.Entry(fila).Property(r => r.Version).OriginalValue = versionCliente;
fila.Cancelada = true;
fila.Version = Guid.NewGuid();
try
{
    await db.SaveChangesAsync(ct);
}
catch (DbUpdateConcurrencyException)
{
    return Results.Conflict(new { codigo = "version_obsoleta" });
}
```

Este fragmento presupone que ya se verificó acceso al recurso. Cambiar Version sin configurar IsConcurrencyToken no protege la escritura. El ejemplo devuelve un conflicto esperado, no una traza de excepción.

**Resultado:** La segunda escritura con una versión antigua se rechaza. Verifícalo con dos contextos y el motor real.

### Tema 08 · Consultas eficientes, paginación y medición

**Caso de uso · Consultar una página acotada**

Un grupo acumula miles de reservas y la pantalla solo muestra una página. Cargar todo antes de filtrar eleva el costo de cada solicitud.

Revisarás filtro, proyección, orden y límite antes de ejecutar, y definirás qué mediciones sostienen una mejora de rendimiento.

**Objetivo:** Limitar el trabajo de una consulta y verificar su costo.

**Requisitos:** Concurrencia optimista y carreras de inserción.

#### 01. Qué es y qué problema resuelve

Una consulta eficiente recupera los datos necesarios con trabajo acotado. Antes de cambiar de herramienta mide duración, filas, viajes y SQL.

N+1 aparece cuando recorres resultados y consultas relaciones una por una; los datos pequeños pueden ocultarlo durante desarrollo.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un proyecto con EF Core 10, proveedor elegido, DbContext y entidades configurados. db, ct e identificadores representan dependencias del caso. El ZIP de consola no contiene esta integración. Verifica el esquema y las garantías con el proveedor. Primera página acotada. Para continuar implementa un cursor o una política de offset; comprueba DateTimeOffset con tu proveedor.

```C#
var pagina = await db.Reservas.AsNoTracking()
    .Where(r => r.GrupoId == grupo && !r.Cancelada)
    .OrderBy(r => r.Inicio).ThenBy(r => r.Id)
    .Select(r => new { r.Id, r.Inicio })
    .Take(Math.Clamp(limite, 1, 50)).ToListAsync(ct);
```

**Cómo funciona:**

1. Sigue el filtro autorizado antes de proyectar o materializar.
2. Selecciona solo los campos necesarios y un orden estable.
3. Limita la página y observa SQL, filas obtenidas y viajes a la base.

**Resultado esperado:** Como máximo 50 resultados, dos campos y orden determinista.

#### 03. Cuándo usarlo y qué debes evitar

Ordena de manera estable antes de paginar. Un offset profundo puede exigir saltar muchas filas; un cursor por valores ordenados ayuda a continuar.

Un índice debe corresponder a filtros y orden reales. Una caché agrega invalidación e identidad de grupo: no uses una clave compartida para datos de usuarios distintos.

**Error frecuente:** AsNoTracking reduce seguimiento, pero no arregla una consulta que sigue cargando un volumen excesivo.

#### 04. Practica lo aprendido

Compara SQL, viajes y filas antes y después de proyectar.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Compara SQL, viajes y filas antes y después de proyectar.
3. Comprueba y registra el resultado: Como máximo 50 resultados, dos campos y orden determinista.

**Solución orientativa:**

La mejora necesita medición con carga representativa. No concluyas rendimiento por contar líneas de C#.

#### 05. Comprueba lo aprendido

**¿AsNoTracking corrige una consulta que carga millones de filas?**

No. Reduce seguimiento; todavía debes filtrar, proyectar y limitar.

**¿Qué observarías para comprobar una mejora además de reducir líneas de C#?**

Compara SQL, filas, viajes y duración con datos y carga representativos. Menos código o ausencia de seguimiento no demuestra menos trabajo del proveedor.

**Cómo demostrar el objetivo:**

- La consulta devuelve como máximo 50 filas del grupo y dos campos en orden determinista.
- La comparación registra SQL, filas y viajes con datos representativos antes de concluir una mejora.

**Qué debes recordar:**

- Filtra, proyecta y pagina antes de cargar.
- Una mejora de rendimiento debe medirse.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Proyectar, paginar y medir la consulta**

Para leer, proyecta los campos necesarios antes de materializar. AsNoTracking evita mantener seguimiento cuando no vas a modificar las entidades; una proyección a escalares ya reduce la carga de seguimiento. Include carga relaciones: úsalo para el resultado que necesitas, no para todas las navegaciones. Una consulta por cada fila suele causar N+1: cuenta viajes y examina el SQL con datos representativos.

La paginación por offset es sencilla pero puede degradarse con páginas profundas. Un cursor basado en (Inicio, Id) permite continuar con orden estable si ambos valores se incluyen en el contrato. El máximo de página se valida en servidor; no basta confiar en un número enviado por el cliente. Para tareas pesadas define límites y presupuesto de tiempo antes de agregar caché. Una caché multiusuario debe incluir el grupo en la clave y tener una política de invalidación.

**Lista acotada de reservas autorizadas** · Consulta ilustrativa con proveedor relacional

```C#
var limite = Math.Clamp(tamanoSolicitado, 1, 50);
var lista = await db.Reservas
    .AsNoTracking()
    .Where(r => r.GrupoId == grupoAutorizado && !r.Cancelada)
    .OrderBy(r => r.Inicio).ThenBy(r => r.Id)
    .Select(r => new { r.Id, r.SalaId, r.Inicio, r.Fin })
    .Take(limite)
    .ToListAsync(ct);
```

db.Reservas es DbSet<ReservaFila>; grupoAutorizado proviene de una identidad comprobada. Con EF Core agrega using Microsoft.EntityFrameworkCore. Es una primera página; para páginas posteriores implementa un cursor.

**Resultado:** Una consulta obtiene como máximo 50 filas con orden determinista y sin campos internos.

## Ruta 04

### Tema 01 · FluentValidation y reglas de entrada

**Caso de uso · Informar dos errores de entrada**

Llega una solicitud con SalaId vacío y fin anterior al inicio. Queremos indicar ambos errores antes de llamar al caso de uso.

Aplicarás reglas de forma con FluentValidation y revisarás dónde invocar el validador; separarás este rechazo de proteger disponibilidad al escribir.

**Objetivo:** Validar entradas sin confundir formato con reglas de negocio.

**Requisitos:** Fundamentos de la ruta anterior.

#### 01. Qué es y qué problema resuelve

Un validador de entrada organiza reglas de formato y límites antes del caso de uso. FluentValidation permite expresarlas mediante un contrato separado.

La regla de disponibilidad o autorización no queda resuelta por validar una cadena o un intervalo.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere FluentValidation y un DTO CrearReservaDto con SalaId, Inicio y Fin. Define el validador e invoca ValidateAsync explícitamente. No incluye endpoint ni escritura; la integración debe impedir invocar el caso de uso cuando el resultado es inválido. Requiere FluentValidation y el DTO del curso. El ejemplo es propio; registra IValidator<CrearReservaDto> y traduce sus errores en el endpoint.

```C#
public sealed class CrearReservaValidador : AbstractValidator<CrearReservaDto>
{
    public CrearReservaValidador()
    {
        RuleFor(x => x.SalaId).NotEmpty().MaximumLength(80);
        RuleFor(x => x.Fin).GreaterThan(x => x.Inicio);
    }
}
// En el adaptador: await validador.ValidateAsync(entrada, ct);
```

**Cómo funciona:**

1. Lee cada RuleFor y la condición que comprueba sobre el DTO.
2. Construye una entrada válida y otra con sala vacía o intervalo incorrecto.
3. Invoca ValidateAsync desde la integración y convierte sus errores en una respuesta consistente.

**Resultado esperado:** Sala vacía y fin no posterior generan errores antes de guardar.

#### 03. Cuándo usarlo y qué debes evitar

Registrar un validador no garantiza que Minimal APIs lo ejecute. Invoca ValidateAsync deliberadamente y transmite cancelación.

La validación automática tradicional no admite todas las reglas asíncronas y su integración tiene alcance específico. Mantén invariantes también en dominio; una tarea interna puede crear objetos sin pasar por el DTO HTTP.

**Error frecuente:** Registrar el validador no garantiza que Minimal APIs lo ejecute; la invocación debe formar parte del flujo.

#### 04. Practica lo aprendido

Invoca ValidateAsync con una entrada que tenga SalaId vacío y fin anterior al inicio. Identifica ambos errores y diseña la condición que impediría llamar al caso de uso.

1. Prepara CrearReservaDto con SalaId, Inicio y Fin y el paquete FluentValidation.
2. Invoca explícitamente el validador con la entrada inválida y otra válida.
3. Inspecciona los errores y escribe la condición del adaptador; la integración HTTP es una ampliación.

**Solución orientativa:**

La entrada inválida devuelve errores de SalaId y Fin. Solo una rama con resultado válido debe invocar el caso de uso. Definir la clase o registrarla no ejecuta por sí solo la validación.

#### 05. Comprueba lo aprendido

**¿Una regla que consulta disponibilidad elimina la carrera de escritura?**

No. La condición puede cambiar después de validar; protege la confirmación.

**¿Definir la clase del validador basta para impedir que el endpoint guarde una entrada inválida?**

No. El flujo debe invocar ValidateAsync, revisar su resultado y detenerse antes de llamar al caso de uso si hay errores. El registro en DI solo permite obtener la instancia.

**Cómo demostrar el objetivo:**

- ValidateAsync devuelve errores de sala y de horario para la entrada inválida.
- El diseño del adaptador impide llamar al caso de uso cuando la validación falla.

**Qué debes recordar:**

- La entrada se valida antes de ejecutar el caso de uso.
- El dominio conserva sus invariantes aunque se omita el endpoint.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Tres niveles de validación**

El transporte valida estructura, tipos y tamaños; el dominio conserva invariantes; la aplicación verifica condiciones que requieren identidad o datos. Un texto de 10.000 caracteres puede rechazarse antes de consultar la base. Un horario invertido tampoco debe llegar a persistencia. La disponibilidad de la sala requiere consulta y protección contra carreras: no es solo una regla del DTO.

Para reglas pequeñas usa funciones puras. Un conjunto grande puede organizarse con FluentValidation, verificando versión y modo de integración. En Minimal APIs, un validador externo no se ejecuta automáticamente por registrarlo: invócalo o configura deliberadamente su integración. Si una regla requiere I/O usa validación asíncrona compatible. Las validaciones que consultan pueden quedar obsoletas: confirma las invariantes compartidas en la escritura.

**Errores de entrada acumulados** · Función y uso guiado con el DTO del módulo 2

```C#
static Dictionary<string, string[]> Validar(CrearReservaDto entrada)
{
    var errores = new Dictionary<string, string[]>();
    if (string.IsNullOrWhiteSpace(entrada.SalaId) || entrada.SalaId.Length > 80)
        errores["salaId"] = ["Sala obligatoria, máximo 80 caracteres."];
    if (entrada.Fin <= entrada.Inicio)
        errores["fin"] = ["Debe ser posterior al inicio."];
    return errores;
}
// En el endpoint, antes del caso de uso:
var errores = Validar(entrada);
if (errores.Count > 0) return Results.ValidationProblem(errores);
```

La función permite reportar varios errores en una sola respuesta. No valida autorización ni disponibilidad. Un DTO nulo o un JSON mal formado se atiende en el límite HTTP.

**Resultado:** 400 con errores por campo; ninguna escritura en almacenamiento.

### Tema 02 · ProblemDetails: errores HTTP consistentes

**Caso de uso · Comunicar una versión obsoleta**

El portal intenta actualizar una reserva con una versión antigua. Necesita reconocer el conflicto sin analizar un texto humano ni recibir una traza interna.

Representarás el rechazo mediante ProblemDetails con estado, código estable y correlación; lo compararás con entrada inválida y fallo inesperado.

**Objetivo:** Comunicar errores HTTP con códigos y estados consistentes.

**Requisitos:** FluentValidation y reglas de entrada.

#### 01. Qué es y qué problema resuelve

ProblemDetails ofrece un formato para describir errores de HTTP. El consumidor necesita un tipo o código estable y un estado coherente, no una excepción serializada.

Un traceId permite relacionar la respuesta con evidencia interna sin publicar el diagnóstico completo.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Fragmento de endpoint con HttpContext http. El conflicto es esperado; el texto no revela detalles de almacenamiento.

```C#
return Results.Problem(
    title: "No se pudo actualizar la reserva",
    statusCode: 409,
    extensions: new Dictionary<string, object?>
    {
        ["codigo"] = "version_obsoleta",
        ["traceId"] = http.TraceIdentifier
    });
```

**Cómo funciona:**

1. Identifica el estado HTTP y el significado del conflicto.
2. Lee el título y los datos seguros que recibirá el consumidor.
3. Usa traceId para relacionar la respuesta con el diagnóstico interno.

**Resultado esperado:** 409 y un cuerpo ProblemDetails con código estable y correlación.

#### 03. Cuándo usarlo y qué debes evitar

El detalle debe ser seguro y útil: nombre de campo y regla incumplida para entrada, código de conflicto para estado y mensaje genérico para fallo inesperado. No incluyas SQL, rutas locales, tokens o cookies.

La respuesta debe mantener content type, código y cuerpo consistentes; no devuelvas 200 con un campo error para simular éxito.

**Error frecuente:** Devolver la excepción o la traza interna puede revelar detalles que el consumidor no necesita.

#### 04. Practica lo aprendido

Diseña cuerpo para entrada inválida, conflicto y fallo interno.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Diseña cuerpo para entrada inválida, conflicto y fallo interno.
3. Comprueba y registra el resultado: 409 y un cuerpo ProblemDetails con código estable y correlación.

**Solución orientativa:**

Mantén estados distintos y prueba que el 500 no contiene excepciones. El consumidor usa códigos, no analiza frases humanas.

#### 05. Comprueba lo aprendido

**¿ProblemDetails decide cuándo autorizar?**

No. Es representación del error; los controles ocurren antes.

**¿Qué dato debe usar el portal para reconocer el conflicto sin depender de la frase del título?**

Usa el código estable version_obsoleta y el estado 409. El título puede cambiar o traducirse; traceId sirve para correlacionar diagnóstico, no para clasificar el conflicto.

**Cómo demostrar el objetivo:**

- El conflicto usa 409 y un código estable version_obsoleta.
- El diseño de 500 conserva correlación y omite excepciones y detalles internos.

**Qué debes recordar:**

- El error público debe ser estable y comprensible.
- La correlación permite investigar sin publicar el diagnóstico.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**ProblemDetails y errores con significado**

Una respuesta de error consistente incluye tipo, título, estado y un identificador de correlación. El detalle debe ayudar al consumidor sin revelar SQL, nombres internos, rutas locales ni credenciales. Para fallos inesperados devuelve un mensaje genérico y conserva el diagnóstico en un registro protegido. Para conflictos esperados ofrece un código estable que el cliente pueda interpretar sin analizar una frase.

AddProblemDetails registra servicios; UseExceptionHandler intercepta fallos del pipeline. No confundas configurarlos con capturar toda excepción dentro de cada caso de uso. Una cancelación solicitada por el cliente no es automáticamente un 500. En .NET 10, un IExceptionHandler que devuelve true suprime por defecto ciertos diagnósticos de excepciones atendidas: revisa SuppressDiagnosticsCallback o registra explícitamente lo que necesitas sin duplicarlo.

**Correlación segura en el contrato** · Configuración ASP.NET Core 10

```C#
builder.Services.AddProblemDetails(opciones =>
{
    opciones.CustomizeProblemDetails = contexto =>
        contexto.ProblemDetails.Extensions["traceId"] =
            contexto.HttpContext.TraceIdentifier;
});
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
```

Coloca el manejo de errores antes de componentes susceptibles de fallar. Este fragmento no añade un IExceptionHandler propio: usa la respuesta ProblemDetails predeterminada y agrega correlación.

**Resultado:** Errores compatibles pueden incluir traceId; la respuesta no expone la excepción original.

### Tema 03 · Manejo global de excepciones y cancelación

**Caso de uso · Distinguir caída y cancelación**

Una consulta falla inesperadamente y otra se interrumpe porque el cliente cancela. Tratar ambas como éxito o como el mismo error impediría diagnosticar lo ocurrido.

Revisarás el manejador global y clasificarás fallos, rechazos previstos y cancelaciones antes de decidir su respuesta y registro.

**Objetivo:** Tratar fallos inesperados sin ocultar rechazos o cancelaciones.

**Requisitos:** ProblemDetails: errores HTTP consistentes.

#### 01. Qué es y qué problema resuelve

Una excepción expresa un fallo que interrumpe el recorrido normal. Un manejador central evita repetir try/catch y filtrar diagnósticos en cada endpoint.

No conviertas todos los errores de negocio en excepciones si ya pueden representarse como un resultado esperado.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Configuración básica; no incluye un handler propio. Define políticas de errores conocidos y registros en tu integración.

```C#
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
// Los endpoints posteriores quedan dentro del manejo configurado.
```

**Cómo funciona:**

1. Registra el soporte de ProblemDetails y el manejador de excepciones.
2. Coloca el manejo global donde pueda observar los fallos posteriores.
3. Distingue un rechazo esperado de una excepción y de una cancelación solicitada.

**Resultado esperado:** Un fallo inesperado posterior puede traducirse a una respuesta controlada sin traza pública.

#### 03. Cuándo usarlo y qué debes evitar

Configura servicios y middleware, y separa cancelación solicitada de error interno. En .NET 10, un IExceptionHandler que atiende una excepción puede suprimir ciertos diagnósticos por defecto; revisa la política para conservar observabilidad sin duplicados.

La página de desarrollo pertenece al entorno de desarrollo controlado.

**Error frecuente:** Capturar cualquier excepción y devolver éxito oculta fallos; registrar cada cancelación normal como error genera ruido.

#### 04. Practica lo aprendido

Provoca un fallo de prueba y una cancelación del cliente; registra diferencias.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Provoca un fallo de prueba y una cancelación del cliente; registra diferencias.
3. Comprueba y registra el resultado: Un fallo inesperado posterior puede traducirse a una respuesta controlada sin traza pública.

**Solución orientativa:**

El fallo inesperado requiere diagnóstico protegido; la cancelación sigue su política y no se etiqueta automáticamente como 500.

#### 05. Comprueba lo aprendido

**¿Atrapar Exception y devolver éxito mejora disponibilidad?**

No. Oculta fallos y comunica un resultado falso.

**¿Por qué una cancelación del cliente requiere una decisión diferente a un fallo inesperado?**

La cancelación puede ser una interrupción esperada, mientras el fallo requiere diagnóstico de una operación que no pudo completarse. Aplica políticas distintas y evita afirmar éxito en ambos casos.

**Cómo demostrar el objetivo:**

- El fallo inesperado posterior al manejador produce una respuesta controlada, sin traza pública.
- La cancelación tiene una política distinta y nunca se traduce automáticamente a éxito.

**Qué debes recordar:**

- Los fallos inesperados necesitan diagnóstico interno.
- Los rechazos de negocio y la cancelación requieren semánticas propias.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**ProblemDetails y errores con significado**

Una respuesta de error consistente incluye tipo, título, estado y un identificador de correlación. El detalle debe ayudar al consumidor sin revelar SQL, nombres internos, rutas locales ni credenciales. Para fallos inesperados devuelve un mensaje genérico y conserva el diagnóstico en un registro protegido. Para conflictos esperados ofrece un código estable que el cliente pueda interpretar sin analizar una frase.

AddProblemDetails registra servicios; UseExceptionHandler intercepta fallos del pipeline. No confundas configurarlos con capturar toda excepción dentro de cada caso de uso. Una cancelación solicitada por el cliente no es automáticamente un 500. En .NET 10, un IExceptionHandler que devuelve true suprime por defecto ciertos diagnósticos de excepciones atendidas: revisa SuppressDiagnosticsCallback o registra explícitamente lo que necesitas sin duplicarlo.

**Correlación segura en el contrato** · Configuración ASP.NET Core 10

```C#
builder.Services.AddProblemDetails(opciones =>
{
    opciones.CustomizeProblemDetails = contexto =>
        contexto.ProblemDetails.Extensions["traceId"] =
            contexto.HttpContext.TraceIdentifier;
});
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
```

Coloca el manejo de errores antes de componentes susceptibles de fallar. Este fragmento no añade un IExceptionHandler propio: usa la respuesta ProblemDetails predeterminada y agrega correlación.

**Resultado:** Errores compatibles pueden incluir traceId; la respuesta no expone la excepción original.

### Tema 04 · Logging estructurado y Serilog

**Caso de uso · Investigar un rechazo sin guardar secretos**

Soporte recibe un identificador de correlación, pero el log solo dice error. Necesita localizar el rechazo sin almacenar tokens ni cuerpos completos.

Diseñarás un evento estructurado con resultado y correlación, y decidirás qué datos excluir y cuánto conservar.

**Objetivo:** Registrar eventos estructurados con contexto suficiente.

**Requisitos:** Manejo global de excepciones y cancelación.

#### 01. Qué es y qué problema resuelve

Una plantilla de log conserva propiedades separadas de la frase. Esas propiedades permiten agrupar fallos y reconstruir decisiones.

Registra operación, resultado y correlación; el contenido de una credencial no ayuda a autorizar y no debe aparecer en los registros.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. logger y http son el logger y HttpContext recibidos en la operación; la forma de consultar propiedades depende del proveedor de logs. Fragmento con ILogger y HttpContext. No registra la entrada completa, el token ni la excepción en la respuesta.

```C#
logger.LogInformation(
    "Validación de reserva {Resultado} {TraceId}",
    "rechazada", http.TraceIdentifier);
```

**Cómo funciona:**

1. Lee la plantilla del mensaje y los nombres de sus propiedades.
2. Relaciona el identificador de reserva con el evento que ocurrió.
3. Busca por esas propiedades en un destino de logs configurado.

**Resultado esperado:** Resultado y TraceId se almacenan como propiedades consultables.

#### 03. Cuándo usarlo y qué debes evitar

ILogger desacopla el consumidor del destino. Serilog es un proveedor posible para estructuración y sinks; elegirlo requiere configurar transporte, retención y acceso.

Evita concatenaciones costosas y logging indiscriminado del cuerpo. Un identificador puede ser sensible: decide necesidad y política, no solo nivel de severidad.

**Error frecuente:** Interpolar todos los datos en una cadena reduce estructura; registrar tokens o datos sensibles aumenta exposición.

#### 04. Practica lo aprendido

Diseña un evento de éxito y uno de rechazo, y revisa datos que expondrían.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Diseña un evento de éxito y uno de rechazo, y revisa datos que expondrían.
3. Comprueba y registra el resultado: Resultado y TraceId se almacenan como propiedades consultables.

**Solución orientativa:**

Incluye resultado y correlación; excluye Authorization, cookies y secretos. Establece retención y responsable.

#### 05. Comprueba lo aprendido

**¿Más volumen de logs equivale a mejor observabilidad?**

No. El diagnóstico necesita señales pertinentes, estructura y límites.

**¿Qué registrarías para investigar este rechazo sin copiar el token del usuario?**

Registra operación, resultado y un TraceId que soporte búsqueda. Excluye Authorization, cookies y secretos; acceso y retención del log también necesitan una decisión.

**Cómo demostrar el objetivo:**

- El evento permite buscar Resultado y TraceId como propiedades.
- La revisión excluye Authorization, cookies y secretos y define responsable y retención.

**Qué debes recordar:**

- Un evento estructurado separa mensaje y propiedades.
- El contexto debe ayudar a investigar sin revelar secretos.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Logs estructurados y observabilidad**

Un log estructurado almacena propiedades con nombres estables, en vez de concatenar valores dentro de una frase. Registra operación, resultado, duración y correlación. Evita cuerpos completos, Authorization, cookies, contraseñas y refresh tokens. Incluso identificadores pueden ser datos sensibles: define necesidad, retención y acceso. Serilog es una opción de proveedor; ILogger mantiene el contrato de la aplicación sin obligarla a conocer un destino concreto.

Logs cuentan eventos; métricas resumen frecuencias y latencias; trazas conectan etapas de una solicitud. Una métrica por cada ReservaId genera cardinalidad excesiva. Usa etiquetas acotadas como operación y resultado; guarda identificadores en eventos controlados. Una alerta debe indicar condición, impacto y acción: una suma de errores sin ventana ni responsable no ayuda a operar.

**Una plantilla de log, dos propiedades** · Fragmento con ILogger inyectado

```C#
logger.LogInformation(
    "Cancelación procesada {ReservaId} {Resultado}",
    id, resultado.EsExito ? "aceptada" : "rechazada");
```

ILogger pertenece al adaptador o aplicación según la frontera elegida. Para producción evalúa si registrar el identificador es necesario; evita adjuntar el JWT para depurar permisos.

**Resultado:** El destino recibe ReservaId y Resultado como propiedades consultables.

## Ruta 05

### Tema 01 · DDD básico: lenguaje, agregados y límites

**Caso de uso · Separar identidad y horario**

Una reserva puede cambiar de estado y conserva su identidad; su horario representa un valor. La disponibilidad de una sala depende de más de una reserva.

Usarás lenguaje del negocio para distinguir entidad, valor y regla compartida antes de elegir límites de consistencia.

**Objetivo:** Nombrar reglas y límites a partir del lenguaje del negocio.

**Requisitos:** Fundamentos de la ruta anterior.

#### 01. Qué es y qué problema resuelve

El modelado del dominio empieza por palabras y reglas del negocio. Entidad, valor y agregado no son nombres de carpetas: representan identidad, significado y un límite de consistencia.

El agregado protege cambios que deben coordinarse juntos sin obligar a cargar todo el sistema.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Contrato o esquema de diseño para analizar; no es una aplicación ejecutable completa. La evidencia principal es la decisión y su justificación. Una comprobación automatizada requiere implementar el control en una solución preparada. Modelo textual original. Separa explícitamente invariantes locales y consistencia compartida.

```Texto
Reserva
  identidad: Id
  valores: SalaId, GrupoId, Intervalo
  transición: activa → cancelada

Regla compartida: no insertar dos intervalos incompatibles en una sala.
```

**Cómo funciona:**

1. Identifica las palabras que usan quienes gestionan salas y reservas.
2. Distingue entidades con identidad de valores definidos por sus propiedades.
3. Agrupa los cambios que deben sostener una garantía consistente.

**Resultado esperado:** Puedes asignar un responsable y una prueba distintos a cada regla.

#### 03. Cuándo usarlo y qué debes evitar

Reserva controla cancelación; Intervalo controla forma del horario. Evitar solapamientos depende de varias reservas y del almacenamiento, por lo que no puede atribuirse solo a una entidad.

Divide por reglas y transacciones, no por cada sustantivo. Un dominio sencillo puede beneficiarse de un modelo pequeño sin adoptar todas las técnicas de DDD.

**Error frecuente:** Convertir cada tabla en un agregado no explica dónde vive una regla ni qué cambios deben confirmarse juntos.

#### 04. Practica lo aprendido

Clasifica Id, horario, sala y disponibilidad como identidad, valor o regla compartida.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Clasifica Id, horario, sala y disponibilidad como identidad, valor o regla compartida.
3. Comprueba y registra el resultado: Puedes asignar un responsable y una prueba distintos a cada regla.

**Solución orientativa:**

Id identifica; horario es valor; referencia de sala vincula; disponibilidad requiere coordinación.

#### 05. Comprueba lo aprendido

**¿Un agregado debe contener cada tabla relacionada?**

No. Su límite responde a invariantes y consistencia local.

**¿Qué diferencia hay entre validar un intervalo y garantizar disponibilidad de la sala?**

La validez del intervalo depende de sus propios valores; disponibilidad depende de otras reservas y escritores concurrentes. Una entidad aislada no puede proteger por sí sola esa condición compartida.

**Cómo demostrar el objetivo:**

- Clasificas Id como identidad e Intervalo como valor con reglas propias.
- Explicas por qué disponibilidad requiere coordinación fuera de una única reserva.

**Qué debes recordar:**

- El lenguaje del negocio guía el modelo.
- Un agregado protege una frontera de consistencia.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Clean Architecture y modelo del dominio**

Organiza dependencias hacia reglas estables. Dominio contiene Reserva e Intervalo; Aplicación define CancelarReserva e IReservas; Infraestructura implementa almacenamiento; API compone y traduce HTTP. La separación se expresa con referencias entre proyectos, no solo con nombres de carpetas. Dominio no debe referenciar EF Core ni ASP.NET Core. La API sí puede conocer Infraestructura para registrar adaptadores en su raíz de composición.

DDD ayuda cuando hay vocabulario y reglas complejas: entidad tiene identidad, objeto de valor representa un concepto y agregado delimita consistencia local. No convierte cada tabla en un agregado ni obliga a microservicios. Reserva protege su transición; la capacidad compartida de Sala puede requerir coordinación fuera de ese objeto. Define el límite según las reglas y la transacción que necesitas, no según la pantalla.

Repository es una abstracción orientada a recuperar y persistir agregados. EF Core ya ofrece muchas capacidades de repositorio y unidad de trabajo mediante DbSet y DbContext. Una capa adicional debe aportar lenguaje, aislamiento o una frontera de pruebas útil. Evita una interfaz genérica que replique todos los métodos de EF o esconda IQueryable sin política clara.

**Referencias que puedes verificar** · Esquema de diseño

```Texto
Reserva.Dominio       → ninguna capa del sistema
Reserva.Aplicacion    → Reserva.Dominio
Reserva.Infraestructura → Reserva.Aplicacion + Reserva.Dominio
Reserva.Api           → Reserva.Aplicacion + Reserva.Infraestructura
Reserva.Pruebas       → proyectos bajo prueba
```

Las flechas significan referencias de compilación. En ejecución la aplicación llama al adaptador inyectado, pero su código solo depende del contrato.

**Resultado:** Puedes cambiar un adaptador sin agregar una referencia de infraestructura al dominio.

### Tema 02 · Clean Architecture y dirección de dependencias

**Caso de uso · Cambiar HTTP sin cambiar el dominio**

El dominio importa Results y un contexto de persistencia. Cada cambio del framework web o de almacenamiento llega a las reglas de reserva.

Leerás la dirección de referencias y moverás detalles a adaptadores conservando contratos expresados en términos del negocio.

**Objetivo:** Mantener reglas independientes de HTTP y persistencia.

**Requisitos:** DDD básico: lenguaje, agregados y límites.

#### 01. Qué es y qué problema resuelve

Clean Architecture conserva reglas de negocio independientes de transporte y almacenamiento. Su garantía se expresa con referencias de compilación: dominio no necesita el framework web; aplicación define capacidades; infraestructura aporta adaptadores y la API conecta el conjunto.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Contrato o esquema de diseño para analizar; no es una aplicación ejecutable completa. La evidencia principal es la decisión y su justificación. Una comprobación automatizada requiere implementar el control en una solución preparada. Las flechas indican dependencias de código, no orden de ejecución. La API registra adaptadores y el caso de uso conoce contratos.

```Texto
Dominio → sin referencias al framework web o a persistencia
Aplicación → Dominio
Infraestructura → Aplicación, Dominio
API → Aplicación, Infraestructura
```

**Cómo funciona:**

1. Empieza por Dominio y las reglas que no dependen de tecnología.
2. Sigue las referencias de Aplicación hacia sus contratos.
3. Ubica EF Core y HTTP en adaptadores y la conexión de implementaciones en composición.

**Resultado esperado:** Un cambio en almacenamiento no obliga al dominio a importar sus tipos.

#### 03. Cuándo usarlo y qué debes evitar

Una carpeta Domain dentro de un proyecto que importa EF sigue teniendo acceso al proveedor. Verifica referencias y tipos usados.

No añadas capas que solo pasen argumentos sin proteger una frontera. Puedes comenzar con un diseño pequeño y separar proyectos cuando ayude a conservar límites verificables.

**Error frecuente:** Una interfaz que expone tipos del proveedor mantiene ese acoplamiento aunque cambie de carpeta.

#### 04. Practica lo aprendido

Revisa referencias de una solución y localiza una dependencia hacia afuera.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Revisa referencias de una solución y localiza una dependencia hacia afuera.
3. Comprueba y registra el resultado: Un cambio en almacenamiento no obliga al dominio a importar sus tipos.

**Solución orientativa:**

Si Dominio importa un contexto o resultados HTTP, mueve ese detalle a un adaptador y conserva el contrato necesario.

#### 05. Comprueba lo aprendido

**¿Una interfaz elimina cualquier acoplamiento?**

No. Si su firma contiene tipos del proveedor, mantiene esa dependencia.

**¿Dónde colocarías la traducción a Results y el acceso al contexto?**

Results pertenece al adaptador HTTP. El acceso al contexto pertenece a infraestructura; el dominio expresa sus reglas sin importar ninguno de esos tipos.

**Cómo demostrar el objetivo:**

- El diagrama sitúa Dominio sin referencias al framework web ni a persistencia.
- Localizas una dependencia hacia afuera y propones una firma que no exponga tipos del proveedor.

**Qué debes recordar:**

- Las dependencias apuntan hacia las reglas.
- Los adaptadores implementan los detalles que esas reglas necesitan.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Clean Architecture y modelo del dominio**

Organiza dependencias hacia reglas estables. Dominio contiene Reserva e Intervalo; Aplicación define CancelarReserva e IReservas; Infraestructura implementa almacenamiento; API compone y traduce HTTP. La separación se expresa con referencias entre proyectos, no solo con nombres de carpetas. Dominio no debe referenciar EF Core ni ASP.NET Core. La API sí puede conocer Infraestructura para registrar adaptadores en su raíz de composición.

DDD ayuda cuando hay vocabulario y reglas complejas: entidad tiene identidad, objeto de valor representa un concepto y agregado delimita consistencia local. No convierte cada tabla en un agregado ni obliga a microservicios. Reserva protege su transición; la capacidad compartida de Sala puede requerir coordinación fuera de ese objeto. Define el límite según las reglas y la transacción que necesitas, no según la pantalla.

Repository es una abstracción orientada a recuperar y persistir agregados. EF Core ya ofrece muchas capacidades de repositorio y unidad de trabajo mediante DbSet y DbContext. Una capa adicional debe aportar lenguaje, aislamiento o una frontera de pruebas útil. Evita una interfaz genérica que replique todos los métodos de EF o esconda IQueryable sin política clara.

**Referencias que puedes verificar** · Esquema de diseño

```Texto
Reserva.Dominio       → ninguna capa del sistema
Reserva.Aplicacion    → Reserva.Dominio
Reserva.Infraestructura → Reserva.Aplicacion + Reserva.Dominio
Reserva.Api           → Reserva.Aplicacion + Reserva.Infraestructura
Reserva.Pruebas       → proyectos bajo prueba
```

Las flechas significan referencias de compilación. En ejecución la aplicación llama al adaptador inyectado, pero su código solo depende del contrato.

**Resultado:** Puedes cambiar un adaptador sin agregar una referencia de infraestructura al dominio.

### Tema 03 · Repository Pattern con propósito

**Caso de uso · Elegir una frontera de datos útil**

El caso de uso necesita buscar y guardar una reserva. El equipo propone una interfaz, pero debe justificar qué garantía protege y qué complejidad incorpora.

Examinarás un contrato de acceso a datos y lo compararás con usar el contexto directamente desde aplicación, explicando límites y costos.

**Objetivo:** Definir una frontera de acceso a datos con una garantía clara.

**Requisitos:** Clean Architecture y dirección de dependencias.

#### 01. Qué es y qué problema resuelve

Un repositorio ofrece acceso a agregados mediante un contrato orientado al caso de uso. Puede ser útil para aislar una frontera o expresar lenguaje.

DbSet y DbContext ya cubren muchas capacidades: envolver cada método con CRUD genérico puede duplicar abstracciones sin ganar garantías.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Contrato C# que usa Reserva del laboratorio; falta implementar el adaptador y definir sus garantías de búsqueda y guardado. La interfaz por sí sola no configura persistencia ni autorización. Contrato utilizado por CancelarReserva en el laboratorio. La implementación en memoria enseña el límite, sin garantizar persistencia.

```C#
public interface IReservas
{
    Task<Reserva?> BuscarAsync(Guid id, CancellationToken ct);
    Task GuardarAsync(CancellationToken ct);
}
```

**Cómo funciona:**

1. Lee las operaciones que realmente necesita el caso de uso.
2. Identifica qué promete el repositorio sobre consulta y confirmación.
3. Implementa ese contrato en infraestructura y prueba su comportamiento real.

**Resultado esperado:** El caso de uso coordina acceso y transición sin conocer el motor.

#### 03. Cuándo usarlo y qué debes evitar

Define si GuardarAsync confirma trabajo pendiente y dónde vive la unidad de trabajo. Evita devolver IQueryable sin límites claros porque permite al consumidor construir consultas arbitrarias fuera del adaptador.

No confundas un fake rápido con una implementación real de transacciones o concurrencia.

**Error frecuente:** Devolver IQueryable sin límites permite construir consultas fuera de la frontera y debilita la garantía del contrato.

#### 04. Practica lo aprendido

Justifica usar este repositorio frente a consultar DbContext directamente desde aplicación.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Justifica usar este repositorio frente a consultar DbContext directamente desde aplicación.
3. Comprueba y registra el resultado: El caso de uso coordina acceso y transición sin conocer el motor.

**Solución orientativa:**

Documenta qué frontera protege y qué costo agrega. Una interfaz para un contexto puede ser otra alternativa, si su acoplamiento es aceptable.

#### 05. Comprueba lo aprendido

**¿Un repositorio garantiza independencia del motor en todas las consultas?**

No. Traducción, aislamiento y rendimiento siguen necesitando integración.

**¿Qué garantía tendría que aportar IReservas para justificar esta abstracción?**

Debe ofrecer una frontera y semántica útiles, como qué busca, cómo representa ausencia y cuándo confirma cambios. Si solo replica DbContext sin una garantía adicional, revisa si su costo se justifica.

**Cómo demostrar el objetivo:**

- El contrato explica búsqueda, ausencia y confirmación de cambios.
- La decisión justifica la frontera y reconoce que traducción y consistencia requieren pruebas del proveedor.

**Qué debes recordar:**

- Un repositorio debe tener un propósito.
- La confirmación y la unidad de trabajo necesitan un alcance explícito.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Clean Architecture y modelo del dominio**

Organiza dependencias hacia reglas estables. Dominio contiene Reserva e Intervalo; Aplicación define CancelarReserva e IReservas; Infraestructura implementa almacenamiento; API compone y traduce HTTP. La separación se expresa con referencias entre proyectos, no solo con nombres de carpetas. Dominio no debe referenciar EF Core ni ASP.NET Core. La API sí puede conocer Infraestructura para registrar adaptadores en su raíz de composición.

DDD ayuda cuando hay vocabulario y reglas complejas: entidad tiene identidad, objeto de valor representa un concepto y agregado delimita consistencia local. No convierte cada tabla en un agregado ni obliga a microservicios. Reserva protege su transición; la capacidad compartida de Sala puede requerir coordinación fuera de ese objeto. Define el límite según las reglas y la transacción que necesitas, no según la pantalla.

Repository es una abstracción orientada a recuperar y persistir agregados. EF Core ya ofrece muchas capacidades de repositorio y unidad de trabajo mediante DbSet y DbContext. Una capa adicional debe aportar lenguaje, aislamiento o una frontera de pruebas útil. Evita una interfaz genérica que replique todos los métodos de EF o esconda IQueryable sin política clara.

**Referencias que puedes verificar** · Esquema de diseño

```Texto
Reserva.Dominio       → ninguna capa del sistema
Reserva.Aplicacion    → Reserva.Dominio
Reserva.Infraestructura → Reserva.Aplicacion + Reserva.Dominio
Reserva.Api           → Reserva.Aplicacion + Reserva.Infraestructura
Reserva.Pruebas       → proyectos bajo prueba
```

Las flechas significan referencias de compilación. En ejecución la aplicación llama al adaptador inyectado, pero su código solo depende del contrato.

**Resultado:** Puedes cambiar un adaptador sin agregar una referencia de infraestructura al dominio.

### Tema 04 · Specification Pattern: criterios y evaluadores

**Caso de uso · Mantener el mismo criterio en dos consultas**

El listado y un reporte necesitan las reservas activas del grupo-lima. Dos copias del filtro ya difieren y una incluye reservas canceladas.

Encapsularás el criterio en una Specification y lo probarás en memoria; distinguirás esa evaluación de traducir la expresión con un proveedor de consultas.

**Objetivo:** Encapsular un criterio reutilizable y elegir dónde evaluarlo.

**Requisitos:** Repository Pattern con propósito.

#### 01. Qué es y qué problema resuelve

Cuando necesitas el mismo filtro en varios lugares, repetirlo facilita que las versiones se vuelvan distintas. Specification Pattern permite darle un nombre y conservar el criterio en un solo objeto.

En este ejemplo, ReservasActivasDelGrupo selecciona reservas que pertenecen a un grupo y siguen activas. El criterio describe qué incluir; un evaluador decide cómo aplicarlo.

Expression<Func<T, bool>> representa la condición como una estructura que un proveedor puede inspeccionar. Al llamar Compile se obtiene un delegado: una función que evalúa objetos ya cargados en memoria.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Usa los tipos del núcleo del laboratorio .NET descargable. El fragmento ilustra la regla local; no configura HTTP ni persistencia. El tipo Reserva y la colección reservas pertenecen al laboratorio descargable. La interfaz conserva el criterio como expresión y la clase le da un significado. En EF Core, el modelo y el proveedor deben admitir su traducción.

```C#
using System.Linq.Expressions;
public interface IEspecificacion<T>
{
    Expression<Func<T, bool>> Criterio { get; }
}
public sealed class ReservasActivasDelGrupo(string grupoId)
    : IEspecificacion<Reserva>
{
    public Expression<Func<Reserva, bool>> Criterio =>
        r => r.GrupoId == grupoId && !r.Cancelada;
}

public static class ConsultaEjemplo
{
    public static List<Reserva> Filtrar(IEnumerable<Reserva> reservas)
    {
        var spec = new ReservasActivasDelGrupo("grupo-lima");
        return reservas.Where(spec.Criterio.Compile()).ToList();
    }
}
// En un proveedor compatible: origen.Where(spec.Criterio), sin Compile.
```

**Cómo funciona:**

1. IEspecificacion define una expresión; ReservasActivasDelGrupo le da un significado concreto.
2. El criterio incluye reservas activas del grupo y excluye las canceladas o ajenas.
3. Sobre objetos ya cargados se compila; en un proveedor compatible se entrega la expresión sin Compile.

**Resultado esperado:** El grupo ajeno y las reservas canceladas quedan excluidos; verifica SQL por separado.

#### 03. Cuándo usarlo y qué debes evitar

Una regla pura de dominio puede evaluar objetos; una consulta puede incluir orden y límites en una frontera de aplicación o infraestructura. Mantén evaluadores EF fuera del dominio.

Autorizar una cancelación requiere comprobar recurso y actor aunque exista un filtro de listado. Una prueba con Compile no demuestra traducción SQL.

**Error frecuente:** Una prueba en memoria no demuestra traducción SQL, y filtrar un listado no autoriza una operación sobre un recurso.

#### 04. Practica lo aprendido

Prepara tres reservas en memoria: activa del grupo-lima, cancelada del mismo grupo y activa de otro. Aplica la Specification y explica cómo cambiaría la evaluación con IQueryable.

1. Usa Reserva del laboratorio y el fragmento de esta lección para preparar los tres casos.
2. Evalúa el predicado y comprueba que solo la reserva activa del grupo queda incluida.
3. Explica por qué la prueba en memoria no verifica SQL. Como ampliación, prepara EF Core y comprueba la traducción sin Compile.

**Solución orientativa:**

El predicado acepta la activa del grupo y rechaza las otras dos. Compile genera un delegado para memoria. Un IQueryable debe recibir la expresión y su proveedor debe poder traducirla; eso requiere una prueba de integración separada.

#### 05. Comprueba lo aprendido

**¿Una Specification es siempre más clara que LINQ local?**

No. Úsala cuando el criterio tenga significado y reutilización.

**¿Qué puedes demostrar en memoria y qué queda pendiente al usar la Specification con EF Core?**

En memoria demuestras la lógica del predicado con inclusión y exclusión. EF Core necesita recibir la expresión sin Compile y traducirla con su proveedor; SQL, restricciones y costo requieren otra prueba.

**Cómo demostrar el objetivo:**

- El predicado incluye la reserva activa del grupo y excluye la cancelada y la de otro grupo.
- Distingues Compile para memoria de pasar la expresión a IQueryable y verificar su traducción por separado.

**Qué debes recordar:**

- Specification nombra un criterio reutilizable.
- El evaluador determina dónde y cómo se aplica.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Specification Pattern: una consulta con nombre**

Una especificación encapsula un criterio que tiene significado y puede reutilizarse. ReservasActivasDelGrupo expresa qué filas interesan; el evaluador decide cómo aplicar el criterio en un proveedor. Para consultas usa Expression<Func<T,bool>> si necesitas traducción SQL; Func<T,bool> es un delegado para ejecutar sobre objetos. Compilar la expresión sirve para la consola, pero no debe introducir evaluación local accidental en una consulta de base de datos.

Una especificación de dominio puede contestar si un objeto cumple una regla sin conocer EF. Una especificación de consulta puede contener filtro, orden y paginación; colócala en una frontera donde esos conceptos pertenezcan. Si incluye relaciones o proyecciones específicas del almacenamiento, no la presentes como regla pura del dominio. El evaluador EF vive en Infraestructura.

El patrón se estudia después de Repository y antes de CQRS: primero reconoces la frontera de datos, luego nombras consultas y después separas responsabilidades de lectura/escritura. No es obligatorio para toda consulta. Una proyección única, sencilla y local puede ser más comprensible expresada directamente. No uses una especificación como única autorización: quien invoca la operación sigue teniendo que verificar actor, acción y recurso.

**Especificación mínima del laboratorio** · Tipos completos compilados en el laboratorio

```C#
using System.Linq.Expressions;
public interface IEspecificacion<T>
{
    Expression<Func<T, bool>> Criterio { get; }
}
public sealed class ReservasActivasDelGrupo(string grupoId)
    : IEspecificacion<Reserva>
{
    public Expression<Func<Reserva, bool>> Criterio =>
        r => r.GrupoId == grupoId && !r.Cancelada;
}
```

No incorpora una biblioteca. Captura un grupo validado y expresa un filtro. En memoria se usa Criterio.Compile(); en un proveedor compatible se usa Where(Criterio) sin compilar.

**Resultado:** Incluye una reserva activa del grupo y excluye una cancelada o de otro grupo.

## Ruta 06

### Tema 01 · Result Pattern y rechazos esperados

**Caso de uso · Comunicar un rechazo previsto**

Cancelar una reserva puede fallar porque no existe o porque pertenece a otro grupo. El mismo caso de uso servirá a HTTP y a una consola.

Representarás éxito y rechazo mediante Result, dejando al consumidor traducir el código y separando fallos inesperados de decisiones previstas.

**Objetivo:** Representar un éxito o rechazo previsto sin depender de HTTP.

**Requisitos:** Fundamentos de la ruta anterior.

#### 01. Qué es y qué problema resuelve

Result representa éxito o un rechazo previsto sin convertir cada decisión en una excepción. El caso de uso puede expresar no_encontrada o sin_permiso; la API traduce esos códigos a HTTP.

Un fallo de infraestructura inesperado conserva su diagnóstico mediante la política de excepciones.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Usa los tipos del núcleo del laboratorio .NET descargable. El fragmento ilustra la regla local; no configura HTTP ni persistencia. Usa CancelarReserva del laboratorio. Un recurso ausente produce un resultado previsto; una cancelación solicitada sigue propagándose.

```C#
var resultado = await caso.EjecutarAsync(id, grupo, ct);
if (!resultado.EsExito)
    Console.WriteLine($"Rechazo previsto: {resultado.Codigo}");
```

**Cómo funciona:**

1. Ejecuta el caso de uso con una reserva existente y otra ausente.
2. Lee el resultado y el código que describe el rechazo esperado.
3. Deja al adaptador HTTP la conversión del código a una respuesta.

**Resultado esperado:** El consumidor interpreta un código estable sin conocer HTTP.

#### 03. Cuándo usarlo y qué debes evitar

Diseña el tipo para evitar combinaciones inválidas. El Resultado del laboratorio es pedagógico y pequeño; un contrato más amplio puede controlar constructores y tipar el valor de éxito.

No escondas todas las excepciones detrás de un error genérico, porque pierdes causas y confundes rechazo con indisponibilidad.

**Error frecuente:** Convertir todos los fallos en Result puede ocultar excepciones de infraestructura que necesitan diagnóstico.

#### 04. Practica lo aprendido

Distingue recurso ausente, acceso ajeno y fallo de conexión.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Distingue recurso ausente, acceso ajeno y fallo de conexión.
3. Comprueba y registra el resultado: El consumidor interpreta un código estable sin conocer HTTP.

**Solución orientativa:**

Los dos primeros son resultados previstos; el tercero requiere diagnóstico y política de disponibilidad.

#### 05. Comprueba lo aprendido

**¿Result implica capturar cualquier Exception?**

No. Elige explícitamente cuáles decisiones pertenecen al contrato.

**¿Cómo puede la consola interpretar un rechazo sin recibir un resultado HTTP?**

El caso de uso entrega un código de rechazo y la consola decide cómo mostrarlo. El adaptador HTTP puede traducir ese mismo código a un estado sin incorporar HTTP al caso de uso.

**Cómo demostrar el objetivo:**

- El consumidor identifica éxito o rechazo por un código estable sin importar HTTP.
- Clasificas ausencia y acceso ajeno como decisiones previstas y conexión fallida como fallo inesperado.

**Qué debes recordar:**

- Result hace explícitas alternativas esperadas.
- La API traduce el resultado; el dominio no decide estados HTTP.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Result para rechazos previstos**

Un resultado distingue éxito de un rechazo esperado: recurso ausente, falta de permiso o conflicto. Evita usar excepciones como flujo habitual para cada decisión del negocio. Una excepción continúa siendo adecuada para fallos inesperados que necesitan propagación y diagnóstico. No captures Exception para devolver siempre un resultado vacío: oculta defectos y confunde disponibilidad con reglas.

El laboratorio usa Resultado con EsExito y Codigo para enseñar el flujo. Es deliberadamente pequeño; en un contrato público más grande impide combinaciones inválidas como éxito con error mediante tipos o constructores controlados. Puedes agregar un valor tipado, una categoría y metadatos seguros. Mantén mensajes técnicos fuera del dominio y realiza la traducción HTTP en el adaptador.

**Coordinar sin depender de HTTP** · Método compilado en el laboratorio

```C#
public async Task<Resultado> EjecutarAsync(
    Guid id, string? grupo, CancellationToken ct)
{
    ct.ThrowIfCancellationRequested();
    var reserva = await reservas.BuscarAsync(id, ct);
    if (reserva is null) return Resultado.Rechazo("no_encontrada");
    if (!Acceso.PuedeCancelar(grupo, reserva))
        return Resultado.Rechazo("sin_permiso");
    reserva.Cancelar();
    await reservas.GuardarAsync(ct);
    return Resultado.Exito();
}
```

Es el método de CancelarReserva del laboratorio. IReservas se recibe por constructor. Autorización ocurre antes de modificar; la cancelación repetida conserva el estado sin producir un rechazo.

**Resultado:** Ausencia y acceso ajeno se distinguen; éxito llega después de GuardarAsync.

### Tema 02 · CQRS: comandos y consultas separados

**Caso de uso · Consultar sin modificar estado**

Un método cancela una reserva y devuelve un listado completo; quien lo llama no sabe si está leyendo o cambiando datos.

Separarás la intención del comando de la forma de una consulta, sin exigir procesos o bases diferentes.

**Objetivo:** Separar la intención de una escritura de la necesidad de lectura.

**Requisitos:** Result Pattern y rechazos esperados.

#### 01. Qué es y qué problema resuelve

CQRS separa intención de cambiar estado de intención de obtener una vista. Una consulta no debería modificar silenciosamente una reserva.

Un comando aplica permisos y consistencia, mientras una consulta puede proyectar una representación optimizada para lectura.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Contrato o esquema de diseño para analizar; no es una aplicación ejecutable completa. La evidencia principal es la decisión y su justificación. Una comprobación automatizada requiere implementar el control en una solución preparada. Mensajes independientes; debes implementar handlers y obtener grupo desde contexto validado, no desde una propiedad manipulable.

```C#
public sealed record CancelarReservaComando(Guid Id);
public sealed record ConsultarReserva(Guid Id);
public sealed record ReservaVista(Guid Id, string SalaId, bool Cancelada);
```

**Cómo funciona:**

1. Compara el comando que solicita un cambio con la consulta que obtiene datos.
2. Identifica qué regla debe verificar la escritura.
3. Diseña la representación de lectura según su consumidor.

**Resultado esperado:** La consulta devuelve una vista y el comando conserva responsabilidad de cambio.

#### 03. Cuándo usarlo y qué debes evitar

Puedes usar handlers separados y la misma base de datos. No necesitas un bus, event sourcing ni dos almacenes para comenzar.

Si agregas proyecciones asíncronas, define atraso, reconciliación y qué verá el consumidor después de una escritura. La complejidad debe resolver un problema concreto.

**Error frecuente:** Separar comandos y consultas no exige bases distintas, eventos ni procesos distribuidos.

#### 04. Practica lo aprendido

Separa un método que actualiza y devuelve un listado sin especificar intención.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Separa un método que actualiza y devuelve un listado sin especificar intención.
3. Comprueba y registra el resultado: La consulta devuelve una vista y el comando conserva responsabilidad de cambio.

**Solución orientativa:**

Crea comando para la actualización y consulta para la vista; documenta consistencia del resultado posterior.

#### 05. Comprueba lo aprendido

**¿CQRS exige microservicios?**

No. Es separación de responsabilidades y puede vivir en un monolito.

**¿Qué responsabilidad tendría cada operación al separar el método actual?**

El comando aplica la transición con permisos y consistencia. La consulta obtiene la vista sin efectos; si usa una proyección asíncrona, su resultado puede quedar temporalmente atrasado.

**Cómo demostrar el objetivo:**

- El comando declara el cambio y la consulta devuelve una vista sin modificar estado.
- Explicas cuándo la consulta posterior observaría el cambio y qué garantía de consistencia se necesita.

**Qué debes recordar:**

- Un comando expresa intención de cambio.
- Una consulta obtiene una representación sin efectos de negocio.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**CQRS como separación de responsabilidades**

CQRS separa comandos que cambian estado de consultas que leen. No exige dos bases de datos, un bus ni consistencia eventual. En una aplicación pequeña puedes usar un handler por comando y un servicio de consultas que proyecte DTOs en la misma base. El beneficio inicial es reconocer intención y no reutilizar una consulta de lectura como una operación con efectos ocultos.

Un comando como CancelarReserva contiene una intención; su handler aplica permisos y cambios. Una consulta como ListarReservasActivas devuelve una vista acotada. Un mediador puede despachar mensajes y centralizar behaviors de validación o medición, pero introduce indirección y dependencia. Evalúa mantenimiento, versión y licencia del paquete elegido. Llamar directamente al handler es suficiente para aprender la separación.

CQRS no equivale a event sourcing: este último guarda eventos como fuente de verdad y reconstruye estado. Tampoco implica que cada handler viva en un microservicio. Agrega complejidad distribuida solo si puedes explicar el problema de escalado, autonomía o dominio que la justifica y el costo de operarla.

**Mensajes con intención explícita** · Tipos de mensajes; handlers como ejercicio

```C#
public sealed record CancelarReservaComando(Guid ReservaId);
public sealed record ListarReservasConsulta(int Limite);
public sealed record ReservaResumen(Guid Id, string SalaId);

// El grupo se deriva del contexto de identidad validado.
// Un servicio de aplicación recibe comando + contexto.
// La consulta devuelve IReadOnlyList<ReservaResumen>, sin modificar estado.
```

El comando no transporta permisos elegidos por el cliente. Los mensajes organizan responsabilidades; no requieren MediatR ni otro despachador.

**Resultado:** Puedes probar el handler de cancelación separado del servicio de lectura.

### Tema 03 · Mediator y MediatR: despacho y behaviors

**Caso de uso · Elegir cómo despachar un caso de uso**

El endpoint llama directamente al caso de uso. Se propone agregar un mediador para compartir validación y medición, pero primero debemos entender qué aporta.

Compararás la llamada explícita con el recorrido endpoint, despachador, behaviors y handler; separarás despacho local de mensajería durable.

**Objetivo:** Reconocer el despacho de mensajes y sus responsabilidades.

**Requisitos:** CQRS: comandos y consultas separados.

#### 01. Qué es y qué problema resuelve

Un mediador conecta un mensaje con su handler y puede aplicar comportamientos comunes. Esa indirección facilita validación o medición coherente, pero hace menos visible el recorrido si se utiliza sin convenciones.

CQRS describe responsabilidades; MediatR es una biblioteca opcional para despacho.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Fragmento de llamada explícita, no una implementación de mediador. Define caso, Resultado e identificadores; el diagrama de la lección compara este recorrido con despacho y behaviors. Despacho local propio mediante un delegado; no usa MediatR ni reproduce su API. Sirve como punto de comparación antes de elegir una biblioteca.

```C#
Func<Guid, CancellationToken, Task<Resultado>> ejecutar =
    (id, ct) => caso.EjecutarAsync(id, grupoValidado, ct);
var resultado = await ejecutar(reservaId, cancellationToken);
```

**Cómo funciona:**

1. El consumidor envía una solicitud a un punto de despacho.
2. El despacho selecciona la función o handler que la atiende.
3. Identifica qué validaciones transversales rodean la ejecución y cuáles siguen perteneciendo al negocio.

**Resultado esperado:** El mismo caso de uso se ejecuta de forma explícita y puede probarse directamente.

#### 03. Cuándo usarlo y qué debes evitar

Antes de incorporarla revisa versión, licencia y registro en su documentación oficial. Un handler debe conservar reglas del caso de uso; un behavior no debería cambiar silenciosamente permisos o commit.

Para aprender el principio comienza con una llamada explícita y compárala con despacho. No declares que usar Send vuelve distribuido el trabajo.

**Error frecuente:** Introducir un mediador solo para reenviar una llamada agrega recorrido sin garantizar una mejor separación.

#### 04. Practica lo aprendido

Dibuja endpoint, despachador, behavior y handler; decide qué registro conserva duración.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Dibuja endpoint, despachador, behavior y handler; decide qué registro conserva duración.
3. Comprueba y registra el resultado: El mismo caso de uso se ejecuta de forma explícita y puede probarse directamente.

**Solución orientativa:**

Mide alrededor del handler y propaga cancelación. La política de acceso sigue verificándose antes de modificar.

#### 05. Comprueba lo aprendido

**¿Un mediator sustituye un broker de mensajes?**

No. El despacho local no aporta entrega durable ni coordinación entre procesos.

**¿Qué agregaría un mediador a la llamada directa y qué no aportaría respecto de un broker?**

Puede centralizar despacho y behaviors de validación o medición si existe ese propósito. No agrega por sí solo almacenamiento durable, entrega entre procesos ni recuperación de mensajes de un broker.

**Cómo demostrar el objetivo:**

- El diagrama ubica medición y cancelación alrededor del handler.
- La decisión explica una responsabilidad concreta del mediador y reconoce lo que no garantiza entre procesos.

**Qué debes recordar:**

- Mediator organiza el despacho.
- Los behaviors necesitan un orden y un propósito explícitos.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**CQRS como separación de responsabilidades**

CQRS separa comandos que cambian estado de consultas que leen. No exige dos bases de datos, un bus ni consistencia eventual. En una aplicación pequeña puedes usar un handler por comando y un servicio de consultas que proyecte DTOs en la misma base. El beneficio inicial es reconocer intención y no reutilizar una consulta de lectura como una operación con efectos ocultos.

Un comando como CancelarReserva contiene una intención; su handler aplica permisos y cambios. Una consulta como ListarReservasActivas devuelve una vista acotada. Un mediador puede despachar mensajes y centralizar behaviors de validación o medición, pero introduce indirección y dependencia. Evalúa mantenimiento, versión y licencia del paquete elegido. Llamar directamente al handler es suficiente para aprender la separación.

CQRS no equivale a event sourcing: este último guarda eventos como fuente de verdad y reconstruye estado. Tampoco implica que cada handler viva en un microservicio. Agrega complejidad distribuida solo si puedes explicar el problema de escalado, autonomía o dominio que la justifica y el costo de operarla.

**Mensajes con intención explícita** · Tipos de mensajes; handlers como ejercicio

```C#
public sealed record CancelarReservaComando(Guid ReservaId);
public sealed record ListarReservasConsulta(int Limite);
public sealed record ReservaResumen(Guid Id, string SalaId);

// El grupo se deriva del contexto de identidad validado.
// Un servicio de aplicación recibe comando + contexto.
// La consulta devuelve IReadOnlyList<ReservaResumen>, sin modificar estado.
```

El comando no transporta permisos elegidos por el cliente. Los mensajes organizan responsabilidades; no requieren MediatR ni otro despachador.

**Resultado:** Puedes probar el handler de cancelación separado del servicio de lectura.

### Tema 04 · Idempotencia, reintentos y deduplicación

**Caso de uso · Recibir de nuevo una cancelación**

El cliente no recibe respuesta y repite la cancelación. Queremos que la segunda llamada no produzca otra transición ni otro efecto injustificado.

Comprobarás la transición local repetida y distinguirás esa evidencia del protocolo persistente necesario para reintentar creaciones entre instancias.

**Objetivo:** Controlar efectos cuando una operación se procesa otra vez.

**Requisitos:** Mediator y MediatR: despacho y behaviors.

#### 01. Qué es y qué problema resuelve

Una respuesta perdida puede llevar al cliente a repetir una solicitud ya confirmada. Idempotencia limita los efectos de esa repetición.

Cancelar puede llevar dos veces al mismo estado; crear requiere reconocer una intención repetida y devolver un resultado coherente.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Usa los tipos del núcleo del laboratorio .NET descargable. El fragmento ilustra la regla local; no configura HTTP ni persistencia. Usa Reserva del laboratorio. La transición es localmente idempotente; efectos externos requieren otra política.

```C#
var primera = reserva.Cancelar();
var segunda = reserva.Cancelar();
Console.WriteLine($"Cambios: {primera}, {segunda}");
```

**Cómo funciona:**

1. Cancela la reserva por primera vez y observa el cambio.
2. Repite la misma llamada y comprueba que no añade otro cambio.
3. Distingue esa transición local de crear reservas o entregar efectos externos.

**Resultado esperado:** True, False: la segunda llamada no añade un cambio de estado.

#### 03. Cuándo usarlo y qué debes evitar

Una clave de idempotencia necesita ámbito por actor o grupo, contenido asociado y restricción persistente. La misma clave con otra entrada se rechaza.

No guardes claves solo en memoria si despliegas varias instancias. El outbox puede entregar más de una vez: el consumidor debe deduplicar y definir recuperación de fallos.

**Error frecuente:** Una bandera en memoria no resuelve deduplicación entre procesos ni recuperación después de reiniciar.

#### 04. Practica lo aprendido

Cancela dos veces la misma reserva del laboratorio y registra transición y estado final. Diseña aparte qué persistirías para reintentar una creación después de perder la respuesta.

1. Ejecuta las dos llamadas sobre la misma instancia de Reserva.
2. Comprueba True, False y estado cancelado.
3. Como extensión de diseño, relaciona clave, huella de entrada y resultado persistido para una creación entre procesos.

**Solución orientativa:**

La primera llamada cambia estado y la segunda no. Esta prueba solo verifica la transición local. Una creación reintentable necesita coordinación persistida y una política para claves, entradas incompatibles y respuestas.

#### 05. Comprueba lo aprendido

**¿Idempotencia garantiza que la respuesta siempre sea idéntica?**

Su garantía principal son efectos controlados; documenta aparte la respuesta.

**¿Por qué dos llamadas a Cancelar no prueban idempotencia distribuida para crear reservas?**

Cancelar dos veces solo verifica el estado de una instancia ya existente. Crear entre procesos exige reconocer la intención repetida y coordinar clave, entrada y resultado en almacenamiento persistente.

**Cómo demostrar el objetivo:**

- Dos cancelaciones sobre la misma reserva producen True y False y conservan el estado cancelado.
- Explicas por qué ese resultado no demuestra una creación única entre procesos.

**Qué debes recordar:**

- La repetición debe conservar los efectos previstos.
- La respuesta y la política de reintentos deben documentarse.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Reintentos, idempotencia y efectos externos**

Una solicitud puede confirmarse en la base y perder su respuesta por un corte de red. El cliente reintenta sin saber si se ejecutó. Cancelar una reserva puede ser idempotente porque llegar otra vez al mismo estado es aceptable. Crear una reserva requiere una clave de idempotencia con ámbito por actor/grupo y una restricción persistente: la misma clave y entrada devuelve el resultado previo; una clave repetida con otra entrada debe rechazarse.

Un outbox guarda el cambio y el mensaje pendiente en la misma transacción. Un worker entrega el mensaje al menos una vez; el receptor deduplica por identificador. No prometas exactly once por registrar un evento. Define caducidad, número de intentos, fallos definitivos y reconciliación. El laboratorio no implementa esa infraestructura: la práctica exige diseñarla y probarla con almacenamiento.

### Tema 05 · Antipatrones: acoplamiento y efectos ocultos

**Caso de uso · Quitar complejidad sin perder comportamiento**

Una consulta bloquea con .Result y atraviesa una capa que solo reenvía parámetros. El equipo necesita corregirlo sin perder controles ni efectos.

Examinarás bloqueo y responsabilidades redundantes y justificarás cada cambio con garantías que deban conservarse.

**Objetivo:** Detectar complejidad y efectos que no aportan una garantía.

**Requisitos:** Idempotencia, reintentos y deduplicación.

#### 01. Qué es y qué problema resuelve

Un antipatrón es una solución que parece conveniente pero introduce un costo repetido. Endpoints con SQL y permisos duplicados dificultan cambios; .Result durante I/O bloquea hilos; capas que solo reenvían argumentos ocultan un recorrido sencillo sin protegerlo.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Fragmento dentro de un método async con un servicio que implementa BuscarAsync y un CancellationToken. Debes definir servicio e id; no es una API completa. Fragmento de un consumidor con un servicio asíncrono. await conserva la espera sin bloqueo explícito; no vuelve segura una dependencia compartida.

```C#
// Evitar: var reserva = servicio.BuscarAsync(id, ct).Result;
var reserva = await servicio.BuscarAsync(id, ct);
// La firma del consumidor también debe ser asíncrona.
```

**Cómo funciona:**

1. Localiza el bloqueo producido por .Result en una operación de I/O.
2. Sustitúyelo por await y propaga la asincronía al consumidor.
3. Conserva pruebas del resultado y de los efectos para verificar la corrección.

**Resultado esperado:** El consumidor obtiene el resultado propagando cancelación sin .Result.

#### 03. Cuándo usarlo y qué debes evitar

Corregir exige una evidencia concreta. Separa un efecto inesperado de una preferencia de estilo.

Si una abstracción no protege vocabulario, pruebas o evolución, evalúa eliminarla. Conserva pruebas observables para que la simplificación no modifique semántica de permisos o consistencia.

**Error frecuente:** Eliminar una capa por preferencia estética puede borrar una frontera útil; exige un problema y evidencia concretos.

#### 04. Practica lo aprendido

Identifica una capa redundante y un bloqueo de I/O; corrige y conserva pruebas.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Identifica una capa redundante y un bloqueo de I/O; corrige y conserva pruebas.
3. Comprueba y registra el resultado: El consumidor obtiene el resultado propagando cancelación sin .Result.

**Solución orientativa:**

Justifica la eliminación con responsabilidades, y comprueba resultado y efectos antes y después.

#### 05. Comprueba lo aprendido

**¿Todo repositorio es un antipatrón?**

No. Evalúa propósito y costo en el contexto del sistema.

**¿Qué evidencia exigirías antes de quitar una capa que parece redundante?**

Comprueba que conserva permisos, resultados y efectos y que ninguna responsabilidad dependía de esa frontera. Justifica la simplificación con el problema observado y pruebas antes y después.

**Cómo demostrar el objetivo:**

- La firma asíncrona usa await y propaga el token.
- La eliminación de una capa conserva comportamiento y controles, con una razón explícita.

**Qué debes recordar:**

- Una simplificación debe conservar comportamiento.
- El costo de una abstracción debe justificarse por su propósito.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Monolito modular y antipatrón de complejidad**

Comienza con módulos internos y fronteras de datos explícitas cuando un equipo necesita entregar una aplicación coherente. Separar despliegues exige contratos remotos, observabilidad distribuida, evolución de mensajes y tolerancia a fallos parciales. Un módulo con muchas llamadas síncronas a otro puede empeorar al convertirse en servicio remoto.

Identifica tres señales de sobrearquitectura: interfaces que solo reexpresan una clase; eventos para encadenar pasos que deben ser atómicos; y handlers que atraviesan muchas capas sin agregar significado. Simplificar no significa mezclar permisos con SQL en cualquier endpoint. Conserva las fronteras que protegen cambios y elimina las que solo agregan navegación.

### Tema 06 · Monolito modular y decisión de microservicios

**Caso de uso · Decidir qué separar primero**

Disponibilidad y reservas comparten la regla de no solapamiento; los avisos pueden entregarse después. El equipo propone desplegar todo por separado.

Compararás un módulo con despliegues independientes a partir de transacciones, fallos parciales y necesidad operativa medible.

**Objetivo:** Comparar límites modulares con el costo de separar despliegues.

**Requisitos:** Antipatrones: acoplamiento y efectos ocultos.

#### 01. Qué es y qué problema resuelve

Un módulo separa responsabilidades dentro de un despliegue. Un microservicio añade contratos remotos y operación independiente.

Separar procesos puede permitir autonomía, pero también convierte fallos de red y evolución de mensajes en parte del comportamiento cotidiano.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Contrato o esquema de diseño para analizar; no es una aplicación ejecutable completa. La evidencia principal es la decisión y su justificación. Una comprobación automatizada requiere implementar el control en una solución preparada. Decisión original y revisable; no es una receta universal. Diferencia consistencia crítica de un efecto que puede entregarse después.

```Texto
Decisión: mantener reservas y disponibilidad en un módulo.
Motivo: comparten la garantía de no solapamiento.
Frontera candidata: entrega de avisos mediante outbox.
Revisión: cuando exista necesidad medible de operación independiente.
```

**Cómo funciona:**

1. Identifica la garantía que comparten reservas y disponibilidad.
2. Compara una transacción local con fallos parciales entre servicios.
3. Documenta qué necesidad medible justificaría separar procesos.

**Resultado esperado:** El equipo puede explicar cuándo una separación aporta valor y qué pruebas exigiría.

#### 03. Cuándo usarlo y qué debes evitar

Comienza por límites de dominio y datos. Si dos funciones deben confirmar juntas constantemente, separarlas exige una estrategia de consistencia explícita.

Evalúa equipo, escalado y disponibilidad frente al costo de trazas distribuidas, reintentos, despliegue y recuperación. No uses cantidad de carpetas como evidencia de autonomía.

**Error frecuente:** Más procesos no significan menos acoplamiento: contratos remotos y reintentos pueden hacerlo más difícil de gestionar.

#### 04. Practica lo aprendido

Compara dos despliegues y un módulo para disponibilidad y reservas.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Compara dos despliegues y un módulo para disponibilidad y reservas.
3. Comprueba y registra el resultado: El equipo puede explicar cuándo una separación aporta valor y qué pruebas exigiría.

**Solución orientativa:**

Documenta transacción, fallos parciales y costo operativo. No concluyas solo por una expectativa genérica de escala.

#### 05. Comprueba lo aprendido

**¿Un microservicio elimina el acoplamiento?**

No. Un contrato remoto mal diseñado puede aumentar acoplamiento.

**¿Qué diferencia hace que avisos sea una frontera distinta de disponibilidad?**

Avisos puede procesar trabajo confirmado posteriormente mediante outbox y reintentos. Disponibilidad y reserva comparten una garantía de escritura; separarlas cambia el problema de consistencia.

**Cómo demostrar el objetivo:**

- La decisión explica por qué reservas y disponibilidad conservan un límite común.
- Para avisos identificas contrato, entrega pendiente y fallos que exigirían otra estrategia.

**Qué debes recordar:**

- Un módulo separa responsabilidades dentro de un despliegue.
- Un servicio independiente añade contratos y operación propios.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Monolito modular y antipatrón de complejidad**

Comienza con módulos internos y fronteras de datos explícitas cuando un equipo necesita entregar una aplicación coherente. Separar despliegues exige contratos remotos, observabilidad distribuida, evolución de mensajes y tolerancia a fallos parciales. Un módulo con muchas llamadas síncronas a otro puede empeorar al convertirse en servicio remoto.

Identifica tres señales de sobrearquitectura: interfaces que solo reexpresan una clase; eventos para encadenar pasos que deben ser atómicos; y handlers que atraviesan muchas capas sin agregar significado. Simplificar no significa mezclar permisos con SQL en cualquier endpoint. Conserva las fronteras que protegen cambios y elimina las que solo agregan navegación.

## Ruta 07

### Tema 01 · Autenticación y validación de JWT

**Caso de uso · Rechazar un token para otra API**

Una solicitud trae un JWT legible, pero está destinado a otro servicio. Leer sus claims sin validarlos daría una identidad falsa para la API de reservas.

Configurarás autenticación Bearer y el contrato de emisor y audiencia; comprenderás que proteger el endpoint y disponer de un emisor son pasos adicionales.

**Objetivo:** Validar una identidad antes de usar sus claims.

**Requisitos:** Fundamentos de la ruta anterior.

#### 01. Qué es y qué problema resuelve

Autenticar significa comprobar quién presenta una solicitud. Un JWT firmado puede transportar identidad y permisos, pero su apariencia no prueba autenticidad.

La API debe validar firma, emisor, audiencia y tiempo de validez mediante su biblioteca y un emisor configurado. Leer un token sin validarlo solo decodifica datos controlados por quien lo presenta.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Configura el paquete de JWT Bearer, un emisor de pruebas confiable y RequireAuthorization en el endpoint; no uses la URL ficticia como un emisor real. Fragmento ASP.NET Core que requiere el paquete JwtBearer compatible con .NET 10 y un emisor real HTTPS. La autoridad es configuración confiable del servidor, no entrada del cliente.

```C#
builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = builder.Configuration["Identidad:Autoridad"];
        options.Audience = "reservas-api";
        options.RequireHttpsMetadata = true;
    });
builder.Services.AddAuthorization();
// Tras construir app:
app.UseAuthentication();
app.UseAuthorization();
```

**Cómo funciona:**

1. Configura una autoridad confiable y la audiencia esperada por la API.
2. El handler valida las condiciones del token antes de establecer identidad.
3. Autenticación precede a autorización en el recorrido de la solicitud.

**Resultado esperado:** Los endpoints protegidos solo aceptan tokens válidos para la audiencia configurada.

#### 03. Cuándo usarlo y qué debes evitar

Separa emisión y validación. Una API que recibe tokens de un proveedor debe configurar su autoridad y audiencia; no inventar un protocolo de inicio de sesión.

Evita registrar tokens y enviarlos en URLs. El fallo de autenticación produce 401; una identidad válida sin permiso produce 403.

Estos casos deben tener pruebas diferentes.

**Error frecuente:** Decodificar un JWT permite leer su contenido, pero no demuestra que sus afirmaciones sean auténticas.

#### 04. Practica lo aprendido

Escribe una matriz para token ausente, expirado, audiencia incorrecta y token válido. Si preparas un host y emisor de pruebas, aplícala a un endpoint protegido; el fragmento y el ZIP no incluyen ese entorno.

1. Relaciona Authority y Audience con un emisor confiable y el recurso esperado.
2. Añade RequireAuthorization al endpoint del host de pruebas y prepara tokens de laboratorio sin registrarlos.
3. Documenta resultados esperados; marca como observados solo los casos que hayas ejecutado.

**Solución orientativa:**

Los tres primeros casos deben recibir 401 en el endpoint protegido. Un token válido permite autenticación; el permiso se evalúa aparte y puede producir 403. La lectura del JWT no demuestra su validez.

#### 05. Comprueba lo aprendido

**¿Decodificar un JWT autentica al usuario?**

No. Deben validarse firma y condiciones del token.

**¿Por qué un JWT decodificable para otra audiencia no permite entrar a esta API?**

La audiencia indica a qué recurso se dirige el token. Un JWT legible para otro recurso falla la validación de esta API aunque su formato y firma sean correctos para otro servicio.

**Cómo demostrar el objetivo:**

- La matriz rechaza token ausente, expirado o de audiencia incorrecta en un endpoint protegido.
- Distingues validar identidad de conceder permisos sobre una reserva y no registras credenciales.

**Qué debes recordar:**

- La API valida firma, emisor, audiencia y vigencia.
- Una identidad válida todavía necesita autorización.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Autenticación y validación de tokens**

Autenticación identifica al actor; autorización determina qué acción puede ejecutar sobre qué recurso. Un JWT es un formato de token firmado, no un permiso universal y no cifrado por defecto. Decodificarlo solo permite leer datos: validar exige firma, emisor, audiencia, vigencia y algoritmos permitidos. Confía en claims de grupo solo si un emisor validado controla su asignación.

Usa un proveedor de identidad y protocolos estándar apropiados al cliente. Para una aplicación de navegador considera Authorization Code con PKCE y una arquitectura que limite exposición de tokens. No inventes un servidor de identidad ni un algoritmo criptográfico como práctica inicial. Para cookies de autenticación agrega protección CSRF en acciones con efectos; CORS no cumple ese papel. Para clientes con Bearer, evita almacenamiento accesible a scripts cuando el modelo de amenazas permita alternativas más protegidas.

Un token de acceso debe ser de vida limitada. Un refresh token es una credencial de alto valor: usa almacenamiento protegido, revocación y rotación conforme al proveedor. Si administras tokens opacos en tu propio servicio, almacena una representación verificable como un hash, detecta reutilización de tokens rotados y revoca la familia según política. No agregues un endpoint casero de refresh sin ese diseño.

**Configurar un emisor, sin claves en el código** · Integración con proveedor de identidad; no emite tokens

```C#
builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", opciones =>
    {
        opciones.Authority = builder.Configuration["Identidad:Emisor"];
        opciones.Audience = "reservas-api";
        opciones.RequireHttpsMetadata = true;
    });
builder.Services.AddAuthorization();
// En el pipeline:
app.UseAuthentication();
app.UseAuthorization();
```

Requiere el paquete JwtBearer compatible con ASP.NET Core 10 y un emisor real con metadata y claves. Valida configuración al iniciar. No pega un token dentro del código ni desactiva firma o vigencia.

**Resultado:** Un token para otra audiencia o de emisor no confiable debe rechazarse antes del caso de uso.

### Tema 02 · Autorización mediante políticas

**Caso de uso · Consultar sin gestionar reservas**

Una identidad puede consultar, pero no gestionar reservas. El endpoint de gestión debe aplicar ese límite aunque el portal muestre un botón por error.

Expresarás una capacidad con una política del servidor y compararás identidad ausente, permiso presente y permiso faltante.

**Objetivo:** Expresar permisos de acceso reutilizables en el servidor.

**Requisitos:** Autenticación y validación de JWT.

#### 01. Qué es y qué problema resuelve

Una política expresa una condición de acceso reutilizable. Permite nombrar la intención, por ejemplo gestionar reservas, y concentrar los requisitos.

Un claim pertenece a una identidad autenticada y validada; un parámetro de la solicitud no puede convertirse en permiso porque lo envió el cliente.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Fragmento de registro y endpoint, con autenticación previamente configurada. El claim debe venir de un emisor confiable.

```C#
builder.Services.AddAuthorization(options =>
    options.AddPolicy("GestionarReservas", policy => policy
        .RequireAuthenticatedUser()
        .RequireClaim("permiso", "reservas:gestionar")));
app.MapGet("/api/reservas/gestion", () => Results.Ok("Acceso autorizado"))
   .RequireAuthorization("GestionarReservas");
```

**Cómo funciona:**

1. La política exige una identidad autenticada.
2. RequireClaim comprueba el permiso esperado en esa identidad validada.
3. RequireAuthorization aplica la política al endpoint.

**Resultado esperado:** Identidad con el permiso: 200. Identidad autenticada sin el permiso: 403.

#### 03. Cuándo usarlo y qué debes evitar

Prefiere permisos orientados a acciones cuando los roles cambian según organización. Un rol puede agrupar permisos, pero su nombre no describe por sí mismo todas las operaciones.

Aplica las políticas en el servidor y prueba accesos permitidos y denegados. Ocultar un botón no protege el endpoint.

**Error frecuente:** Un permiso general no decide si una reserva concreta pertenece al grupo del usuario; eso requiere una regla por recurso.

#### 04. Practica lo aprendido

Crea una matriz de accesos para consultar, crear y gestionar reservas.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Crea una matriz de accesos para consultar, crear y gestionar reservas.
3. Comprueba y registra el resultado: Identidad con el permiso: 200. Identidad autenticada sin el permiso: 403.

**Solución orientativa:**

Define una política por capacidad que lo necesite y prueba que añadir permisos de consulta no concede gestión.

#### 05. Comprueba lo aprendido

**¿Una política sustituye la validación del token?**

No. Usa la identidad que entrega la autenticación validada.

**¿Qué respuesta distingue identidad ausente de identidad autenticada sin el permiso?**

El endpoint protegido debe desafiar con 401 cuando falta autenticación. Con identidad válida y permiso insuficiente devuelve 403; con el permiso requerido permite la operación.

**Cómo demostrar el objetivo:**

- La matriz distingue 401 sin autenticación, 403 sin permiso y 200 con permiso válido.
- Añadir capacidad de consulta no concede gestión y el endpoint exige su política.

**Qué debes recordar:**

- Una política representa una capacidad de acceso.
- El permiso debe venir de una identidad confiable.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Permisos por recurso y aislamiento de grupos**

Un rol general como operador no prueba que la reserva pertenezca al grupo del actor. Carga o consulta el recurso en un ámbito autorizado y compara contexto validado con propietario. Para operaciones diferenciadas usa políticas o handlers de autorización por recurso que representen lectura, cancelación y administración. No basta ocultar botones; el servidor aplica la regla.

Una ruta que recibe reservaId puede sufrir acceso a objetos ajenos si comprueba solo autenticación. Ensaya con grupo-lima y grupo-cobre; un usuario de cobre no debe cancelar una reserva de lima aunque conozca su Guid. Revisa efectos secundarios, errores y registros además de la respuesta. Si la política permite una excepción administrativa, representa el permiso explícitamente y audita su uso.

El laboratorio modela una sola regla de propiedad para enseñar denegación predeterminada. No modela membresías múltiples, delegaciones ni un motor completo de políticas. El identificador de grupo se compara de manera exacta; normaliza identificadores en su origen si el negocio requiere otra convención.

**Denegar antes de tocar el estado** · Regla compilada y probada en el laboratorio

```C#
public static bool PuedeCancelar(
    string? grupoAutenticado, Reserva reserva) =>
    !string.IsNullOrWhiteSpace(grupoAutenticado)
    && grupoAutenticado == reserva.GrupoId;
```

El contexto no proviene del cuerpo de la petición. CancelarReserva invoca esta regla antes de Reserva.Cancelar. Una especificación que filtra listados no protege automáticamente este comando.

**Resultado:** true para grupo-lima sobre su reserva; false para grupo-cobre y para identidad sin grupo.

### Tema 03 · Autorización por recurso y aislamiento de grupos

**Caso de uso · Cancelar una reserva de otro grupo**

Una identidad tiene permiso de cancelar y conoce el identificador de una reserva ajena. El permiso general no debe habilitar ese recurso.

Combinarás capacidad y pertenencia vigente, y delimitarás la diferencia entre una función de decisión y su aplicación al cambio persistido.

**Objetivo:** Verificar permiso y pertenencia sobre el recurso solicitado.

**Requisitos:** Autorización mediante políticas.

#### 01. Qué es y qué problema resuelve

Un permiso general no responde si la reserva concreta pertenece al grupo del usuario. La autorización por recurso evalúa la operación sobre un objeto o sobre sus atributos confiables.

Es necesaria cuando dos usuarios comparten el mismo permiso y deben actuar sobre datos distintos.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Ejemplo de consola .NET 10. Integra el fragmento en Program.cs o en sus tipos auxiliares según corresponda; no mezcles declaraciones de tipos antes de instrucciones de nivel superior. No requiere una API ni una base. Ejemplo puro que hace visible la regla. En una API usa IAuthorizationService y handlers para centralizarla; nunca tomes grupoIdentidad del cuerpo enviado.

```C#
static bool PuedeCancelar(string grupoIdentidad, string grupoReserva,
    bool tienePermiso) =>
    tienePermiso && StringComparer.Ordinal.Equals(grupoIdentidad, grupoReserva);

Console.WriteLine(PuedeCancelar("taller-a", "taller-b", true));
Console.WriteLine(PuedeCancelar("taller-a", "taller-a", true));
```

**Cómo funciona:**

1. Obtén el grupo de la identidad validada y el grupo del recurso.
2. Evalúa permiso y pertenencia juntos.
3. Compara acceso al propio grupo con acceso a otro grupo aunque el permiso exista.

**Resultado esperado:** False para otra organización; True para el mismo grupo con permiso.

#### 03. Cuándo usarlo y qué debes evitar

Comprueba pertenencia antes del efecto. El identificador de grupo debe provenir de identidad validada y el recurso debe obtenerse dentro de ese límite o autorizarse explícitamente.

Decide si devolver 403 o 404 para evitar revelar existencia, y aplica esa decisión de forma consistente. El repositorio y el endpoint deben sostener el mismo aislamiento.

**Error frecuente:** Aceptar el grupo enviado por el cliente como identidad permite elegir el ámbito de autorización.

#### 04. Practica lo aprendido

Ejecuta PuedeCancelar con las cuatro combinaciones de grupo propio o ajeno y permiso presente o ausente. Describe la prueba adicional necesaria antes de modificar una reserva persistida.

1. Prepara las cuatro combinaciones con identificadores ficticios.
2. Registra el resultado booleano de cada una.
3. Como ampliación HTTP y de base, comprueba que una denegación no consulta datos ajenos ni escribe estado.

**Solución orientativa:**

Solo grupo propio con permiso produce True. Las otras tres combinaciones dan False. El fragmento comprueba la decisión; no demuestra que un endpoint la aplique antes de guardar.

#### 05. Comprueba lo aprendido

**¿Tener permiso de cancelar autoriza cualquier reserva?**

No. Debe verificarse también el recurso y su pertenencia.

**¿Qué combinación de permiso y pertenencia admite la cancelación?**

Solo mismo grupo y permiso presente produce True. El grupo debe provenir de identidad y recurso confiables, y el servidor debe aplicar la decisión antes del efecto.

**Cómo demostrar el objetivo:**

- La tabla cubre permiso presente y ausente para grupo propio y ajeno.
- La función solo admite grupo propio con permiso; la integración debe verificar además ausencia de escritura ajena.

**Qué debes recordar:**

- El permiso general y el acceso al objeto son comprobaciones distintas.
- La denegación debe impedir cambios y exposición de datos ajenos.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Permisos por recurso y aislamiento de grupos**

Un rol general como operador no prueba que la reserva pertenezca al grupo del actor. Carga o consulta el recurso en un ámbito autorizado y compara contexto validado con propietario. Para operaciones diferenciadas usa políticas o handlers de autorización por recurso que representen lectura, cancelación y administración. No basta ocultar botones; el servidor aplica la regla.

Una ruta que recibe reservaId puede sufrir acceso a objetos ajenos si comprueba solo autenticación. Ensaya con grupo-lima y grupo-cobre; un usuario de cobre no debe cancelar una reserva de lima aunque conozca su Guid. Revisa efectos secundarios, errores y registros además de la respuesta. Si la política permite una excepción administrativa, representa el permiso explícitamente y audita su uso.

El laboratorio modela una sola regla de propiedad para enseñar denegación predeterminada. No modela membresías múltiples, delegaciones ni un motor completo de políticas. El identificador de grupo se compara de manera exacta; normaliza identificadores en su origen si el negocio requiere otra convención.

**Denegar antes de tocar el estado** · Regla compilada y probada en el laboratorio

```C#
public static bool PuedeCancelar(
    string? grupoAutenticado, Reserva reserva) =>
    !string.IsNullOrWhiteSpace(grupoAutenticado)
    && grupoAutenticado == reserva.GrupoId;
```

El contexto no proviene del cuerpo de la petición. CancelarReserva invoca esta regla antes de Reserva.Cancelar. Una especificación que filtra listados no protege automáticamente este comando.

**Resultado:** true para grupo-lima sobre su reserva; false para grupo-cobre y para identidad sin grupo.

### Tema 04 · Renovación de sesión y rotación de tokens

**Caso de uso · Renovar una sesión sin reutilizar el secreto**

Una sesión necesita renovar acceso. Si dos solicitudes consumen el mismo token de renovación, el servidor debe controlar su uso y revocación.

Generarás un secreto aleatorio y su huella; después diseñarás estados y consumo atómico, sin atribuir rotación al fragmento criptográfico.

**Objetivo:** Distinguir generación del secreto, renovación atómica y revocación de una sesión.

**Requisitos:** Autorización por recurso y aislamiento de grupos.

#### 01. Qué es y qué problema resuelve

Un access token breve reduce el tiempo durante el que una credencial robada puede usarse. Un refresh token permite obtener nuevos tokens sin repetir el inicio de sesión; por ello requiere protección y revocación propias.

La renovación debe estar a cargo del proveedor de identidad o de un diseño de sesión explícito.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Ejemplo de consola .NET 10. Integra el fragmento en Program.cs o en sus tipos auxiliares según corresponda; no mezcles declaraciones de tipos antes de instrucciones de nivel superior. No requiere una API ni una base. Ejemplo de generación y huella para tokens opacos de alta entropía. No constituye un servidor OAuth ni implementa rotación o revocación.

```C#
using System.Security.Cryptography;
using System.Text;

string token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
string huella = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
Console.WriteLine($"Longitud de la huella: {huella.Length}");
// Entregar token por canal seguro; persistir huella y metadatos de sesión.
```

**Cómo funciona:**

1. Genera un token opaco con bytes criptográficamente aleatorios.
2. Calcula una huella para almacenarla junto con el estado de sesión.
3. La renovación completa debe consumir y rotar el token atómicamente.

**Resultado esperado:** Una huella hexadecimal de 64 caracteres, sin revelar el token.

#### 03. Cuándo usarlo y qué debes evitar

Si gestionas tokens opacos, genera valores criptográficamente aleatorios, almacena una huella y registra familia, expiración y estado. La rotación debe consumir el anterior y crear el siguiente atómicamente.

Dos renovaciones concurrentes y la reutilización de un token consumido requieren una política documentada. No guardes valores originales en logs.

**Error frecuente:** Este fragmento genera un token y su huella; todavía no implementa rotación, revocación ni un proveedor de identidad.

#### 04. Practica lo aprendido

Genera un token y su huella sin mostrarlos. Diseña la transición vigente → consumido y la revocación de sesión, incluyendo dos renovaciones simultáneas.

1. Comprueba la longitud de la huella sin registrar el secreto.
2. Dibuja estados y define una actualización atómica que solo permita consumir un token vigente una vez.
3. Identifica qué prueba con almacenamiento necesitarías para verificar concurrencia y revocación.

**Solución orientativa:**

La huella hexadecimal tiene 64 caracteres. Generarla no implementa renovación. El servidor debe vincularla con sesión, expiración y estado, y confirmar el consumo de forma atómica; la política distingue reintento legítimo y reutilización sospechosa.

#### 05. Comprueba lo aprendido

**¿Un refresh token es solo un JWT de mayor duración?**

No. Tiene otra finalidad y necesita controles de renovación y revocación.

**¿Qué parte de una renovación segura muestra el código y qué parte falta implementar?**

El código genera un secreto y su huella. Falta gestionar sesión, expiración, consumo atómico, rotación y revocación; medir 64 caracteres no verifica ninguna de esas transiciones.

**Cómo demostrar el objetivo:**

- La huella tiene 64 caracteres y el token no aparece en logs.
- El diseño distingue vigente, consumido y revocado y prevé un único consumo confirmado en almacenamiento.

**Qué debes recordar:**

- Renovar una sesión requiere estado y controles propios.
- El token original no debe aparecer en logs.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**CORS, límites y protección de recursos**

CORS controla qué orígenes pueden leer respuestas desde un navegador; no evita que otro cliente llame a tu API ni es autenticación. Lista orígenes concretos cuando el navegador necesita acceso. Si usas credenciales, no combines origen comodín con esa política. Permite solo métodos y cabeceras necesarios; revisa preflight y llamadas normales.

Rate limiting restringe una dimensión de consumo. Un límite global evita una parte de la carga; una partición por usuario distribuye presupuesto; límites por IP requieren interpretar proxies de confianza y pueden agrupar usuarios legítimos detrás de una red. El limitador en memoria solo cubre una instancia. Para varias instancias considera un control compartido o de borde y documenta garantías.

Protege tamaño del cuerpo, longitud de filtros, número de resultados y tiempo de consultas. Un 429 indica rechazo por cuota; diseña reintentos con espera y evita un ciclo inmediato que empeore la carga. Ningún limitador sustituye índices, autorización o un presupuesto de trabajo por operación.

**CORS acotado para el portal ficticio** · Configuración ilustrativa de navegador

```C#
builder.Services.AddCors(opciones =>
    opciones.AddPolicy("portal", politica => politica
        .WithOrigins("https://portal.example")
        .WithMethods("GET", "POST")
        .WithHeaders("Authorization", "Content-Type")));
// Después de routing y antes de autorización:
app.UseCors("portal");
```

portal.example es un dominio ficticio. Sustitúyelo solo al integrar el entorno real. Esta política no incluye cookies ni habilita AllowCredentials.

**Resultado:** El navegador permite los orígenes definidos; un cliente de consola aún necesita autorización.

### Tema 05 · CORS y límites de confianza del navegador

**Caso de uso · Entender una respuesta bloqueada por el navegador**

El portal autorizado puede consultar disponibilidad, pero otra página no puede leer la respuesta. Un cliente HTTP directo sí la recibe.

Examinarás la política CORS y diferenciarás lectura por el navegador, envío de solicitudes y controles de acceso en la API.

**Objetivo:** Distinguir permisos de lectura del navegador y autorización de la API.

**Requisitos:** Renovación de sesión y rotación de tokens.

#### 01. Qué es y qué problema resuelve

CORS decide qué orígenes pueden leer respuestas mediante JavaScript en navegadores. No autentica usuarios ni bloquea clientes fuera del navegador.

Un origen incluye esquema, host y puerto, y debe definirse según el frontend real. Permitir cualquier origen amplía lectura pública cuando los datos y el modelo de credenciales lo permiten.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Fragmento para una API con bearer token y un portal ficticio. Ajusta orígenes y métodos a tu despliegue; el dominio de ejemplo no es un servicio disponible.

```C#
builder.Services.AddCors(options => options.AddPolicy("Portal", policy =>
    policy.WithOrigins("https://portal.example.test")
          .WithMethods("GET", "POST")
          .WithHeaders("Content-Type", "Authorization")));
app.UseCors("Portal");
// No se habilitan cookies ni credenciales en este ejemplo.
```

**Cómo funciona:**

1. WithOrigins permite el origen específico del portal ficticio.
2. La política restringe métodos y headers usados por el navegador.
3. Comprueba que la autorización sigue siendo necesaria para cualquier cliente.

**Resultado esperado:** El navegador puede leer respuestas desde el origen permitido; la autorización sigue siendo responsabilidad de la API.

#### 03. Cuándo usarlo y qué debes evitar

Si usas cookies entre sitios, debes evaluar además CSRF, SameSite y protección de solicitudes que cambian estado. Si permites credenciales en CORS, usa orígenes específicos; no combines credenciales con comodín.

El preflight tampoco demuestra que la operación de negocio esté autorizada.

**Error frecuente:** CORS no bloquea clientes directos ni sustituye CSRF cuando el modelo de credenciales usa cookies.

#### 04. Practica lo aprendido

Compara una solicitud desde dos orígenes y una petición directa con un cliente HTTP.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Compara una solicitud desde dos orígenes y una petición directa con un cliente HTTP.
3. Comprueba y registra el resultado: El navegador puede leer respuestas desde el origen permitido; la autorización sigue siendo responsabilidad de la API.

**Solución orientativa:**

Explica por qué el cliente directo no depende de CORS y comprueba que todos los clientes siguen necesitando permiso.

#### 05. Comprueba lo aprendido

**¿CORS protege la API frente a un cliente sin navegador?**

No. Es una política aplicada por navegadores.

**¿Por qué una respuesta legible con un cliente HTTP directo no demuestra que CORS esté mal configurado?**

CORS es una política que aplican navegadores al acceso desde otro origen. Un cliente HTTP directo no la aplica; la API sigue obligada a autenticar y autorizar según su contrato.

**Cómo demostrar el objetivo:**

- Explicas el resultado esperado para origen permitido, otro origen y cliente directo.
- Mantienes autenticación y autorización para los clientes aunque CORS no intervenga.

**Qué debes recordar:**

- CORS regula lectura entre orígenes en navegadores.
- La API debe autorizar todas las operaciones por separado.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**CORS, límites y protección de recursos**

CORS controla qué orígenes pueden leer respuestas desde un navegador; no evita que otro cliente llame a tu API ni es autenticación. Lista orígenes concretos cuando el navegador necesita acceso. Si usas credenciales, no combines origen comodín con esa política. Permite solo métodos y cabeceras necesarios; revisa preflight y llamadas normales.

Rate limiting restringe una dimensión de consumo. Un límite global evita una parte de la carga; una partición por usuario distribuye presupuesto; límites por IP requieren interpretar proxies de confianza y pueden agrupar usuarios legítimos detrás de una red. El limitador en memoria solo cubre una instancia. Para varias instancias considera un control compartido o de borde y documenta garantías.

Protege tamaño del cuerpo, longitud de filtros, número de resultados y tiempo de consultas. Un 429 indica rechazo por cuota; diseña reintentos con espera y evita un ciclo inmediato que empeore la carga. Ningún limitador sustituye índices, autorización o un presupuesto de trabajo por operación.

**CORS acotado para el portal ficticio** · Configuración ilustrativa de navegador

```C#
builder.Services.AddCors(opciones =>
    opciones.AddPolicy("portal", politica => politica
        .WithOrigins("https://portal.example")
        .WithMethods("GET", "POST")
        .WithHeaders("Authorization", "Content-Type")));
// Después de routing y antes de autorización:
app.UseCors("portal");
```

portal.example es un dominio ficticio. Sustitúyelo solo al integrar el entorno real. Esta política no incluye cookies ni habilita AllowCredentials.

**Resultado:** El navegador permite los orígenes definidos; un cliente de consola aún necesita autorización.

### Tema 06 · Rate limiting y tratamiento de abuso

**Caso de uso · Acotar una ráfaga de consultas**

Una ráfaga consume veinte permisos en un minuto. Al levantar otra instancia, el equipo espera que ambas compartan el mismo contador, aunque el limitador guarda estado local.

Seguirás una ventana fija sin cola y delimitarás contador por instancia, particiones y una eventual cuota compartida.

**Objetivo:** Limitar solicitudes y reconocer el alcance del contador.

**Requisitos:** CORS y límites de confianza del navegador.

#### 01. Qué es y qué problema resuelve

Limitar solicitudes controla el consumo de capacidad. Una ventana fija permite cierta cantidad por intervalo; otras estrategias suavizan ráfagas o limitan concurrencia.

El diseño necesita identificar qué recurso proteges: entrada de peticiones, operaciones costosas o capacidad compartida entre clientes.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Fragmento de ASP.NET Core. Este límite nombrado comparte veinte permisos por instancia entre solicitudes a las que se aplique; no particiona por usuario.

```C#
using System.Threading.RateLimiting;

builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = 429;
    options.AddFixedWindowLimiter("Consulta", limiter =>
    {
        limiter.PermitLimit = 20;
        limiter.Window = TimeSpan.FromMinutes(1);
        limiter.QueueLimit = 0;
    });
});
app.UseRateLimiter();
app.MapGet("/api/disponibilidad", () => Results.Ok("Disponible"))
   .RequireRateLimiting("Consulta");
```

**Cómo funciona:**

1. La ventana fija define veinte permisos por minuto.
2. QueueLimit cero rechaza las solicitudes que no obtienen permiso.
3. El endpoint aplica el límite nombrado y devuelve 429 al agotarlo.

**Resultado esperado:** Tras consumir permisos en la ventana, las solicitudes adicionales reciben 429.

#### 03. Cuándo usarlo y qué debes evitar

Define particiones y consecuencias con cuidado. Una IP puede representar muchos usuarios detrás de un proxy; una identidad puede faltar antes de autenticar.

Un límite en memoria se aplica por instancia y no equivale a una cuota global. La denegación debe ser observable y comunicarse como 429, con información de reintento cuando sea posible.

**Error frecuente:** El contador se comparte por política dentro de una instancia; no constituye una cuota por usuario ni una cuota global.

#### 04. Practica lo aprendido

En una API preparada, envía una ráfaga superior a veinte solicitudes dentro de una ventana y registra los rechazos. Predice por separado qué ocurriría al agregar otra instancia.

1. Configura el endpoint y el limitador de ventana fija sin cola.
2. Envía solicitudes dentro de la misma ventana y anota estados y tiempos.
3. Explica el contador local y diseña como ampliación una cuota compartida o una partición confiable.

**Solución orientativa:**

Una instancia permite veinte solicitudes por ventana y rechaza las adicionales con 429. El borde de ventana afecta el ensayo. Dos instancias conservan contadores separados; no existe una cuota global ni por usuario en este registro.

#### 05. Comprueba lo aprendido

**¿Este limitador establece una cuota global de veinte solicitudes?**

No. Su estado local no se comparte entre instancias.

**¿Qué cambia en la cuota efectiva al ejecutar este contador local en dos procesos?**

Cada proceso conserva su propia ventana de veinte permisos. Si el tráfico se reparte, el conjunto puede aceptar más de veinte; una cuota global requiere coordinación adicional.

**Cómo demostrar el objetivo:**

- En una instancia, la solicitud que supera los permisos de la ventana recibe 429.
- Explicas por qué dos procesos no forman por sí solos una cuota global de veinte.

**Qué debes recordar:**

- El límite protege una capacidad definida.
- Partición y estado compartido determinan quién consume la cuota.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Prueba negativa como evidencia de seguridad**

Construye una matriz actor × acción × recurso: propietario, miembro autorizado, ajeno, administrador y anónimo. Para cada celda define permitido o denegado. La prueba negativa no solo espera 403: comprueba que el estado, outbox y registros de éxito no cambiaron. Una denegación acompañada por un correo de confirmación también revela un defecto.

Valida token expirado, audiencia incorrecta y firma inválida en pruebas del adaptador de identidad. Separa esas pruebas de la política de recursos usando identidades ficticias en el host de pruebas. Conserva datos de ejemplo y jamás incluyas credenciales reales en fixtures, cuadernos o capturas.

## Ruta 08

### Tema 01 · Pruebas unitarias del comportamiento

**Caso de uso · Detectar un cambio en el borde del aforo**

Una modificación reemplaza <= por <. La sala deja de aceptar exactamente su capacidad aunque muchos casos de prueba siguen pasando.

Construirás pruebas de comportamiento para tres bordes y provocarás un defecto temporal para comprobar que una prueba lo detecta.

**Objetivo:** Probar una regla mediante casos que detecten cambios incorrectos.

**Requisitos:** Fundamentos de la ruta anterior.

#### 01. Qué es y qué problema resuelve

Una prueba unitaria verifica una regla con pocas dependencias y una causa de fallo comprensible. El valor no proviene de cubrir líneas, sino de detectar cambios que romperían una garantía.

Una regla de cancelación, un intervalo inválido o una autorización por grupo tienen entradas y resultados observables.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Ejemplo de consola .NET 10. Integra el fragmento en Program.cs o en sus tipos auxiliares según corresponda; no mezcles declaraciones de tipos antes de instrucciones de nivel superior. No requiere una API ni una base. Ejemplo autónomo con comprobaciones de consola. En el proyecto usa el framework de pruebas elegido y una prueba con nombre por comportamiento.

```C#
static bool PuedeReservar(int asistentes, int capacidad) =>
    asistentes > 0 && asistentes <= capacidad;

if (!PuedeReservar(8, 8)) throw new Exception("Debe admitir aforo exacto");
if (PuedeReservar(9, 8)) throw new Exception("Debe rechazar sobreaforo");
if (PuedeReservar(0, 8)) throw new Exception("Debe rechazar grupo vacío");
Console.WriteLine("Tres límites comprobados");
```

**Cómo funciona:**

1. El caso de aforo exacto comprueba el límite inclusivo.
2. El sobreaforo y el grupo vacío comprueban dos rechazos distintos.
3. Cambiar temporalmente <= por < debe hacer fallar la primera comprobación.

**Resultado esperado:** Las tres comprobaciones pasan; cambiar <= por < debe hacer fallar el caso de aforo exacto.

#### 03. Cuándo usarlo y qué debes evitar

Organiza preparación, acción y comprobación. Usa datos que expliquen el límite, y evita afirmar sobre llamadas internas si el requisito es un resultado de negocio.

No mezcles acceso de red con una prueba del dominio. Una prueba negativa debe explicar qué efecto no ocurrió y por qué.

**Error frecuente:** Una prueba que solo sigue el mismo algoritmo de la implementación puede aprobar un defecto compartido.

#### 04. Practica lo aprendido

Comprueba aforo exacto, sobreaforo y grupo vacío con el ejemplo. Cambia temporalmente <= por < y verifica qué prueba detecta el defecto.

1. Ejecuta los tres casos: 8 de 8, 9 de 8 y 0 de 8.
2. Cambia el límite inclusivo por exclusivo y conserva el fallo del caso exacto.
3. Restaura la regla y verifica las tres comprobaciones; después puedes ampliar a Intervalo y Cancelar.

**Solución orientativa:**

8 de 8 se acepta; 9 y 0 se rechazan. Al usar < falla el aforo exacto. Esta sensibilidad demuestra que el caso protege ese borde; no demuestra ausencia de otros errores.

#### 05. Comprueba lo aprendido

**¿Una cobertura alta demuestra ausencia de errores?**

No. Debes evaluar la calidad de casos y las garantías observadas.

**¿Qué prueba debería fallar si el límite inclusivo se convierte en exclusivo?**

Debe fallar el caso de 8 asistentes con capacidad 8. Sobreaforo y grupo vacío siguen rechazados, por lo que no detectan ese cambio de borde.

**Cómo demostrar el objetivo:**

- La regla acepta 8 de 8 y rechaza 9 y 0.
- El cambio temporal a < hace fallar el caso de aforo exacto y restaurar <= lo recupera.

**Qué debes recordar:**

- Prueba garantías observables y límites.
- Una prueba útil debe detectar un cambio que rompa su requisito.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Una estrategia de pruebas por frontera**

Pruebas unitarias comprueban reglas y casos de uso aislados; integración prueba adaptadores, contratos HTTP y motor de datos; arquitectura verifica dependencias; un pequeño conjunto end to end observa flujos críticos desplegados. El número de mocks no mide calidad: una prueba que replica cada llamada interna puede impedir refactorizar sin detectar un defecto real.

El laboratorio usa 15 comprobaciones ejecutables sin paquetes externos para hacer accesible el primer paso. Falla con una excepción y código de salida distinto de cero si una condición no se cumple. Para un proyecto mayor adopta un framework de pruebas que dé descubrimiento, reportes y paralelización controlada. No presentes este runner como pruebas de integración de EF o HTTP.

En integración de EF prueba traducción de Specification, restricciones y dos conexiones en conflicto con el proveedor real. El proveedor InMemory no reproduce SQL, transacciones ni restricciones del motor. Para HTTP usa un host de pruebas y verifica JSON, estado y efectos. Un adaptador falso de autenticación solo se registra en ese host aislado.

**Una aserción que detecta acceso ajeno** · Fragmento de prueba con tipos del laboratorio

```C#
var reserva = new Reserva(Guid.NewGuid(), "grupo-lima",
    "sala-norte", new Intervalo(inicio, inicio.AddHours(1)));
if (Acceso.PuedeCancelar("grupo-cobre", reserva))
    throw new InvalidOperationException("Acceso cruzado aceptado");
```

La prueba protege una regla observable. Si cambias la condición por true, debe fallar. Ese ensayo de mutación muestra que la comprobación puede detectar el defecto que intenta prevenir.

**Resultado:** La identidad ajena se rechaza; la reserva sigue activa.

### Tema 02 · Pruebas de integración de API y persistencia

**Caso de uso · Verificar creación y lectura a través de HTTP**

La API devuelve 201, pero eso no basta para saber si la reserva quedó guardada ni si otra consulta puede recuperarla.

Prepararás cliente, host y almacenamiento de pruebas y enlazarás respuesta HTTP con estado persistido, reconociendo las garantías del proveedor.

**Objetivo:** Comprobar el recorrido HTTP y su efecto persistido.

**Requisitos:** Pruebas unitarias del comportamiento.

#### 01. Qué es y qué problema resuelve

Una prueba de integración comprueba colaboraciones reales: serialización, middleware, autorización, endpoints y almacenamiento. Puede arrancar la API con WebApplicationFactory y configurar un entorno de pruebas.

Su propósito es detectar diferencias que una prueba aislada no ve, como un contrato HTTP incorrecto o una restricción de base de datos.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. Fragmento de prueba que requiere cliente autenticado, fixture, DTO real, using System.Net.Http.Json y un framework con Assert. Ajusta entrada al contrato completo; no es un programa autónomo.

```C#
using var respuesta = await cliente.PostAsJsonAsync("/api/reservas",
    new { salaId = "sala-7", asistentes = 8 }, ct);
Assert.Equal(HttpStatusCode.Created, respuesta.StatusCode);
Assert.NotNull(respuesta.Headers.Location);
// Consultar Location con la misma identidad y verificar los datos persistidos.
```

**Cómo funciona:**

1. El cliente de pruebas envía una entrada al endpoint configurado.
2. Comprueba estado 201 y la existencia de Location.
3. Consulta el recurso creado y verifica lo persistido con la misma identidad.

**Resultado esperado:** La API devuelve 201 con Location y la consulta posterior recupera la reserva creada.

#### 03. Cuándo usarlo y qué debes evitar

Elige infraestructura según la garantía. Un proveedor en memoria no reproduce transacciones, índices ni traducción SQL de tu base de producción.

Usa una base compatible y datos aislados cuando compruebes consistencia. No ejecutes pruebas contra una base compartida con usuarios.

Registra resultados sin exponer secretos de autenticación.

**Error frecuente:** Un proveedor en memoria no reproduce todos los índices, restricciones ni comportamientos de la base real.

#### 04. Practica lo aprendido

Con host, identidad, cliente y proveedor de pruebas preparados, crea una reserva y consulta Location. Comprueba la fila persistida y limpia los datos entre ejecuciones. Amplía después a autorización y concurrencia.

1. Prepara una API de prueba y datos aislados; el fragmento requiere un fixture y un framework de tests.
2. Envía la creación y comprueba 201 y Location.
3. Consulta el recurso y la base con un contexto nuevo; ejecuta de nuevo tras limpiar datos.

**Solución orientativa:**

201 y Location comprueban el contrato HTTP; consulta y lectura persistida comprueban el efecto. El ZIP de consola no contiene la API ni WebApplicationFactory. Conflictos y restricciones exigen pruebas adicionales con un proveedor que los reproduzca.

#### 05. Comprueba lo aprendido

**¿Una prueba HTTP con base en memoria verifica todos los índices SQL?**

No. Los índices y restricciones deben comprobarse en una base que los soporte.

**¿Qué falta comprobar después de recibir 201 para afirmar que la reserva fue creada?**

Debes consultar Location y verificar el dato persistido, preferentemente con otro contexto. La respuesta 201 sola podría estar emitida aun si el flujo no guardó correctamente.

**Cómo demostrar el objetivo:**

- El test comprueba 201 y Location y consulta después el recurso con la identidad adecuada.
- Verificas la fila persistida y el aislamiento de datos entre ejecuciones con un proveedor apropiado.

**Qué debes recordar:**

- La integración comprueba colaboradores reales.
- El contrato HTTP y los efectos persistidos necesitan evidencia.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Una estrategia de pruebas por frontera**

Pruebas unitarias comprueban reglas y casos de uso aislados; integración prueba adaptadores, contratos HTTP y motor de datos; arquitectura verifica dependencias; un pequeño conjunto end to end observa flujos críticos desplegados. El número de mocks no mide calidad: una prueba que replica cada llamada interna puede impedir refactorizar sin detectar un defecto real.

El laboratorio usa 15 comprobaciones ejecutables sin paquetes externos para hacer accesible el primer paso. Falla con una excepción y código de salida distinto de cero si una condición no se cumple. Para un proyecto mayor adopta un framework de pruebas que dé descubrimiento, reportes y paralelización controlada. No presentes este runner como pruebas de integración de EF o HTTP.

En integración de EF prueba traducción de Specification, restricciones y dos conexiones en conflicto con el proveedor real. El proveedor InMemory no reproduce SQL, transacciones ni restricciones del motor. Para HTTP usa un host de pruebas y verifica JSON, estado y efectos. Un adaptador falso de autenticación solo se registra en ese host aislado.

**Una aserción que detecta acceso ajeno** · Fragmento de prueba con tipos del laboratorio

```C#
var reserva = new Reserva(Guid.NewGuid(), "grupo-lima",
    "sala-norte", new Intervalo(inicio, inicio.AddHours(1)));
if (Acceso.PuedeCancelar("grupo-cobre", reserva))
    throw new InvalidOperationException("Acceso cruzado aceptado");
```

La prueba protege una regla observable. Si cambias la condición por true, debe fallar. Ese ensayo de mutación muestra que la comprobación puede detectar el defecto que intenta prevenir.

**Resultado:** La identidad ajena se rechaza; la reserva sigue activa.

### Tema 03 · Comprobaciones de arquitectura

**Caso de uso · Impedir una referencia hacia afuera**

Una modificación agrega persistencia al proyecto de dominio. El equipo quiere detectar ese acoplamiento durante la revisión y no meses después.

Definirás una regla de referencias y la convertirás en un control automático que falle ante una dependencia prohibida.

**Objetivo:** Definir una regla de dependencias y convertirla en un control automático verificable.

**Requisitos:** Pruebas de integración de API y persistencia.

#### 01. Qué es y qué problema resuelve

Las reglas de dependencias pueden verificarse automáticamente. Si el dominio debe ignorar HTTP y persistencia, una referencia nueva a la capa de infraestructura es una regresión aunque las pruebas funcionales sigan pasando.

Estas comprobaciones hacen explícita la dirección de las dependencias.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Contrato o esquema de diseño para analizar; no es una aplicación ejecutable completa. La evidencia principal es la decisión y su justificación. Una comprobación automatizada requiere implementar el control en una solución preparada. Regla declarativa para la solución del caso ficticio. Puede implementarse con análisis de csproj o una biblioteca de pruebas de arquitectura.

```Texto
Referencias permitidas:
Aplicación -> Dominio
Infraestructura -> Aplicación, Dominio
API -> Aplicación, Infraestructura

Comprobación: Dominio.csproj no contiene ProjectReference a API
ni PackageReference a EntityFrameworkCore o ASP.NET Core.
```

**Cómo funciona:**

1. Lee las referencias permitidas entre proyectos.
2. Comprueba que Dominio no referencia API ni persistencia.
3. Agrega una dependencia prohibida temporalmente y observa el fallo del control.

**Resultado esperado:** Una referencia de dominio hacia infraestructura hace fallar la comprobación.

#### 03. Cuándo usarlo y qué debes evitar

Elige reglas que protejan una decisión existente. Comprueba referencias de proyectos o tipos públicos y registra una excepción solo con motivo claro.

Una prueba que prohíbe un nombre sin distinguir propósito se vuelve frágil. La revisión sigue siendo necesaria para detectar dependencias conceptuales y efectos ocultos.

**Error frecuente:** Una prueba de nombres o carpetas no demuestra que las responsabilidades estén bien diseñadas.

#### 04. Practica lo aprendido

Convierte la regla de referencias del ejemplo en un script o prueba de tu solución. Agrega temporalmente una referencia prohibida al dominio y comprueba fallo y restauración.

1. Prepara los proyectos y define el conjunto de referencias prohibidas.
2. Implementa un control que devuelva error si encuentra una referencia prohibida; el esquema mostrado no lo ejecuta.
3. Introduce la referencia, registra fallo, elimínala y registra éxito.

**Solución orientativa:**

Un control automatizado debe fallar con la referencia y pasar al retirarla. Revisar solo el diagrama es evidencia documental, no una prueba automática. El control sobre csproj tampoco cubre todos los usos indirectos de tipos; delimita su alcance.

#### 05. Comprueba lo aprendido

**¿Las pruebas de arquitectura garantizan un diseño correcto?**

No. Verifican restricciones concretas, no toda la calidad del diseño.

**¿Por qué un diagrama de referencias no demuestra que exista un control automático?**

El diagrama declara una intención. Solo un script o prueba que se ejecute y falle con la referencia prohibida aporta evidencia de que el control automático detecta esa regresión.

**Cómo demostrar el objetivo:**

- La regla enumera referencias permitidas y prohibidas del proyecto de dominio.
- Conservas un resultado de fallo con la referencia prohibida y otro de éxito al restaurar el diseño.

**Qué debes recordar:**

- Automatiza restricciones arquitectónicas concretas.
- La revisión sigue siendo necesaria para evaluar el diseño.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Una estrategia de pruebas por frontera**

Pruebas unitarias comprueban reglas y casos de uso aislados; integración prueba adaptadores, contratos HTTP y motor de datos; arquitectura verifica dependencias; un pequeño conjunto end to end observa flujos críticos desplegados. El número de mocks no mide calidad: una prueba que replica cada llamada interna puede impedir refactorizar sin detectar un defecto real.

El laboratorio usa 15 comprobaciones ejecutables sin paquetes externos para hacer accesible el primer paso. Falla con una excepción y código de salida distinto de cero si una condición no se cumple. Para un proyecto mayor adopta un framework de pruebas que dé descubrimiento, reportes y paralelización controlada. No presentes este runner como pruebas de integración de EF o HTTP.

En integración de EF prueba traducción de Specification, restricciones y dos conexiones en conflicto con el proveedor real. El proveedor InMemory no reproduce SQL, transacciones ni restricciones del motor. Para HTTP usa un host de pruebas y verifica JSON, estado y efectos. Un adaptador falso de autenticación solo se registra en ese host aislado.

**Una aserción que detecta acceso ajeno** · Fragmento de prueba con tipos del laboratorio

```C#
var reserva = new Reserva(Guid.NewGuid(), "grupo-lima",
    "sala-norte", new Intervalo(inicio, inicio.AddHours(1)));
if (Acceso.PuedeCancelar("grupo-cobre", reserva))
    throw new InvalidOperationException("Acceso cruzado aceptado");
```

La prueba protege una regla observable. Si cambias la condición por true, debe fallar. Ese ensayo de mutación muestra que la comprobación puede detectar el defecto que intenta prevenir.

**Resultado:** La identidad ajena se rechaza; la reserva sigue activa.

### Tema 04 · Selección y mantenimiento de bibliotecas

**Caso de uso · Actualizar un paquete sin sorpresas**

Una biblioteca trae dependencias transitivas. Restaurar en otro equipo puede cambiar la resolución si el proyecto no conserva un bloqueo revisado.

Inventariarás paquetes directos y transitivos y usarás un archivo de bloqueo, junto con criterios de licencia, capacidad y mantenimiento.

**Objetivo:** Evaluar paquetes y mantener una resolución reproducible.

**Requisitos:** Comprobaciones de arquitectura.

#### 01. Qué es y qué problema resuelve

Una biblioteca aporta una capacidad y también una dependencia operativa. Evalúa propósito, mantenimiento, licencia, compatibilidad, seguridad y costo de sustitución.

Una lista de paquetes populares no explica qué necesita tu sistema. Antes de instalar, formula una función comprobable que hoy no esté cubierta de manera suficiente.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Ejecuta los comandos en un proyecto o solución .NET. Habilita RestorePackagesWithLockFile y genera y versiona packages.lock.json antes de locked-mode. El análisis de vulnerabilidades depende de los datos disponibles al ejecutar. Comandos de inspección y restauración. Un resultado sin vulnerabilidades conocidas no prueba seguridad total; depende de la información disponible.

```Shell
# En una solución con proyectos .NET:
dotnet list package --include-transitive
dotnet list package --vulnerable --include-transitive
dotnet restore --locked-mode
# locked-mode requiere un archivo de bloqueo creado y versionado previamente.
```

**Cómo funciona:**

1. Lista dependencias directas y transitivas.
2. Consulta vulnerabilidades conocidas y revisa el propósito de cada paquete.
3. Usa locked-mode únicamente después de generar y versionar el archivo de bloqueo.

**Resultado esperado:** Inventario directo y transitivo; restauración falla si el bloqueo no coincide con la declaración.

#### 03. Cuándo usarlo y qué debes evitar

Mantén versiones explícitas y un procedimiento de actualización. El archivo de bloqueo ayuda a reproducir la resolución, pero no vuelve seguros los paquetes.

Revisa dependencias transitivas y cambios de API, y prueba la capacidad usada antes de aceptar una actualización. Usa fuentes confiables y evita confundir paquetes de nombres parecidos.

**Error frecuente:** Fijar versiones mejora reproducibilidad, pero no elimina vulnerabilidades ni la necesidad de actualizar.

#### 04. Practica lo aprendido

En un proyecto de pruebas, inventaría paquetes directos y transitivos. Crea y versiona el archivo de bloqueo antes de exigir locked-mode. Documenta además una decisión de selección de biblioteca.

1. Habilita RestorePackagesWithLockFile y ejecuta restore para generar packages.lock.json.
2. Revisa y versiona el bloqueo; después ejecuta restore --locked-mode.
3. Cambia una versión declarada sin actualizar el bloqueo y comprueba el rechazo; restaura el cambio.

**Solución orientativa:**

El bloqueo conserva una resolución revisable. locked-mode rechaza cambios que requieren actualizarlo. El inventario y la evaluación de capacidad, mantenimiento, vulnerabilidades y licencia siguen siendo necesarios.

#### 05. Comprueba lo aprendido

**¿Fijar una versión elimina la necesidad de actualizar?**

No. Mejora reproducibilidad y exige mantener un proceso de actualización.

**¿Qué preparación necesita locked-mode y qué riesgo no resuelve por sí solo?**

Primero habilita, genera, revisa y versiona packages.lock.json. El modo bloqueado protege la resolución acordada, pero no demuestra seguridad, licencia adecuada ni calidad del paquete.

**Cómo demostrar el objetivo:**

- El inventario distingue dependencias directas y transitivas y documenta una decisión de selección.
- Generas y versionas el bloqueo antes de usar locked-mode y compruebas el rechazo de un cambio incompatible.

**Qué debes recordar:**

- Cada paquete aporta capacidad y costo de mantenimiento.
- La resolución reproducible necesita un proceso de actualización.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Health checks y señales de operación**

Liveness indica si el proceso responde; readiness si está listo para recibir trabajo. Si ambos dependen de una base temporalmente caída, el orquestador puede reiniciar procesos sanos y empeorar una incidencia. El endpoint de disponibilidad debe reflejar dependencias esenciales con tiempo límite; protege resultados detallados del diagnóstico.

No incluyas host, cadena de conexión ni excepción en una respuesta pública de health. Mide tasas de error, latencias por operación y estado de procesamiento del outbox. Diseña un runbook: señal, impacto, comprobaciones, mitigación y criterio de recuperación. Un dashboard no sustituye una persona responsable de responder.

**Liveness básica del proceso** · Fragmento ASP.NET Core; no garantiza readiness

```C#
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health/live");
// Readiness es otro endpoint con chequeos de dependencias esenciales.
// No agregues un chequeo SQL a liveness por defecto.
```

El fragmento solo comprueba que el proceso puede responder; no asegura conectividad de base de datos. Para readiness registra checks concretos y una salida mínima.

**Resultado:** /health/live responde saludable mientras el proceso atiende; una prueba separada cubre readiness.

### Tema 05 · Health checks: vida y preparación

**Caso de uso · Retirar tráfico cuando falla la base**

El proceso sigue vivo, pero su base no responde. Reiniciarlo por cada fallo puede empeorar la situación; seguir enviándole solicitudes tampoco ayuda.

Separarás liveness de readiness y observarás que la preparación solo evalúa comprobaciones realmente registradas.

**Objetivo:** Separar respuesta del proceso y disponibilidad para recibir tráfico.

**Requisitos:** Selección y mantenimiento de bibliotecas.

#### 01. Qué es y qué problema resuelve

Una señal de vida indica que el proceso puede responder. Una señal de preparación indica que puede recibir el tráfico para el que fue diseñado.

Un proceso vivo puede depender de una base indisponible. Confundir ambas señales puede provocar reinicios repetidos o enviar tráfico a un servicio que todavía no está listo.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere un host ASP.NET Core 10 preparado. builder y app corresponden a WebApplication.CreateBuilder y Build; las variables y tipos auxiliares deben definirse. Registra los servicios usados antes de Build. El ZIP de consola no contiene esta API. AddHealthChecks solo prepara el servicio; para comprobar una base debes registrar el check de esa dependencia. Fragmento ASP.NET Core. Sin checks registrados, ready solo confirma la infraestructura de health checks: debes añadir y probar las dependencias requeridas.

```C#
using Microsoft.AspNetCore.Diagnostics.HealthChecks;

builder.Services.AddHealthChecks();
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false
});
app.MapHealthChecks("/health/ready");
// Registrar una comprobación real de la base con etiqueta de preparación.
```

**Cómo funciona:**

1. live excluye comprobaciones de dependencias.
2. ready ejecuta los checks registrados.
3. Añade una comprobación real de base y simula su fallo para verificar la diferencia.

**Resultado esperado:** live no ejecuta checks; ready ejecuta los registrados. La preparación de base no queda implementada en este fragmento.

#### 03. Cuándo usarlo y qué debes evitar

Comprueba dependencias necesarias para atender solicitudes en preparación. Evita consultas costosas y no publiques detalles sensibles del diagnóstico.

El orquestador debe conocer qué endpoint corresponde a cada intención. Decide cómo operar en degradación cuando una dependencia auxiliar falla.

**Error frecuente:** Sin checks registrados, ready no demuestra que la base esté disponible.

#### 04. Practica lo aprendido

Predice qué comprueban live y ready sin checks registrados. Después, en un host de pruebas, registra un check de dependencia que pueda fallar y compara ambos endpoints.

1. Lee el predicado de live y la lista inicialmente vacía de checks.
2. Registra un check de prueba cuyo resultado puedas alternar; para la base se requiere un check específico.
3. Provoca fallo y verifica live disponible y ready no disponible sin detalles sensibles públicos.

**Solución orientativa:**

Sin registros, ready no comprueba la base y puede responder saludable. Con el check que falla, ready refleja indisponibilidad; live omite los checks. La prueba de un check simulado no demuestra conectividad de una base real.

#### 05. Comprueba lo aprendido

**¿Un endpoint health que siempre devuelve 200 verifica la base?**

No. Debe ejecutar una comprobación pertinente y con límite de tiempo.

**¿Por qué ready puede responder saludable en este ejemplo aunque todavía no compruebe la base?**

AddHealthChecks registra el servicio, pero todavía no hay un check de la base. El resultado saludable con lista vacía no demuestra conectividad; registra y prueba esa dependencia.

**Cómo demostrar el objetivo:**

- El diseño mantiene live independiente de la base y vincula ready con una comprobación explícita.
- Distingues el ejemplo sin checks de la prueba de fallo de base después de registrar uno.

**Qué debes recordar:**

- Liveness observa vida del proceso.
- Readiness evalúa las dependencias necesarias para atender tráfico.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Contenedor y pipeline reproducible**

Un contenedor empaqueta la aplicación y su runtime; no resuelve secretos, permisos de base ni migraciones. Usa etapas de compilación y ejecución, una imagen compatible mantenida y un usuario sin privilegios. No copies .git, tokens o archivos de desarrollo; una .dockerignore y revisión del contexto ayudan a controlar el artefacto. Los tags de versión facilitan actualizaciones; fija digest cuando necesites identidad exacta y establece un proceso para renovarlo.

El pipeline restaura dependencias, compila, ejecuta pruebas, analiza dependencias y secretos, publica un artefacto trazable y despliega ese mismo artefacto. Un paso de prueba no valida un proyecto que no contiene pruebas descubiertas. En el laboratorio usa dotnet run para ejecutar sus comprobaciones; al pasar a un framework usa dotnet test y conserva resultados. No recompiles el código después de aprobarlo sin volver a comprobar el resultado.

Separa secretos de build y runtime. Planifica migraciones con compatibilidad entre versiones. Una entrega gradual y health checks pueden reducir impacto, pero la estrategia de rollback debe considerar datos. Restaurar una versión antigua que ya no entiende el esquema nuevo puede fallar aun cuando el binario previo era correcto.

**Ejecutar las comprobaciones antes de publicar** · Comandos verificados con SDK .NET 10

```Shell
dotnet restore Laboratorio.csproj
dotnet build Laboratorio.csproj --configuration Release --no-restore
dotnet run --project Laboratorio.csproj --configuration Release --no-build
# En un proyecto con framework de pruebas:
# dotnet test Reserva.sln --configuration Release
```

Las primeras tres líneas son ejecutables en el ZIP. La última es orientación para tu futura solución, no un comando válido sobre una solución inexistente en el laboratorio.

**Resultado:** Compilación sin advertencias y 15 comprobaciones aprobadas. El proceso devuelve error ante una comprobación fallida.

### Tema 06 · Observabilidad: logs, métricas y trazas

**Caso de uso · Explicar un aumento de conflictos**

El contador de conflictos sube y soporte necesita localizar qué operación falla. Tener un log aislado o crear un ActivitySource no basta para seguir el recorrido.

Relacionarás métricas, trazas y logs, y separarás instrumentar de recolectar y exportar señales observables.

**Objetivo:** Relacionar eventos, mediciones y operaciones sin generar ruido.

**Requisitos:** Health checks: vida y preparación.

#### 01. Qué es y qué problema resuelve

Los logs describen eventos, las métricas cuantifican comportamiento y las trazas relacionan operaciones de una solicitud. Una investigación necesita conectar síntoma e impacto: aumento de latencia, conflictos de reserva, rechazos de acceso o errores de dependencia.

Recoger datos sin preguntas operativas produce ruido.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Fragmento de instrumentación .NET 10. Para observar actividades y mediciones registra listeners o un proveedor de telemetría y exportación. Sin listener, StartActivity puede devolver null. No incluye backend ni dashboard. Ejemplo de APIs de diagnóstico .NET. Una Activity requiere listeners para recogerse; la métrica requiere un consumidor. Añade instrumentación alrededor de la operación real.

```C#
using System.Diagnostics;
using System.Diagnostics.Metrics;

using var fuente = new ActivitySource("Academia.Reservas");
using var medidor = new Meter("Academia.Reservas");
var contador = medidor.CreateCounter<long>("reservas.creadas");
using var actividad = fuente.StartActivity("CrearReserva");
contador.Add(1, new KeyValuePair<string, object?>("resultado", "confirmada"));
```

**Cómo funciona:**

1. ActivitySource nombra el origen de las operaciones trazadas.
2. Meter crea un contador y una etiqueta de resultado de baja cardinalidad.
3. Configura listeners y exportación para observar esa instrumentación fuera del proceso.

**Resultado esperado:** Se emite una medición de creación confirmada; la exportación depende de configurar instrumentación y backend.

#### 03. Cuándo usarlo y qué debes evitar

Define nombres y cardinalidad. Una etiqueta con un identificador diferente por reserva crea series sin límite; un contador agregado por resultado es más sostenible.

Protege los datos que registras y el acceso a la plataforma. La instrumentación local necesita un exportador y una configuración de muestreo para ser útil fuera del proceso.

**Error frecuente:** Crear instrumentos no garantiza que aparezcan en un panel: faltan consumidores y configuración de exportación.

#### 04. Practica lo aprendido

Relaciona un conteo de conflictos con una traza y un log de correlación. Identifica lo que debe configurarse para recolectar y exportar las señales del fragmento.

1. Define qué pregunta responde cada señal y sus etiquetas permitidas.
2. Distingue crear instrumentos de registrar listeners, recolectores y exportadores.
3. Diseña una verificación en el backend de pruebas y marca como propuesta lo que no hayas observado.

**Solución orientativa:**

La métrica resume resultados, la traza sitúa la operación y el log aporta detalle por correlación. StartActivity puede devolver null sin listener; la exportación no está implementada en el fragmento. No uses IDs de reserva como etiquetas de alta cardinalidad.

#### 05. Comprueba lo aprendido

**¿Crear ActivitySource garantiza que una traza aparezca en un panel?**

No. Deben existir listeners, exportación y configuración.

**¿Qué falta para observar una actividad creada si no hay ningún listener configurado?**

Un listener que seleccione la fuente y una configuración de recolección y exportación. Sin listener, StartActivity puede devolver null; crear el instrumento no asegura datos visibles en un panel.

**Cómo demostrar el objetivo:**

- El diseño conecta un conteo por resultado con una operación y su correlación.
- Identificas listener y exportación pendientes y evitas secretos e identificadores de alta cardinalidad en etiquetas.

**Qué debes recordar:**

- Logs, métricas y trazas responden preguntas distintas.
- La instrumentación necesita un destino y límites de cardinalidad.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Contenedor y pipeline reproducible**

Un contenedor empaqueta la aplicación y su runtime; no resuelve secretos, permisos de base ni migraciones. Usa etapas de compilación y ejecución, una imagen compatible mantenida y un usuario sin privilegios. No copies .git, tokens o archivos de desarrollo; una .dockerignore y revisión del contexto ayudan a controlar el artefacto. Los tags de versión facilitan actualizaciones; fija digest cuando necesites identidad exacta y establece un proceso para renovarlo.

El pipeline restaura dependencias, compila, ejecuta pruebas, analiza dependencias y secretos, publica un artefacto trazable y despliega ese mismo artefacto. Un paso de prueba no valida un proyecto que no contiene pruebas descubiertas. En el laboratorio usa dotnet run para ejecutar sus comprobaciones; al pasar a un framework usa dotnet test y conserva resultados. No recompiles el código después de aprobarlo sin volver a comprobar el resultado.

Separa secretos de build y runtime. Planifica migraciones con compatibilidad entre versiones. Una entrega gradual y health checks pueden reducir impacto, pero la estrategia de rollback debe considerar datos. Restaurar una versión antigua que ya no entiende el esquema nuevo puede fallar aun cuando el binario previo era correcto.

**Ejecutar las comprobaciones antes de publicar** · Comandos verificados con SDK .NET 10

```Shell
dotnet restore Laboratorio.csproj
dotnet build Laboratorio.csproj --configuration Release --no-restore
dotnet run --project Laboratorio.csproj --configuration Release --no-build
# En un proyecto con framework de pruebas:
# dotnet test Reserva.sln --configuration Release
```

Las primeras tres líneas son ejecutables en el ZIP. La última es orientación para tu futura solución, no un comando válido sobre una solución inexistente en el laboratorio.

**Resultado:** Compilación sin advertencias y 15 comprobaciones aprobadas. El proceso devuelve error ante una comprobación fallida.

### Tema 07 · Contenedores y configuración de ejecución

**Caso de uso · Ejecutar el laboratorio en otro equipo**

Queremos entregar la consola del laboratorio sin incluir el SDK en la imagen final. El nombre de la DLL y el contexto de construcción deben coincidir con el proyecto.

Leerás una construcción multietapa y comprobarás entrada, usuario y salida, distinguiendo la consola de una aplicación HTTP.

**Objetivo:** Separar compilación y ejecución en una imagen de contenedor.

**Requisitos:** Observabilidad: logs, métricas y trazas.

#### 01. Qué es y qué problema resuelve

Una imagen empaqueta una aplicación con su entorno de ejecución. Un build multietapa utiliza el SDK para compilar y deja el runtime en la imagen final.

Esto reduce componentes de ejecución, pero no sustituye la actualización de imágenes, el control de permisos ni el diseño de configuración.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Requiere Docker y los archivos extraídos del laboratorio en el contexto de construcción, incluido Laboratorio.csproj. Esta imagen ejecuta una consola y termina; una API necesita una imagen ASP.NET, su entrada y configuración HTTP. La construcción debe verificarse en tu entorno. Dockerfile didáctico para la consola Laboratorio.csproj del ZIP. Requiere Docker y el contexto correcto; una API necesita la imagen aspnet y configuración de puertos. La imagen no fue ejecutada como parte de este material.

```Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o /out

FROM mcr.microsoft.com/dotnet/runtime:10.0
WORKDIR /app
COPY --from=build /out ./
USER $APP_UID
ENTRYPOINT ["dotnet", "Laboratorio.dll"]
```

**Cómo funciona:**

1. La primera etapa usa el SDK para restaurar y publicar.
2. La segunda copia el artefacto a una imagen de runtime.
3. USER establece el usuario de ejecución y ENTRYPOINT inicia la consola.

**Resultado esperado:** Al construir y ejecutar correctamente, la consola muestra sus comprobaciones y termina. Fija digests para un proceso de entrega reproducible.

#### 03. Cuándo usarlo y qué debes evitar

Construye un artefacto reproducible y ejecútalo con un usuario sin privilegios cuando sea compatible. Mantén secretos fuera de Dockerfile y capas.

Excluye bin, obj y archivos sensibles del contexto con .dockerignore. Comprueba arranque, señales de terminación y persistencia externa según la aplicación; una consola del laboratorio termina al acabar sus comprobaciones.

**Error frecuente:** Esta imagen corresponde a la consola del ZIP; una API requiere la imagen aspnet y configuración de HTTP.

#### 04. Practica lo aprendido

Crea .dockerignore y construye la consola del laboratorio en tu entorno.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Crea .dockerignore y construye la consola del laboratorio en tu entorno.
3. Comprueba y registra el resultado: Al construir y ejecutar correctamente, la consola muestra sus comprobaciones y termina. Fija digests para un proceso de entrega reproducible.

**Solución orientativa:**

Comprueba nombre de DLL, usuario, salida y código de retorno. Explica qué cambiaría para servir HTTP con una API.

#### 05. Comprueba lo aprendido

**¿La imagen runtime puede alojar cualquier API ASP.NET Core sin cambios?**

No. Para ASP.NET Core utiliza la imagen y configuración adecuadas.

**¿Por qué la imagen de esta consola no prepara por sí sola una API HTTP?**

La consola usa runtime y termina al acabar sus comprobaciones. Una API requiere el runtime ASP.NET, su propio punto de entrada y configuración HTTP, además de verificaciones operativas.

**Cómo demostrar el objetivo:**

- El Dockerfile compila con SDK y copia la publicación a una imagen runtime.
- La ejecución de la consola termina con código de éxito y sin SDK en la etapa final; requiere Docker disponible.

**Qué debes recordar:**

- Un build multietapa separa herramientas y runtime.
- El contexto de construcción debe excluir secretos y resultados previos.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Entregable y criterios de cierre**

Entrega un repositorio con instrucciones desde cero, modelo de dominio, contratos HTTP, decisión de arquitectura, estrategia de solapamientos y matriz de permisos. Añade evidencia de pruebas de lectura/escritura, aislamiento entre grupos y conflictos concurrentes. Registra versiones y cómo actualizar paquetes; explica qué fragmentos adaptaste y con qué proveedor los verificaste.

La definición de terminado exige que otra persona pueda ejecutar el proyecto, que una entrada inválida no persista, que una identidad ajena no produzca efectos y que un fallo interno no filtre detalles. En producción exige alertas accionables, migraciones ensayadas y recuperación practicada. Este material enseña a construir esa evidencia; completar una lectura no certifica que una implementación futura ya cumpla todos los criterios.

### Tema 08 · Integración continua y entrega verificable

**Caso de uso · Bloquear una revisión que rompe una regla**

Un cambio rompe una comprobación del laboratorio. Necesitamos que la revisión falle y conservar su relación con el commit antes de pensar en entregar un artefacto.

Recorrerás restauración, compilación y comprobaciones en CI y delimitarás los pasos adicionales de publicación, entrega y reversión.

**Objetivo:** Verificar una revisión en CI y distinguir los pasos necesarios para entregar su artefacto.

**Requisitos:** Contenedores y configuración de ejecución.

#### 01. Qué es y qué problema resuelve

La integración continua ejecuta controles sobre una revisión identificable. Una entrega agrega un artefacto versionado y una transición de entorno.

El objetivo es poder responder qué código se probó y qué artefacto se está ejecutando. Compilar otra vez con entradas distintas durante el despliegue rompe esa relación.

#### 02. Ejemplo paso a paso



**Antes de ejecutar:** Workflow para un repositorio con Laboratorio.csproj en la raíz. Ejecuta las comprobaciones de consola con dotnet run; no incluye un framework de tests, publicación de artefacto ni despliegue. Requiere un servicio compatible con este formato de workflow. Workflow didáctico para la consola del ZIP en la raíz. El comando run falla si una comprobación lanza una excepción. Una solución con tests debe añadir dotnet test. Versiones de actions contrastadas con documentación oficial; fija SHA revisados para producción. El workflow no fue ejecutado en GitHub.

```YAML
name: Verificar laboratorio
on: [push, pull_request]
permissions:
  contents: read
jobs:
  verificar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with:
          dotnet-version: '10.0.301'
      - run: dotnet restore
      - run: dotnet build -c Release --no-restore
      - run: dotnet run -c Release --no-build
```

**Cómo funciona:**

1. El workflow obtiene la revisión y configura el SDK indicado.
2. Restore y build preparan la consola; run ejecuta sus comprobaciones.
3. Un fallo debe impedir aceptar esa revisión y conservar evidencia de la ejecución.

**Resultado esperado:** Cada revisión debe compilar y pasar las comprobaciones. No incluye publicación ni secretos de despliegue.

#### 03. Cuándo usarlo y qué debes evitar

Configura SDK y dependencias reproducibles, permisos mínimos y pruebas pertinentes. El flujo debe fallar cuando falla un control y conservar evidencia útil.

Una entrega de producción requiere además configuración, migraciones compatibles, health checks y una estrategia de reversión. Aprobar un pipeline no garantiza automáticamente esas propiedades.

**Error frecuente:** Esta consola usa dotnet run; una solución con un framework de tests debe incluir dotnet test.

#### 04. Practica lo aprendido

Introduce un defecto que rompa una comprobación y verifica que el flujo rechaza la revisión.

1. Prepara el ejemplo con las dependencias indicadas en «Antes de ejecutar».
2. Introduce un defecto que rompa una comprobación y verifica que el flujo rechaza la revisión.
3. Comprueba y registra el resultado: Cada revisión debe compilar y pasar las comprobaciones. No incluye publicación ni secretos de despliegue.

**Solución orientativa:**

Conserva la referencia del commit y el resultado. Diseña luego entrega del mismo artefacto, migraciones seguras y reversión.

#### 05. Comprueba lo aprendido

**¿CI y CD son exactamente el mismo control?**

No. CI verifica integración; entrega y despliegue agregan decisiones y controles de entorno.

**¿Qué parte de CI/CD automatiza este flujo y qué parte sigue pendiente?**

Automatiza restauración, compilación y comprobaciones de consola en CI. No publica ni despliega un artefacto; entrega, configuración, migraciones y reversión siguen siendo pasos por diseñar.

**Cómo demostrar el objetivo:**

- La revisión defectuosa falla en las comprobaciones y la restaurada pasa.
- Identificas el commit verificado y separas ese control del diseño de entrega del mismo artefacto.

**Qué debes recordar:**

- CI comprueba una revisión identificable.
- La entrega debe desplegar el mismo artefacto verificado.

**Tu evidencia:** entradas utilizadas, resultado observado, caso de rechazo y limitaciones de la prueba.

#### Ampliación opcional

**Entregable y criterios de cierre**

Entrega un repositorio con instrucciones desde cero, modelo de dominio, contratos HTTP, decisión de arquitectura, estrategia de solapamientos y matriz de permisos. Añade evidencia de pruebas de lectura/escritura, aislamiento entre grupos y conflictos concurrentes. Registra versiones y cómo actualizar paquetes; explica qué fragmentos adaptaste y con qué proveedor los verificaste.

La definición de terminado exige que otra persona pueda ejecutar el proyecto, que una entrada inválida no persista, que una identidad ajena no produzca efectos y que un fallo interno no filtre detalles. En producción exige alertas accionables, migraciones ensayadas y recuperación practicada. Este material enseña a construir esa evidencia; completar una lectura no certifica que una implementación futura ya cumpla todos los criterios.
