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
- Qué es ASP.NET Core y qué problema resuelve
- Minimal APIs frente a Controllers
- Crear un proyecto con
dotnet new webapi - Enrutamiento y verbos HTTP:
app.MapGet/app.MapPost - Inyección de dependencias básica en el contenedor de servicios
- Devolver JSON: retomando
System.Text.Jsondel Módulo 5 - Ejemplo completo: API mínima de BiblioTech con
BibliotecaDbContext - Probar la API
- 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.
- 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.
- Crear un proyecto con
dotnet new webapi
dotnet new webapiEl 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.
- Enrutamiento y verbos HTTP:
app.MapGet/app.MapPost
app.MapGet/app.MapPostCada 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.
- 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.
- Devolver JSON: retomando
System.Text.Json del Módulo 5
System.Text.Json del Módulo 5Cuando 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.
- Ejemplo completo: API mínima de BiblioTech con
BibliotecaDbContext
BibliotecaDbContextUniendo 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.
- 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
BibliotecaDbContextconAddSingletonen vez deAddDbContext: unDbContextno 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
Disponibledentro del endpoint en vez de delegarla enlibro.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 conapp.MapGroup(...)para no acabar con un único fichero enorme.
Ejercicios
-
Añade un endpoint
GET /socios/{id}que devuelva elSociocorrespondiente (conResults.Ok) oResults.NotFoundsi no existe ningún socio con eseid. -
Añade un endpoint
POST /devolucionesque reciba unrecord SolicitudDevolucion(int IdPrestamo), busque elPrestamocorrespondiente, llame aRegistrarDevolucion()(Módulo 2) y guarde los cambios conSaveChangesAsync(). DevuelveResults.NotFoundsi 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#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
