Las dos lecciones anteriores dieron a BiblioTech una interfaz de escritorio, primero con Windows Forms y después con WPF —ambas exclusivas de Windows y pensadas para un único usuario ejecutando la aplicación en su propio ordenador—. ASP.NET Core cambia radicalmente ese modelo: en vez de una ventana, expone la lógica de BiblioTech como un servicio web, accesible por HTTP desde cualquier cliente —un navegador, una aplicación móvil, otro servicio, o el propio HttpClient que ya usaste en el Módulo 5 para consumir una API externa—. Esta lección presenta ASP.NET Core centrándose en las Minimal APIs, el estilo más moderno y directo de definir endpoints HTTP en .NET, y construye una API real de BiblioTech con operaciones para consultar el catálogo y registrar préstamos.

Contenido

  1. Qué es ASP.NET Core y qué problema resuelve
  2. Minimal APIs frente a Controllers
  3. Crear un proyecto con dotnet new webapi
  4. Enrutamiento y verbos HTTP: app.MapGet/app.MapPost
  5. Inyección de dependencias básica en el contenedor de servicios
  6. Devolver JSON: retomando System.Text.Json del Módulo 5
  7. Ejemplo completo: API mínima de BiblioTech con BibliotecaDbContext
  8. Probar la API

  1. Qué es ASP.NET Core y qué problema resuelve

ASP.NET Core es el framework de .NET para construir aplicaciones y servicios web: desde APIs que solo devuelven datos (el foco de esta lección) hasta sitios web completos con páginas HTML renderizadas en el servidor. Su pieza central es el servidor web integrado (Kestrel), que escucha peticiones HTTP entrantes y las despacha al código C# que las atiende:

flowchart LR
    C["Cliente HTTP<br/>(navegador, app movil, HttpClient...)"] -->|"GET /libros"| K["Kestrel<br/>(servidor web de ASP.NET Core)"]
    K --> E["Endpoint C#<br/>app.MapGet(...)"]
    E --> B["Biblioteca<br/>(dominio ya existente)"]
    E -->|JSON| K
    K -->|"Respuesta HTTP"| C

A diferencia de Windows Forms y WPF, una aplicación ASP.NET Core no tiene ventana ni interfaz visual propia: se ejecuta como un proceso en segundo plano (en un servidor, en un contenedor, o localmente durante el desarrollo) y responde a peticiones. Esto la hace multiplataforma —corre igual en Windows, Linux o macOS— y adecuada precisamente para el papel que necesitaba BiblioTech: un punto central al que distintos clientes (una futura app Blazor, una app MAUI, un tercero) puedan pedir datos del catálogo o registrar préstamos, sin duplicar la lógica de dominio en cada uno de ellos.

  1. Minimal APIs frente a Controllers

ASP.NET Core ofrece dos estilos para definir endpoints HTTP:

Minimal APIs Controllers
Cómo se definen los endpoints Funciones lambda o métodos registrados directamente sobre app Clases Controller con métodos decorados con atributos ([HttpGet], [HttpPost])
Cantidad de código para un endpoint simple Mínima: una línea por endpoint Mayor: una clase completa, con su propia estructura
Origen histórico Introducidas en .NET 6 como alternativa más ligera El estilo original de ASP.NET Core (y de ASP.NET MVC, anterior a .NET Core)
Cuándo conviene APIs pequeñas o medianas, microservicios, prototipos APIs grandes con muchos endpoints relacionados, que se benefician de la organización en clases

Esta lección se centra en Minimal APIs por ser el estilo más simple y directo para empezar, y porque encaja bien con el tamaño de la API de BiblioTech en esta etapa del curso. Los Controllers no desaparecen ni quedan obsoletos: para una API con decenas de endpoints organizados por recurso, la estructura en clases de los Controllers puede resultar más cómoda de mantener a largo plazo —una decisión de diseño, no una jerarquía de "mejor y peor" opción.

  1. Crear un proyecto con dotnet new webapi

dotnet new webapi -n BiblioTech.Api --use-minimal-apis
cd BiblioTech.Api

El fichero central de una Minimal API es Program.cs, donde se configura y arranca todo:

var builder = WebApplication.CreateBuilder(args);

// aqui se registran servicios en el contenedor (apartado 5)

var app = builder.Build();

// aqui se definen los endpoints (apartado 4)

app.Run();

WebApplication.CreateBuilder(args) prepara un builder con la configuración por defecto (lectura de appsettings.json, sistema de logging, etc.); builder.Build() construye la aplicación (app) a partir de esa configuración; app.Run() arranca Kestrel y se queda escuchando peticiones hasta que el proceso se detiene. Todo el código que registra servicios va antes de Build(); todo el código que define endpoints va después.

  1. Enrutamiento y verbos HTTP: app.MapGet/app.MapPost

Cada endpoint se registra indicando el verbo HTTP, la ruta, y una función que lo atiende:

app.MapGet("/libros", () =>
{
    return new List<string> { "Rayuela", "Ficciones" }; // simplificado, se completa en el apartado 7
});

