Con los requisitos y la planificación de la lección anterior ya cerrados, esta lección ensambla la Iteración 2 completa: la API de BiblioTech. No se escribe lógica de dominio nueva —todo el comportamiento ya existe en Biblioteca, en IRepositorioBiblioteca y en sus implementaciones—; el trabajo consiste en organizar ese código en una estructura de solución con varios proyectos, registrar correctamente las dependencias en el contenedor de DI de ASP.NET Core (retomando 07-03 y 08-03), y exponer un endpoint por cada uno de los siete requisitos funcionales definidos en la Lección 2.

Contenido

  1. Estructura de la solución: varios proyectos .NET
  2. BiblioTech.Dominio: el modelo ya construido, sin cambios
  3. BiblioTech.Persistencia: IRepositorioBiblioteca y sus cuatro implementaciones
  4. BiblioTech.Api: registrando servicios en el contenedor de DI
  5. Endpoints: catálogo, socios, préstamos, devoluciones, metadatos
  6. Program.cs completo
  7. Aplicando patrones y estándares ya vistos

  1. Estructura de la solución: varios proyectos .NET

Hasta ahora, cada lección del curso trabajó con un único proyecto de consola o un único proyecto de la tecnología de turno. Un proyecto real de este tamaño se organiza mejor en varios proyectos .NET separados, agrupados en una solución (.sln), cada uno con una responsabilidad propia —el mismo principio de responsabilidad única de 08-01, aplicado ahora a nivel de proyecto en vez de a nivel de clase:

dotnet new sln -n BiblioTech

dotnet new classlib -n BiblioTech.Dominio
dotnet new classlib -n BiblioTech.Persistencia
dotnet new webapi -n BiblioTech.Api
dotnet new xunit -n BiblioTech.Pruebas

dotnet sln add BiblioTech.Dominio BiblioTech.Persistencia BiblioTech.Api BiblioTech.Pruebas

dotnet add BiblioTech.Persistencia reference BiblioTech.Dominio
dotnet add BiblioTech.Api reference BiblioTech.Dominio BiblioTech.Persistencia
dotnet add BiblioTech.Pruebas reference BiblioTech.Dominio BiblioTech.Persistencia
flowchart TD
    Api["BiblioTech.Api<br/>(ASP.NET Core Minimal API)"] --> Persistencia["BiblioTech.Persistencia<br/>(IRepositorioBiblioteca + 4 implementaciones)"]
    Api --> Dominio["BiblioTech.Dominio<br/>(Biblioteca, MaterialBibliotecario, Socio, Prestamo...)"]
    Persistencia --> Dominio
    Pruebas["BiblioTech.Pruebas<br/>(xUnit + Moq, Modulo 8)"] --> Dominio
    Pruebas --> Persistencia

Las flechas del diagrama son direcciones de dependencia (dotnet add reference): Dominio no depende de ningún otro proyecto —ni siquiera sabe que existe persistencia o una API—, mientras que Persistencia y Api dependen de él. Esta dirección no es casualidad: es exactamente el mismo principio que motivó extraer IRepositorioBiblioteca en 08-03, ahora aplicado a la organización de carpetas y proyectos, no solo a las clases dentro de un único proyecto. Un proyecto opcional BiblioTech.Web (un cliente Blazor) se trataría exactamente igual: dependería solo de lo estrictamente necesario para consumir la API por HTTP, sin referenciar directamente BiblioTech.Dominio ni BiblioTech.Persistencia (el cliente no conoce esas clases: solo conoce los DTOs que la API expone en JSON).

  1. BiblioTech.Dominio: el modelo ya construido, sin cambios

Este proyecto recibe, sin modificar ni una línea de comportamiento, las clases construidas en los Módulos 2 a 4 y las políticas de sanción del Módulo 8:

BiblioTech.Dominio/
├── MaterialBibliotecario.cs   // abstract class, Modulo 3
├── Libro.cs                   // Modulo 3
├── Revista.cs                 // Modulo 3
├── Socio.cs                   // Modulo 3, con SaldoPendiente y AplicarSancion (08-05, ejercicio 2)
├── Prestamo.cs                // Modulo 3, con CalcularSancion(IPoliticaSancion) (08-02)
├── IPrestable.cs              // Modulo 4
├── IBuscable.cs               // Modulo 4
├── IPoliticaSancion.cs        // Modulo 8, con SancionFija y SancionProgresiva
└── Biblioteca.cs              // Modulo 4, con Catalogo/Socios/Prestamos y PrestarLibroAsync

