Hasta ahora, BiblioTech ha guardado y recuperado su propio estado: en texto plano, en JSON, y en
una base de datos relacional. Pero ninguna aplicación real vive aislada: casi siempre necesita
comunicarse con otros sistemas a través de la red, típicamente mediante una API REST
—un servicio que expone datos y operaciones sobre HTTP, con JSON como formato habitual de
intercambio. Esta lección profundiza en System.Text.Json para escenarios más complejos que los
vistos hasta ahora, y presenta HttpClient, la clase de .NET para realizar peticiones HTTP,
consumiendo un servicio externo (simulado) que devuelve metadatos adicionales de un libro a
partir de su ISBN. Cierra así el Módulo 5 (Trabajando con Datos), la última pieza antes del
Módulo 6, dedicado a temas más avanzados del lenguaje y la plataforma.
Contenido
- Repaso y profundización:
System.Text.Jsonen escenarios más complejos - Colecciones anidadas y opciones de nomenclatura con
JsonNamingPolicy HttpClient: la puerta de entrada a servicios HTTP- Consumir un
GETy deserializar la respuesta - Enviar datos con
POST - Buenas prácticas con
HttpClient: reutilización eIHttpClientFactory - Manejo de errores de red y HTTP
- BiblioTech consulta metadatos externos de un libro por ISBN
- Repaso y profundización:
System.Text.Json en escenarios más complejos
System.Text.Json en escenarios más complejosLa lección de Serialización presentó JsonSerializer.Serialize/Deserialize sobre objetos
sencillos y colecciones planas. El JSON que devuelve una API externa real, sin embargo, suele
tener una estructura más rica: objetos anidados dentro de otros objetos, listas dentro de un
objeto, y convenciones de nombres que no coinciden con PascalCase. Esta lección retoma
exactamente esos casos.
- Colecciones anidadas y opciones de nomenclatura con
JsonNamingPolicy
JsonNamingPolicyConsidera una respuesta típica de una API externa de metadatos de libros, con una lista anidada de géneros y un objeto anidado con datos de la editorial:
{
"isbn": "978-84-376-0495-4",
"editorial": { "nombre": "Sudamericana", "pais": "Argentina" },
"generos": ["Novela", "Literatura latinoamericana"],
"puntuacionMedia": 4.6
}Para deserializar esta estructura, se modelan las clases anidadas tal cual reflejan el JSON:
class Editorial
{
public string Nombre { get; set; } = string.Empty;
public string Pais { get; set; } = string.Empty;
}
class MetadatosLibroExterno
{
public string Isbn { get; set; } = string.Empty;
public Editorial Editorial { get; set; } = new Editorial();
public List<string> Generos { get; set; } = new List<string>();
public double PuntuacionMedia { get; set; }
}JsonSerializer.Deserialize<MetadatosLibroExterno>(json) reconstruye automáticamente tanto el
objeto anidado (Editorial) como la lista (Generos), sin ningún código adicional: EF Core,
en la lección anterior, y System.Text.Json, aquí, comparten la misma filosofía de mapear
estructuras completas por convención.
Muchas APIs reales usan camelCase (puntuacionMedia, no PuntuacionMedia) en sus claves
JSON, en vez del PascalCase habitual de las propiedades de C#. En lugar de anotar cada
propiedad con [JsonPropertyName] una a una (visto en la lección de Serialización),
JsonSerializerOptions.PropertyNamingPolicy aplica la conversión a todas las propiedades de
golpe:
var opciones = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
MetadatosLibroExterno? metadatos = JsonSerializer.Deserialize<MetadatosLibroExterno>(json, opciones);
Console.WriteLine(metadatos?.Editorial.Nombre); // "Sudamericana"
string jsonGenerado = JsonSerializer.Serialize(metadatos, opciones);
// las claves se generan en camelCase: "isbn", "editorial", "generos", "puntuacionMedia"PropertyNamingPolicy funciona en ambas direcciones (serializar y deserializar), y es la
opción recomendada frente a [JsonPropertyName] cuando toda una clase sigue la misma
convención de nombres; reserva [JsonPropertyName] para las excepciones puntuales dentro de
una clase que, por lo demás, sigue la convención por defecto.
HttpClient: la puerta de entrada a servicios HTTP
HttpClient: la puerta de entrada a servicios HTTPHttpClient (del namespace System.Net.Http) es la clase de .NET para realizar peticiones
HTTP: GET para obtener datos, POST para enviarlos, y el resto de verbos HTTP habituales
(PUT, DELETE...). Su uso básico consiste en crear una instancia, indicar (opcionalmente) una
BaseAddress, y realizar peticiones contra rutas relativas a ella:
using System.Net.Http;
HttpClient cliente = new HttpClient
{
BaseAddress = new Uri("https://api.bibliotech-externo.example/")
};El paquete System.Net.Http.Json (incluido de serie en .NET moderno) añade métodos de
extensión que combinan la petición HTTP con la deserialización JSON en una sola llamada:
GetFromJsonAsync<T>, PostAsJsonAsync<T>, evitando el paso intermedio de leer el cuerpo de
la respuesta como texto y deserializarlo aparte.
- Consumir un
GET y deserializar la respuesta
GET y deserializar la respuestausing System.Net.Http.Json;
MetadatosLibroExterno? metadatos =
await cliente.GetFromJsonAsync<MetadatosLibroExterno>("libros/978-84-376-0495-4");
if (metadatos is not null)
{
Console.WriteLine($"Editorial: {metadatos.Editorial.Nombre} ({metadatos.Editorial.Pais})");
Console.WriteLine($"Puntuacion media: {metadatos.PuntuacionMedia}");
}GetFromJsonAsync<T> realiza la petición GET, comprueba que la respuesta fue satisfactoria, y
deserializa el cuerpo JSON directamente al tipo T indicado —los tres pasos que, con
JsonSerializer a secas y un HttpClient sin la extensión .Json, requerirían escribirse por
separado (GetAsync, leer el cuerpo con ReadAsStringAsync, y JsonSerializer.Deserialize).
- Enviar datos con
POST
POSTPara enviar datos (por ejemplo, registrar en un servicio externo que BiblioTech ha añadido un
libro nuevo al catálogo), PostAsJsonAsync<T> serializa el objeto indicado a JSON y lo envía
como cuerpo de la petición:
class NuevoLibroExterno
{
public string Isbn { get; set; } = string.Empty;
public string Titulo { get; set; } = string.Empty;
}
NuevoLibroExterno nuevoLibro = new NuevoLibroExterno
{
Isbn = "978-84-376-0497-8",
Titulo = "El Aleph"
};
HttpResponseMessage respuesta = await cliente.PostAsJsonAsync("libros", nuevoLibro);
if (respuesta.IsSuccessStatusCode)
{
Console.WriteLine("Libro registrado en el servicio externo.");
}respuesta.IsSuccessStatusCode es true para cualquier código de estado HTTP 2xx (200, 201,
204...); es la forma habitual de comprobar, sin necesidad de leer el código numérico exacto, si
una petición tuvo éxito.
- Buenas prácticas con
HttpClient: reutilización e IHttpClientFactory
HttpClient: reutilización e IHttpClientFactoryA diferencia de SqliteConnection o StreamReader, HttpClient no debe crearse con
using en cada petición y desecharse inmediatamente después. Aunque HttpClient también
implementa IDisposable, crear una instancia nueva por cada petición puede agotar los sockets
disponibles del sistema operativo bajo carga (cada HttpClient desechado deja su conexión de
red en un estado de cierre que tarda un tiempo en liberarse del todo):
| Patrón | Correcto para... |
|---|---|
Una única instancia de HttpClient, reutilizada durante toda la vida de la aplicación (o de un componente de larga duración) |
Aplicaciones de consola y scripts sencillos, como los de este curso |
IHttpClientFactory (inyectado mediante inyección de dependencias) |
Aplicaciones ASP.NET Core y otros escenarios con muchas peticiones concurrentes (Módulo 7) |
new HttpClient() dentro de un using, en cada petición |
Evitar: puede agotar los sockets disponibles bajo carga sostenida |
Para el alcance de este curso —una aplicación de consola como BiblioTech— basta con crear
una única instancia de HttpClient (por ejemplo, como campo static readonly de la clase
que la usa) y reutilizarla en todas las llamadas; IHttpClientFactory resuelve el mismo
problema de forma más sofisticada en aplicaciones ASP.NET Core, donde el ciclo de vida de los
componentes es distinto, un tema que se retoma en el Módulo 7 (Construcción de Aplicaciones).
- Manejo de errores de red y HTTP
Una petición HTTP puede fallar de dos formas muy distintas, y conviene distinguirlas:
| Tipo de fallo | Excepción / síntoma | Ejemplo |
|---|---|---|
| Fallo de red | HttpRequestException (u otra excepción de red) |
El servidor no responde, no hay conexión a Internet |
| Respuesta HTTP de error | La petición se completa, pero con un código 4xx/5xx | El recurso no existe (404), error del servidor (500) |
try
{
HttpResponseMessage respuesta = await cliente.GetAsync("libros/isbn-inexistente");
respuesta.EnsureSuccessStatusCode(); // lanza HttpRequestException si el codigo no es 2xx
MetadatosLibroExterno? metadatos =
await respuesta.Content.ReadFromJsonAsync<MetadatosLibroExterno>();
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Error al consultar el servicio externo: {ex.Message}");
}
catch (TaskCanceledException)
{
Console.WriteLine("La peticion ha superado el tiempo de espera.");
}EnsureSuccessStatusCode() convierte un código de error HTTP en una excepción
HttpRequestException, para poder tratarlo con el mismo try/catch que cualquier otro error
(recordando el Manejo de Excepciones del Módulo 2), en vez de comprobar manualmente el código
numérico en cada llamada. TaskCanceledException puede producirse si la petición supera el
tiempo de espera configurado en HttpClient.Timeout, un escenario habitual cuando el servicio
externo no responde a tiempo.
- BiblioTech consulta metadatos externos de un libro por ISBN
Uniendo todo lo anterior, Biblioteca gana un método que consulta un servicio externo
(simulado, como el Task.Delay de la lección de Programación Asíncrona lo era para una
verificación lenta) para enriquecer la información de un Libro con datos que BiblioTech no
guarda por sí misma:
class Biblioteca
{
// ... Catalogo, Socios, Prestamos, metodos anteriores del curso sin cambios ...
private static readonly HttpClient ClienteHttp = new HttpClient
{
BaseAddress = new Uri("https://api.bibliotech-externo.example/")
};
private static readonly JsonSerializerOptions OpcionesJsonExterno = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public async Task<MetadatosLibroExterno?> ObtenerMetadatosPorIsbnAsync(string isbn)
{
try
{
HttpResponseMessage respuesta = await ClienteHttp.GetAsync($"libros/{isbn}");
respuesta.EnsureSuccessStatusCode();
return await respuesta.Content.ReadFromJsonAsync<MetadatosLibroExterno>(OpcionesJsonExterno);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"No se pudieron obtener metadatos externos para '{isbn}': {ex.Message}");
return null;
}
}
}Libro rayuela = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
biblioteca.AgregarMaterial(rayuela);
MetadatosLibroExterno? metadatos = await biblioteca.ObtenerMetadatosPorIsbnAsync(rayuela.Isbn);
if (metadatos is not null)
{
Console.WriteLine($"'{rayuela.Titulo}' - Editorial: {metadatos.Editorial.Nombre}, puntuacion: {metadatos.PuntuacionMedia}");
}
else
{
Console.WriteLine($"No hay metadatos externos disponibles para '{rayuela.Titulo}'.");
}ObtenerMetadatosPorIsbnAsync sigue el mismo patrón que PrestarLibroAsync de la lección de
Programación Asíncrona: un método async Task<T>, con su Async de rigor en el nombre, que
encapsula una operación de E/S (antes, un Task.Delay simulado; ahora, una petición HTTP real)
y maneja sus propios errores devolviendo null cuando la consulta falla, en vez de propagar la
excepción hacia quien lo llama.
sequenceDiagram
participant Main as Codigo cliente
participant Bib as Biblioteca
participant Http as HttpClient
participant Api as API externa
Main->>Bib: await ObtenerMetadatosPorIsbnAsync(isbn)
Bib->>Http: GetAsync("libros/{isbn}")
Http->>Api: GET /libros/{isbn}
Api-->>Http: 200 OK + JSON
Http-->>Bib: HttpResponseMessage
Bib->>Bib: ReadFromJsonAsync<MetadatosLibroExterno>
Bib-->>Main: MetadatosLibroExterno
Errores Comunes y Consejos
- Crear un
HttpClientnuevo por cada petición conusing: bajo carga, agota los sockets disponibles del sistema; reutiliza una única instancia (o usaIHttpClientFactoryen aplicaciones ASP.NET Core). - No distinguir un fallo de red de una respuesta HTTP de error:
GetAsyncno lanza una excepción por sí sola ante un404o un500—hay que llamar aEnsureSuccessStatusCode()(o comprobarIsSuccessStatusCode) explícitamente para tratarlos como error. - Asumir que el JSON externo sigue PascalCase: la mayoría de APIs reales usan
camelCaseosnake_case; configuraPropertyNamingPolicy(o[JsonPropertyName]para casos puntuales) en vez de asumir que coincidirá con los nombres de las propiedades de C#. - Olvidar el manejo de errores en una llamada de red: a diferencia de leer un fichero local,
una petición HTTP depende de un sistema externo que puede no responder, tardar demasiado, o
devolver un error; todo código que use
HttpClienten una aplicación real necesita sutry/catchcorrespondiente. - Consejo: para depurar el JSON exacto que devuelve una API externa antes de escribir las
clases de destino, es útil deserializar primero a un tipo genérico de inspección (como
JsonDocumento inclusostringconWriteIndented) y mirar su estructura real, en vez de adivinar las propiedades a ciegas.
Ejercicios
-
Define las clases
EditorialyMetadatosLibroExternotal como se han presentado en esta lección. Deserializa el JSON de ejemplo del apartado 2 usandoJsonNamingPolicy.CamelCase, y muestra por consolaEditorial.Paisy el primer elemento deGeneros. -
Escribe un método
async Task<bool> ExisteLibroEnServicioExternoAsync(HttpClient cliente, string isbn)que haga unGetAsync($"libros/{isbn}")y devuelvatruesirespuesta.IsSuccessStatusCodees verdadero,falseen caso contrario, sin lanzar ninguna excepción por un404. -
Añade a
Bibliotecael métodoObtenerMetadatosPorIsbnAsyncde esta lección. Llámalo desde unMainasíncrono para un libro del catálogo, y maneja tanto el caso de éxito (mostrando la editorial) como el caso en que el método devuelvanull.
Soluciones
string json =
"""
{
"isbn": "978-84-376-0495-4",
"editorial": { "nombre": "Sudamericana", "pais": "Argentina" },
"generos": ["Novela", "Literatura latinoamericana"],
"puntuacionMedia": 4.6
}
""";
var opciones = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
MetadatosLibroExterno? metadatos = JsonSerializer.Deserialize<MetadatosLibroExterno>(json, opciones);
Console.WriteLine(metadatos?.Editorial.Pais); // "Argentina"
Console.WriteLine(metadatos?.Generos[0]); // "Novela"
async Task<bool> ExisteLibroEnServicioExternoAsync(HttpClient cliente, string isbn)
{
try
{
HttpResponseMessage respuesta = await cliente.GetAsync($"libros/{isbn}");
return respuesta.IsSuccessStatusCode;
}
catch (HttpRequestException)
{
return false;
}
}
Libro libro = biblioteca.Catalogo.OfType<Libro>().First();
MetadatosLibroExterno? metadatos = await biblioteca.ObtenerMetadatosPorIsbnAsync(libro.Isbn);
if (metadatos is not null)
{
Console.WriteLine($"Editorial de '{libro.Titulo}': {metadatos.Editorial.Nombre}");
}
else
{
Console.WriteLine($"No hay metadatos externos disponibles para '{libro.Titulo}'.");
}
OfType<Libro>() es un operador LINQ (de la lección de LINQ, Módulo 4) que filtra una
colección quedándose solo con los elementos de un tipo concreto —aquí, solo los Libro del
Catalogo mixto, descartando las Revista.
Conclusión
En esta lección has profundizado en System.Text.Json para estructuras anidadas y
convenciones de nombres reales, y has aprendido a usar HttpClient para consumir una API REST
externa: peticiones GET y POST, buenas prácticas de reutilización, y manejo diferenciado de
errores de red frente a errores HTTP. Biblioteca ya puede enriquecer su catálogo con
información de un servicio externo, cerrando así el Módulo 5 (Trabajando con Datos): desde el
fichero de texto plano de la primera lección hasta una API REST externa, pasando por JSON, ADO.NET
y Entity Framework, BiblioTech ha dejado de ser una aplicación que solo vive en memoria.
El Módulo 6 (Temas Avanzados) retoma, con más profundidad, varias herramientas que este módulo
ya ha usado de pasada: la reflexión, que es literalmente el mecanismo que permite a
JsonSerializer inspeccionar las propiedades de una clase sin código manual de mapeo; los
atributos, como [JsonPropertyName] o [JsonDerivedType] vistos en la lección de
Serialización; la gestión de memoria y el recolector de basura (Garbage Collector), que
libera automáticamente los objetos que este módulo ha ido creando; y el multihilo real con
Thread y Parallel, que completa lo que async/await dejó apuntado en el Módulo 4 sobre
concurrencia.
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