app.MapGet("/libros/{isbn}", (string isbn) =>
{
    // {isbn} en la ruta se vincula automaticamente al parametro "isbn" de la funcion
    return $"Buscando el libro con ISBN {isbn}";
});

app.MapPost("/prestamos", (string isbn, int idSocio) =>
{
    return $"Prestamo registrado: ISBN {isbn}, socio {idSocio}";
});
Verbo HTTP Método de ASP.NET Core Uso típico
GET app.MapGet(ruta, funcion) Consultar datos, sin modificar nada en el servidor
POST app.MapPost(ruta, funcion) Crear algo nuevo (aquí, un préstamo)
PUT app.MapPut(ruta, funcion) Reemplazar un recurso existente
DELETE app.MapDelete(ruta, funcion) Eliminar un recurso

{isbn} dentro de la ruta es un parámetro de ruta: ASP.NET Core extrae automáticamente ese segmento de la URL (/libros/978-84-376-0495-4) y lo pasa como argumento string isbn a la función, con conversión de tipo automática si el parámetro fuera, por ejemplo, int. Este mecanismo de enlace de parámetros (binding) es análogo, en espíritu, al data binding de WPF: ambos evitan tener que extraer y convertir datos manualmente.

  1. Inyección de dependencias básica en el contenedor de servicios

ASP.NET Core trae integrado un contenedor de inyección de dependencias: un registro central de servicios (builder.Services) del que la aplicación obtiene instancias, en vez de que cada endpoint construya sus propias dependencias con new:

var builder = WebApplication.CreateBuilder(args);

// Registra Biblioteca como servicio unico compartido durante toda la vida de la aplicacion
builder.Services.AddSingleton<Biblioteca>();

var app = builder.Build();

app.MapGet("/libros", (Biblioteca biblioteca) =>
{
    // "biblioteca" se recibe ya construida: ASP.NET Core la resuelve del contenedor automaticamente
    return biblioteca.Catalogo;
});

AddSingleton<Biblioteca>() le dice al contenedor: "crea una única instancia de Biblioteca y reutilízala en toda petición que la necesite". Basta con declarar Biblioteca biblioteca como parámetro de la función del endpoint para que ASP.NET Core la resuelva e inyecte automáticamente, sin que el endpoint tenga que saber cómo se construyó. Este mecanismo —inyección de dependencias— es un pilar central del diseño de aplicaciones .NET modernas, y se estudiará en profundidad en el Módulo 8 (Mejores Prácticas y Patrones de Diseño); aquí basta con reconocer el patrón básico: registrar en builder.Services, recibir como parámetro en el endpoint.

  1. Devolver JSON: retomando System.Text.Json del Módulo 5

Cuando un endpoint devuelve un objeto o una colección (como biblioteca.Catalogo en el ejemplo anterior), ASP.NET Core lo serializa automáticamente a JSON usando System.Text.Json —la misma librería ya usada en el Módulo 5 con JsonSerializer.Serialize— sin que el endpoint tenga que llamarla explícitamente:

app.MapGet("/libros", (Biblioteca biblioteca) =>
{
    return biblioteca.Catalogo; // ASP.NET Core serializa esta lista a JSON automaticamente
});
[
  { "titulo": "Rayuela", "autor": "Julio Cortazar", "disponible": true },
  { "titulo": "Ficciones", "autor": "Jorge Luis Borges", "disponible": true }
]

Por defecto, ASP.NET Core usa camelCase para las claves del JSON de salida (titulo, no Titulo), la misma convención que se vio con JsonNamingPolicy.CamelCase en el Módulo 5 —aquí aplicada automáticamente por el framework, sin configuración adicional—. Si el endpoint necesita control explícito sobre el código de estado HTTP de la respuesta, puede devolver un IResult con Results.Ok(...), Results.NotFound() o Results.BadRequest(...) en vez de devolver el objeto directamente, como se ve en el apartado 7.

  1. Ejemplo completo: API mínima de BiblioTech con BibliotecaDbContext

Uniendo las piezas anteriores con la persistencia en base de datos ya construida en el Módulo 5 (BibliotecaDbContext, con Entity Framework Core), una API mínima realista para BiblioTech:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<BibliotecaDbContext>(opciones =>
    opciones.UseSqlite("Data Source=bibliotech.db"));

var app = builder.Build();

app.MapGet("/libros", async (BibliotecaDbContext contexto) =>
{
    List<Libro> libros = await contexto.Libros.ToListAsync(); // EF Core, Modulo 5
    return Results.Ok(libros);
});

app.MapGet("/libros/{isbn}", async (string isbn, BibliotecaDbContext contexto) =>
{
    Libro? libro = await contexto.Libros.FirstOrDefaultAsync(l => l.Isbn == isbn);

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

    return Results.Ok(libro);
});