Trasladar estas clases a un proyecto propio es, en sí mismo, un ejemplo de "extraer clase" a mayor escala (08-05, apartado 4): el dominio queda físicamente separado de cualquier detalle de persistencia o de infraestructura web, algo que antes solo se conseguía con disciplina dentro de un único proyecto y ahora lo impone la propia estructura de la solución.

  1. BiblioTech.Persistencia: IRepositorioBiblioteca y sus cuatro implementaciones

Este proyecto recibe, igualmente sin cambios de comportamiento, la interfaz y las cuatro clases construidas en 08-03:

BiblioTech.Persistencia/
├── IRepositorioBiblioteca.cs
├── RepositorioTexto.cs
├── RepositorioJson.cs
├── RepositorioSqlite.cs
├── RepositorioEntityFramework.cs
└── BibliotecaDbContext.cs     // Modulo 5, Entity Framework Core

La Lección 2 decidió Entity Framework Core como mecanismo de persistencia de producción; las otras tres implementaciones no se eliminan, simplemente no se registran en el contenedor de DI de BiblioTech.Api (apartado 4). Siguen disponibles, por ejemplo, para pruebas manuales rápidas sin base de datos, tal como ya se vio en 08-03, apartado 7.

  1. BiblioTech.Api: registrando servicios en el contenedor de DI

Con la estructura de proyectos ya en su sitio, el registro de servicios en Program.cs es prácticamente el mismo que se construyó en 08-03, apartado 7, ahora con el resto de endpoints del apartado 5 añadidos:

var builder = WebApplication.CreateBuilder(args);

// Persistencia: Entity Framework Core sobre SQLite, decision de la Leccion 2
builder.Services.AddDbContext<BibliotecaDbContext>(opciones =>
    opciones.UseSqlite(builder.Configuration.GetConnectionString("BiblioTech")));

// IRepositorioBiblioteca resuelto como RepositorioEntityFramework (08-03)
builder.Services.AddScoped<IRepositorioBiblioteca, RepositorioEntityFramework>();

// Biblioteca depende de IRepositorioBiblioteca, mismo ciclo de vida que BibliotecaDbContext (08-03)
builder.Services.AddScoped<Biblioteca>();

// Politica de sancion: Strategy del Modulo 8, sin estado propio, valida como Singleton
builder.Services.AddSingleton<IPoliticaSancion, SancionFija>();

Nótese la diferencia deliberada frente al builder.Configuration.GetConnectionString(...) de 07-03, donde la cadena de conexión estaba escrita directamente en el código: aquí se lee de la configuración (appsettings.json), un adelanto de la configuración por entorno que se completará en la Lección 5, y una aplicación directa del requisito no funcional de seguridad básica de la Lección 2 (no dejar cadenas de conexión de producción escritas en el código fuente).

  1. Endpoints: catálogo, socios, préstamos, devoluciones, metadatos

Cada endpoint corresponde, de forma directa, a uno de los siete requisitos funcionales (RF1-RF7) de la Lección 2:

Endpoint Requisito Qué hace
GET /libros RF3 (parcial: listar) Devuelve biblioteca.Catalogo
GET /libros/buscar?q=... RF3 Filtra Catalogo con LINQ (Módulo 4)
POST /libros RF1 Añade un material con AgregarMaterial
DELETE /libros/{isbn} RF2 Elimina un material del catálogo
POST /socios RF4 Añade un socio con AgregarSocio
POST /prestamos RF5 Registra un préstamo con PrestarLibroAsync
POST /prestamos/{id}/devolucion RF6 Llama a RegistrarDevolucion() y CalcularSancion(politica)
GET /libros/{isbn}/metadatos RF7 Llama a ObtenerMetadatosPorIsbnAsync (Módulo 5)

  1. Program.cs completo

Uniendo todas las piezas anteriores, así queda el Program.cs completo de BiblioTech.Api:

using BiblioTech.Dominio;
using BiblioTech.Persistencia;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// --- Registro de servicios (contenedor de DI, 08-03) ---
builder.Services.AddDbContext<BibliotecaDbContext>(opciones =>
    opciones.UseSqlite(builder.Configuration.GetConnectionString("BiblioTech")));
builder.Services.AddScoped<IRepositorioBiblioteca, RepositorioEntityFramework>();
builder.Services.AddScoped<Biblioteca>();
builder.Services.AddSingleton<IPoliticaSancion, SancionFija>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); // interfaz de prueba interactiva, 07-03 apartado 8

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// --- RF3: listar y buscar en el catalogo ---
app.MapGet("/libros", (Biblioteca biblioteca) =>
{
    biblioteca.CargarCatalogo();
    return Results.Ok(biblioteca.Catalogo);
});

