Tutorial: Realización de solicitudes HTTP en una aplicación de consola de .NET mediante C#

En este tutorial se crea una aplicación que emite solicitudes HTTP a un servicio REST en GitHub. La aplicación lee información en formato JSON y convierte la respuesta JSON en objetos de C#. La conversión de JSON en objetos de C# se conoce como deserialización.

En el tutorial se muestra cómo hacer lo siguiente:

  • Enviar solicitudes HTTP
  • Deserializar respuestas JSON
  • Configurar la deserialización con atributos

Si prefiere seguir las explicaciones con el ejemplo final del tutorial, puede descargarlo. Para obtener instrucciones de descarga, vea Ejemplos y tutoriales.

Requisitos previos

  • SDK de .NET 6.0 o versiones posteriores.
  • Un editor de código como [Visual Studio Code (un editor multiplataforma de código abierto). Puede ejecutar la aplicación de ejemplo en Windows, Linux o macOS, o bien en un contenedor de Docker.

Creación de la aplicación cliente

  1. Abra un símbolo del sistema y cree un directorio para la aplicación. Conviértalo en el directorio actual.

  2. Escriba el siguiente comando en la ventana de consola:

    dotnet new console --name WebAPIClient
    

    Este comando crea los archivos de inicio para una aplicación básica "Hola mundo". El nombre del proyecto es "WebAPIClient".

  3. Vaya al directorio "WebAPIClient" y ejecute la aplicación.

    cd WebAPIClient
    
    dotnet run
    

    dotnet run ejecuta automáticamente dotnet restore para restaurar las dependencias que necesita la aplicación. También ejecuta dotnet build si es necesario. Debería ver la salida de la aplicación "Hello, World!". En el terminal, presione Ctrl+C para detener la aplicación.

Realización de solicitudes HTTP

Esta aplicación llama a la API de GitHub para obtener información sobre los proyectos incluidos bajo el paraguas de .NET Foundation. El extremo es https://api.github.com/orgs/dotnet/repos. Para recuperar información, realiza una solicitud HTTP GET. Los exploradores también realizan solicitudes HTTP GET, por lo que puede pegar esa dirección URL en la barra de direcciones del explorador para ver la información que recibirá y procesará.

Use la clase HttpClient para realizar solicitudes HTTP. HttpClient solo admite métodos asincrónicos para sus API de larga duración. Por lo tanto, los pasos siguientes crean un método asincrónico y lo llaman desde el método Main.

  1. Abra el archivo Program.cs en el directorio del proyecto y reemplace su contenido por lo siguiente:

    await ProcessRepositoriesAsync();
    
    static async Task ProcessRepositoriesAsync(HttpClient client)
    {
    }
    

    Este código:

    • Reemplaza la instrucción Console.WriteLine por una llamada a ProcessRepositoriesAsync que usa la palabra clave await.
    • Define un método ProcessRepositoriesAsync vacío.
  2. En la clase Program, use un elemento HttpClient para controlar solicitudes y respuestas, reemplazando el contenido por el código de C# siguiente.

    using System.Net.Http.Headers;
    
    using HttpClient client = new();
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
    client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");
    
    await ProcessRepositoriesAsync(client);
    
    static async Task ProcessRepositoriesAsync(HttpClient client)
    {
    }
    

    Este código:

    • Configura encabezados HTTP para todas las solicitudes:
      • Un encabezado Accept para aceptar respuestas JSON.
      • Encabezado de User-Agent. Estos encabezados se comprueban mediante el código de servidor de GitHub y son necesarios para recuperar información de GitHub.
  3. En el método ProcessRepositoriesAsync, llame al punto de conexión de GitHub que devuelve una lista de todos los repositorios de la organización de .NET Foundation:

     static async Task ProcessRepositoriesAsync(HttpClient client)
     {
         var json = await client.GetStringAsync(
             "https://api.github.com/orgs/dotnet/repos");
    
         Console.Write(json);
     }
    

    Este código:

    • Espera la tarea devuelta al llamar al método HttpClient.GetStringAsync(String). Este método envía una solicitud HTTP GET al URI especificado. El cuerpo de la respuesta se devuelve como un elemento String, que está disponible cuando se completa la tarea.
    • La cadena de respuesta json se imprime en la consola.
  4. Compile la aplicación y ejecútela.

    dotnet run
    

    No hay ninguna advertencia de compilación porque ProcessRepositoriesAsync ahora contiene un operador await. La salida es una larga presentación de texto JSON.

Deserialización del resultado JSON

Los pasos siguientes convierten la respuesta JSON en objetos de C#. La clase System.Text.Json.JsonSerializer se usa para deserializar JSON en objetos.

  1. Cree un archivo con el nombre Repository.cs y agregue el código siguiente:

    public record class Repository(string name);
    

    El código anterior define una clase para representar el objeto JSON devuelto desde la API de GitHub. Usará esta clase para mostrar una lista de nombres de repositorio.

    El objeto JSON de un objeto de repositorio contiene docenas de propiedades, pero solo se deserializará la propiedad name. El serializador omite automáticamente las propiedades JSON para las que no hay ninguna coincidencia en la clase de destino. Esta característica facilita la creación de tipos que funcionan con solo un subconjunto de campos de un paquete JSON grande.

    La convención de C# es poner en mayúscula la primera letra de los nombres de propiedad, pero la propiedad name comienza aquí con minúscula porque coincide exactamente con lo que hay en JSON. Más adelante verá cómo usar nombres de propiedad de C# que no coinciden con los nombres de propiedad JSON.

  2. Use el serializador para convertir JSON en objetos de C#. Reemplace la llamada a GetStringAsync(String) en el método ProcessRepositoriesAsync por las líneas siguientes:

    await using Stream stream =
        await client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos");
    var repositories =
        await JsonSerializer.DeserializeAsync<List<Repository>>(stream);
    

    El código actualizado reemplaza GetStringAsync(String) por GetStreamAsync(String). Este método del serializador usa como origen una secuencia, en lugar de una cadena.

    El primer argumento para JsonSerializer.DeserializeAsync<TValue>(Stream, JsonSerializerOptions, CancellationToken) es una expresión await. Las expresiones await pueden aparecer prácticamente en cualquier parte del código, aunque hasta ahora solo las ha visto como parte de una instrucción de asignación. Los otros dos parámetros, JsonSerializerOptions y CancellationToken, son opcionales y se omiten en el fragmento de código.

    El método DeserializeAsync es genérico, lo que significa es necesario proporcionar argumentos de tipo para el tipo de objetos que se debe crear a partir del texto JSON. En este ejemplo, se va a deserializar a en List<Repository>, que es otro objeto genérico, System.Collections.Generic.List<T>. La clase List<T> administra una colección de objetos. El argumento de tipo declara el tipo de objetos almacenados en List<T>. El argumento de tipo es el registro Repository, porque el texto JSON representa una colección de objetos de repositorio.

  3. Agregue código para mostrar el nombre de cada repositorio. Reemplace las líneas donde pone:

    Console.Write(json);
    

    por el siguiente:

    foreach (var repo in repositories ?? Enumerable.Empty<Repository>())
        Console.Write(repo.name);
    
  4. Las directivas using siguientes deben situarse en la parte superior del archivo:

    using System.Net.Http.Headers;
    using System.Text.Json;
    
  5. Ejecutar la aplicación.

    dotnet run
    

    La salida es una lista con los nombres de los repositorios que forman parte de .NET Foundation.

Configuración de la deserialización

  1. En Repository.cs, reemplace el contenido del archivo por el código de C# siguiente.

    using System.Text.Json.Serialization;
    
    public record class Repository(
        [property: JsonPropertyName("name")] string Name);
    

    Este código:

    • Cambia el nombre de la propiedad name a Name.
    • Agrega JsonPropertyNameAttribute para especificar cómo aparece esta propiedad en el JSON.
  2. En Program.cs, actualice el código para aplicar el nuevo uso de las mayúsculas de la propiedad Name:

    foreach (var repo in repositories)
       Console.Write(repo.Name);
    
  3. Ejecutar la aplicación.

    La salida es la misma.

Refactorizar el código

El método ProcessRepositoriesAsync puede realizar el trabajo asincrónico y devolver una colección de los repositorios. Cambie ese método para devolver Task<List<Repository>> y mueva el código que escribe en la consola cerca del autor de la llamada.

  1. Cambie la signatura de ProcessRepositoriesAsync para devolver una tarea cuyo resultado sea una lista de objetos Repository:

    static async Task<List<Repository>> ProcessRepositoriesAsync(HttpClient client)
    
  2. Devuelva los repositorios después de procesar la respuesta JSON:

    await using Stream stream =
        await client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos");
    var repositories =
        await JsonSerializer.DeserializeAsync<List<Repository>>(stream);
    return repositories ?? new();
    

    El compilador genera el objeto Task<T> para el valor devuelto porque ha marcado este método como async.

  3. Modifique el archivo Program.cs, reemplazando la llamada a ProcessRepositoriesAsync con lo siguiente para capturar los resultados y escribir cada nombre de repositorio en la consola.

    var repositories = await ProcessRepositoriesAsync(client);
    
    foreach (var repo in repositories)
        Console.Write(repo.Name);
    
  4. Ejecutar la aplicación.

    La salida es la misma.

Deserialización de más propiedades

En los pasos siguientes se agrega código para procesar más propiedades del paquete JSON recibido. Probablemente no le interesará procesar todas las propiedades, pero si agrega algunas más verá una demostración de otras características de C#.

  1. Reemplace el contenido de la clase Repository por la definición record siguiente:

    using System.Text.Json.Serialization;
    
    public record class Repository(
        [property: JsonPropertyName("name")] string Name,
        [property: JsonPropertyName("description")] string Description,
        [property: JsonPropertyName("html_url")] Uri GitHubHomeUrl,
        [property: JsonPropertyName("homepage")] Uri Homepage,
        [property: JsonPropertyName("watchers")] int Watchers);
    

    Los tipos Uri y int tienen una funcionalidad integrada para convertir a y desde la representación de cadena. No se necesita código adicional para deserializar desde el formato de cadena de JSON a esos tipos de destino. Si el paquete JSON contiene datos que no se convierten en un tipo de destino, la acción de serialización genera una excepción.

  2. Actualice el bucle foreach en el archivo Program.cs para mostrar los valores de propiedad:

    foreach (var repo in repositories)
    {
        Console.WriteLine($"Name: {repo.Name}");
        Console.WriteLine($"Homepage: {repo.Homepage}");
        Console.WriteLine($"GitHub: {repo.GitHubHomeUrl}");
        Console.WriteLine($"Description: {repo.Description}");
        Console.WriteLine($"Watchers: {repo.Watchers:#,0}");
        Console.WriteLine();
    }
    
  3. Ejecutar la aplicación.

    La lista ahora incluye las propiedades adicionales.

Incorporación de una propiedad de fecha

La fecha de la última operación de inserción presenta este formato en la respuesta JSON:

2016-02-08T21:27:00Z

Este es el formato de la hora universal coordinada (UTC), por lo que el resultado de la deserialización es un valor DateTime cuya propiedad Kind es Utc.

Para que una fecha y hora se represente en su zona horaria, debe escribir un método de conversión personalizado.

  1. En Repository.cs, agregue una propiedad para la representación UTC de la fecha y hora y una propiedad LastPush de solo lectura que devuelva la fecha convertida a la hora local. El archivo debería tener el siguiente aspecto:

    using System.Text.Json.Serialization;
    
    public record class Repository(
        [property: JsonPropertyName("name")] string Name,
        [property: JsonPropertyName("description")] string Description,
        [property: JsonPropertyName("html_url")] Uri GitHubHomeUrl,
        [property: JsonPropertyName("homepage")] Uri Homepage,
        [property: JsonPropertyName("watchers")] int Watchers,
        [property: JsonPropertyName("pushed_at")] DateTime LastPushUtc)
    {
        public DateTime LastPush => LastPushUtc.ToLocalTime();
    }
    

    La propiedad LastPush se define utilizando un miembro con forma de expresión para el descriptor de acceso get. No hay ningún descriptor de acceso set. La omisión del descriptor de acceso set es una forma de definir una propiedad read-only en C#. (Sí, puede crear propiedades de solo escritura en C#, pero su valor es limitado).

  2. Agregue de nuevo otra instrucción de salida en Program.cs:

    Console.WriteLine($"Last push: {repo.LastPush}");
    
  3. La aplicación completa debe ser similar al archivo Program.cs siguiente:

    using System.Net.Http.Headers;
    using System.Text.Json;
    
    using HttpClient client = new();
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
    client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");
    
    var repositories = await ProcessRepositoriesAsync(client);
    
    foreach (var repo in repositories)
    {
        Console.WriteLine($"Name: {repo.Name}");
        Console.WriteLine($"Homepage: {repo.Homepage}");
        Console.WriteLine($"GitHub: {repo.GitHubHomeUrl}");
        Console.WriteLine($"Description: {repo.Description}");
        Console.WriteLine($"Watchers: {repo.Watchers:#,0}");
        Console.WriteLine($"{repo.LastPush}");
        Console.WriteLine();
    }
    
    static async Task<List<Repository>> ProcessRepositoriesAsync(HttpClient client)
    {
        await using Stream stream =
            await client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos");
        var repositories =
            await JsonSerializer.DeserializeAsync<List<Repository>>(stream);
        return repositories ?? new();
    }
    
  4. Ejecutar la aplicación.

    La salida incluye la fecha y hora de la última inserción en cada repositorio.

Pasos siguientes

En este tutorial, ha creado una aplicación que realiza solicitudes web y analiza los resultados. La versión de la aplicación debe coincidir ahora con el ejemplo terminado.

Obtenga más información sobre cómo configurar la serialización JSON en Procedimiento para serializar y deserializar (calcular referencias y resolver referencias) JSON en .NET.