app.MapPost("/prestamos", async (SolicitudPrestamo solicitud, BibliotecaDbContext contexto) =>
{
    Libro? libro = await contexto.Libros.FirstOrDefaultAsync(l => l.Isbn == solicitud.Isbn);
    Socio? socio = await contexto.Socios.FindAsync(solicitud.IdSocio);

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

    if (!libro.Disponible)
    {
        return Results.BadRequest($"'{libro.Titulo}' ya esta prestado.");
    }

    libro.Prestar(); // logica de dominio ya existente, Modulo 2
    Prestamo prestamo = new Prestamo(libro, socio);
    contexto.Prestamos.Add(prestamo);
    await contexto.SaveChangesAsync(); // EF Core, Modulo 5

    return Results.Created($"/prestamos/{prestamo.Id}", prestamo);
});

app.Run();
// SolicitudPrestamo.cs: un DTO (Data Transfer Object) sencillo para el cuerpo del POST
record SolicitudPrestamo(string Isbn, int IdSocio);

Ningún endpoint reimplementa la lógica de dominio: GET /libros y GET /libros/{isbn} solo consultan BibliotecaDbContext (ya construido en el Módulo 5), y POST /prestamos llama a libro.Prestar() exactamente igual que lo haría la consola o el formulario de Windows Forms. La API es, de nuevo, una capa de acceso nueva sobre el mismo dominio, ahora accesible por HTTP en vez de por teclado o clic de ratón. SolicitudPrestamo es un record (Módulo 3) que modela el cuerpo JSON esperado en el POST; ASP.NET Core lo deserializa automáticamente desde el cuerpo de la petición, sin código adicional, aplicando el mismo mecanismo de binding del apartado 4.

  1. Probar la API

Con el proyecto en ejecución (dotnet run), ASP.NET Core expone típicamente una interfaz de prueba interactiva (Swagger/OpenAPI) en una URL como https://localhost:5001/swagger, además de poder probarse directamente con curl o con HttpClient (Módulo 5):

curl https://localhost:5001/libros

curl -X POST https://localhost:5001/prestamos \
  -H "Content-Type: application/json" \
  -d '{"isbn": "978-84-376-0495-4", "idSocio": 1}'

-H "Content-Type: application/json" indica al servidor que el cuerpo de la petición (-d) es JSON, para que ASP.NET Core lo deserialice correctamente en el parámetro SolicitudPrestamo del endpoint POST /prestamos.

Errores Comunes y Consejos

  • Registrar BibliotecaDbContext con AddSingleton en vez de AddDbContext: un DbContext no está pensado para compartirse como instancia única entre peticiones concurrentes; AddDbContext (visto aquí) gestiona correctamente crear una instancia nueva por petición.
  • Reimplementar la comprobación de Disponible dentro del endpoint en vez de delegarla en libro.Prestar(): duplicaría lógica ya resuelta en el dominio desde el Módulo 2, con riesgo de desincronizarse con el tiempo.
  • Confundir Minimal APIs con "sin inyección de dependencias": Minimal APIs usa el mismo contenedor de servicios que los Controllers; solo cambia cómo se registran los endpoints, no cómo funciona la inyección de dependencias por debajo.
  • Consejo: para una API pequeña como esta, Minimal APIs mantiene todo el enrutamiento visible en Program.cs; si el número de endpoints creciera mucho, conviene agruparlos en ficheros separados con app.MapGroup(...) para no acabar con un único fichero enorme.

Ejercicios

  1. Añade un endpoint GET /socios/{id} que devuelva el Socio correspondiente (con Results.Ok) o Results.NotFound si no existe ningún socio con ese id.

  2. Añade un endpoint POST /devoluciones que reciba un record SolicitudDevolucion(int IdPrestamo), busque el Prestamo correspondiente, llame a RegistrarDevolucion() (Módulo 2) y guarde los cambios con SaveChangesAsync(). Devuelve Results.NotFound si el préstamo no existe.

Soluciones

app.MapGet("/socios/{id}", async (int id, BibliotecaDbContext contexto) =>
{
    Socio? socio = await contexto.Socios.FindAsync(id);

    if (socio is null)
    {
        return Results.NotFound($"No existe ningun socio con id {id}.");
    }

    return Results.Ok(socio);
});
record SolicitudDevolucion(int IdPrestamo);

app.MapPost("/devoluciones", async (SolicitudDevolucion solicitud, BibliotecaDbContext contexto) =>
{
    Prestamo? prestamo = await contexto.Prestamos.FindAsync(solicitud.IdPrestamo);

    if (prestamo is null)
    {
        return Results.NotFound($"No existe ningun prestamo con id {solicitud.IdPrestamo}.");
    }

    prestamo.RegistrarDevolucion(); // Modulo 2
    await contexto.SaveChangesAsync();

    return Results.Ok(prestamo);
});

Conclusión

En esta lección BiblioTech ha dejado de ser una aplicación de un único usuario y un único ordenador: con ASP.NET Core y Minimal APIs, su lógica de dominio —sin ningún cambio— queda accesible por HTTP mediante endpoints GET/POST, con inyección de dependencias básica para obtener BibliotecaDbContext en cada petición y serialización JSON automática de las respuestas.

La siguiente lección presenta Blazor, que retoma directamente esta API (o, según el modelo elegido, se conecta al dominio de forma aún más directa) para construir una interfaz de usuario web con C# en vez de JavaScript —cerrando el círculo entre el data binding ya visto en WPF y el mundo del navegador.

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