app.MapGet("/libros/buscar", (string q, Biblioteca biblioteca) =>
{
    biblioteca.CargarCatalogo();
    List<MaterialBibliotecario> resultados = biblioteca.Catalogo
        .Where(m => m.Titulo.Contains(q, StringComparison.OrdinalIgnoreCase)
                 || m.Autor.Contains(q, StringComparison.OrdinalIgnoreCase))
        .ToList(); // LINQ, Modulo 4

    return Results.Ok(resultados);
});

// --- RF1: alta de material ---
app.MapPost("/libros", (Libro libro, Biblioteca biblioteca) =>
{
    biblioteca.AgregarMaterial(libro);
    biblioteca.GuardarCatalogo();
    return Results.Created($"/libros/{libro.Isbn}", libro);
});

// --- RF2: baja de material ---
app.MapDelete("/libros/{isbn}", (string isbn, Biblioteca biblioteca) =>
{
    biblioteca.CargarCatalogo();
    MaterialBibliotecario? material = biblioteca.Catalogo
        .FirstOrDefault(m => m is Libro libro && libro.Isbn == isbn);

    if (material is null)
    {
        return Results.NotFound($"No existe ningun libro con ISBN {isbn}.");
    }

    biblioteca.Catalogo.Remove(material);
    biblioteca.GuardarCatalogo();
    return Results.NoContent();
});

// --- RF4: alta de socio ---
app.MapPost("/socios", (Socio socio, Biblioteca biblioteca) =>
{
    biblioteca.AgregarSocio(socio);
    return Results.Created($"/socios/{socio.Id}", socio);
});

// --- RF5: registrar prestamo ---
app.MapPost("/prestamos", async (SolicitudPrestamo solicitud, Biblioteca biblioteca) =>
{
    Libro? libro = biblioteca.Catalogo.OfType<Libro>()
        .FirstOrDefault(l => l.Isbn == solicitud.Isbn);
    Socio? socio = biblioteca.BuscarSocioPorId(solicitud.IdSocio);

    if (libro is null || socio is null)
    {
        return Results.BadRequest("ISBN o socio no encontrados.");
    }

    try
    {
        await biblioteca.PrestarLibroAsync(libro, socio); // valida disponibilidad y persiste (08-05)
    }
    catch (InvalidOperationException ex)
    {
        return Results.Conflict(ex.Message);
    }

    return Results.Ok(biblioteca.Prestamos.Last());
});

// --- RF6: registrar devolucion, con calculo de sancion ---
app.MapPost("/prestamos/{id}/devolucion", (int id, Biblioteca biblioteca, IPoliticaSancion politica) =>
{
    Prestamo? prestamo = biblioteca.Prestamos.ElementAtOrDefault(id);

    if (prestamo is null)
    {
        return Results.NotFound($"No existe el prestamo #{id}.");
    }

    prestamo.RegistrarDevolucion();
    decimal sancion = prestamo.CalcularSancion(politica);

    if (sancion > 0)
    {
        prestamo.Socio.AplicarSancion(sancion); // 08-05, ejercicio 2
    }

    biblioteca.GuardarCatalogo();
    return Results.Ok(new { prestamo.FechaDevolucion, Sancion = sancion });
});

// --- RF7: consulta de metadatos externos ---
app.MapGet("/libros/{isbn}/metadatos", async (string isbn, Biblioteca biblioteca) =>
{
    MetadatosLibroExterno? metadatos = await biblioteca.ObtenerMetadatosPorIsbnAsync(isbn);

    return metadatos is not null
        ? Results.Ok(metadatos)
        : Results.NotFound($"No hay metadatos externos disponibles para el ISBN {isbn}.");
});

app.Run();

// DTOs (Modulo 3, record): el cuerpo esperado de un POST /prestamos
record SolicitudPrestamo(string Isbn, int IdSocio);

Ningún endpoint contiene lógica de negocio propia: cada uno se limita a extraer datos de la petición HTTP (el mismo mecanismo de binding de 07-03), llamar a un método ya existente de Biblioteca o de sus dependencias, y traducir el resultado a una respuesta HTTP con Results.Ok/Results.NotFound/Results.Conflict. Esa es, precisamente, la señal de que la arquitectura desacoplada del Módulo 8 funcionó como se esperaba: la API es una capa fina sobre un dominio que ya sabía hacer todo esto desde mucho antes.

  1. Aplicando patrones y estándares ya vistos

Este Program.cs, aunque nuevo como fichero, no introduce ninguna práctica que no se haya visto ya en el curso:

  • Inyección por constructor/parámetro (08-03): cada endpoint recibe Biblioteca, IPoliticaSancion o IRepositorioBiblioteca ya resueltos por el contenedor, nunca los crea con new.
  • Strategy (08-02): IPoliticaSancion permite cambiar de SancionFija a SancionProgresiva con una única línea en el registro de servicios, sin tocar el endpoint de devolución.
  • Manejo de excepciones como control de flujo esperado (Módulo 2, 08-05): el try/catch sobre InvalidOperationException en POST /prestamos traduce un error de dominio esperado (libro no disponible) a un código HTTP concreto (409 Conflict), en vez de dejar que se propague como un error 500 genérico.
  • Nomenclatura consistente (08-01): SolicitudPrestamo, AgregarMaterial, GuardarCatalogo siguen exactamente las mismas convenciones ya establecidas en la Lección 1 del Módulo 8.

Errores Comunes y Consejos

  • Referenciar BiblioTech.Api desde BiblioTech.Dominio: invertiría la dirección de dependencia del diagrama del apartado 1; el dominio nunca debe conocer la existencia de la capa web que lo consume.
  • Registrar IPoliticaSancion como AddScoped en vez de AddSingleton: SancionFija no guarda ningún estado propio entre llamadas (apartado 4 de 08-03), así que puede compartirse de forma segura durante toda la vida de la aplicación; usar un ciclo de vida más corto del necesario no es incorrecto, pero desperdicia el motivo de ser de AddSingleton.
  • Escribir lógica de negocio directamente dentro de un endpoint (por ejemplo, calcular la sanción a mano en vez de llamar a prestamo.CalcularSancion(politica)): duplicaría lógica que ya existe y ya está probada (Módulo 8, Lección 4), rompiendo la ventaja principal de reutilizar el dominio existente.
  • Consejo: si un endpoint empieza a superar 4-5 líneas de lógica propia (sin contar la extracción de parámetros ni la traducción a Results.*), es una señal de "método largo" (08-05) a nivel de endpoint: probablemente falta extraer ese fragmento a un método de Biblioteca.

Ejercicios

  1. Añade un endpoint GET /socios/{id} que devuelva los datos de un socio concreto usando biblioteca.BuscarSocioPorId(id), devolviendo 404 Not Found si no existe.

  2. El endpoint POST /prestamos/{id}/devolucion del apartado 6 usa biblioteca.Prestamos.ElementAtOrDefault(id) para localizar el préstamo por posición en la lista. Explica por qué esto es frágil en un sistema real, y qué cambiarías en el modelo de Prestamo para resolverlo de forma más robusta.

Soluciones

app.MapGet("/socios/{id}", (int id, Biblioteca biblioteca) =>
{
    Socio? socio = biblioteca.BuscarSocioPorId(id);

    return socio is not null
        ? Results.Ok(socio)
        : Results.NotFound($"No existe ningun socio con Id {id}.");
});

Usar la posición dentro de biblioteca.Prestamos como si fuera un identificador es frágil porque esa posición cambia si se elimina algún préstamo de la lista, o si el orden de inserción varía (por ejemplo, al recargar el catálogo desde el repositorio); dos peticiones distintas podrían referirse, sin darse cuenta, a préstamos distintos. La solución robusta es añadir una propiedad Id propia a Prestamo (siguiendo el mismo patrón que ya tiene Socio.Id, Módulo 3), asignada de forma única al crear cada préstamo, y buscar por ese Id en vez de por posición.

Conclusión

Esta lección ha ensamblado la Iteración 2 completa del proyecto: una estructura de solución con varios proyectos .NET con responsabilidades separadas, el registro de IRepositorioBiblioteca y Biblioteca en el contenedor de DI de ASP.NET Core retomando 07-03 y 08-03, y un Program.cs completo con un endpoint por cada uno de los siete requisitos funcionales de la Lección 2, sin ninguna lógica de negocio nueva que no existiera ya en el dominio de BiblioTech. La API funciona, pero todavía no se ha verificado con pruebas más allá de las unitarias del Módulo 8, ni se ha revisado con ningún criterio de calidad formal. La siguiente lección, Pruebas y Depuración, completa ese trabajo: amplía la batería de pruebas con pruebas de integración sobre estos mismos endpoints, e introduce el checklist de calidad que debe superarse antes de pasar a la Lección 5, Despliegue.

Curso de Programación en C#

Módulo 1: Introducción a C#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados