Tutorial: Make HTTP requests in a .NET console app using C#

This tutorial builds an app that issues HTTP requests to a REST service on GitHub. The app reads information in JSON format and converts the JSON into C# objects. Converting from JSON to C# objects is known as deserialization.

The tutorial shows how to:

  • Send HTTP requests.
  • Deserialize JSON responses.
  • Configure deserialization with attributes.

If you prefer to follow along with the final sample for this tutorial, you can download it. For download instructions, see Samples and Tutorials.

Prerequisites

  • .NET SDK 5.0 or later
  • A code editor such as Visual Studio Code, which is an open source, cross platform editor. You can run the sample app on Windows, Linux, or macOS, or in a Docker container.

Create the client app

  1. Open a command prompt and create a new directory for your app. Make that the current directory.

  2. Enter the following command in a console window:

    dotnet new console --name WebAPIClient
    

    This command creates the starter files for a basic "Hello World" app. The project name is "WebAPIClient".

  3. Navigate into the "WebAPIClient" directory, and run the app.

    cd WebAPIClient
    
    dotnet run
    

    dotnet run automatically runs dotnet restore to restore any dependencies that the app needs. It also runs dotnet build if needed.

Make HTTP requests

This app calls the GitHub API to get information about the projects under the .NET Foundation umbrella. The endpoint is https://api.github.com/orgs/dotnet/repos. To retrieve information, it makes an HTTP GET request. Browsers also make HTTP GET requests, so you can paste that URL into your browser address bar to see what information you'll be receiving and processing.

Use the HttpClient class to make HTTP requests. HttpClient supports only async methods for its long-running APIs. So the following steps create an async method and call it from the Main method.

  1. Open the Program.cs file in your project directory and add the following async method to the Program class:

    private static async Task ProcessRepositories()
    {
    }
    
  2. Add a using directive at the top of the Program.cs file so that the C# compiler recognizes the Task type:

    using System.Threading.Tasks;
    

    If you run dotnet build at this point, the compile succeeds but warns that this method doesn't contain any await operators and so runs synchronously. You'll add await operators later as you fill in the method.

  3. Replace the Main method with the following code:

    static async Task Main(string[] args)
    {
        await ProcessRepositories();
    }
    

    This code:

    • Changes the signature of Main by adding the async modifier and changing the return type to Task.
    • Replaces the Console.WriteLine statement with a call to ProcessRepositories that uses the await keyword.
  4. In the Program class, create a static instance of HttpClient to handle requests and responses.

    namespace WebAPIClient
    {
        class Program
        {
            private static readonly HttpClient client = new HttpClient();
    
            static async Task Main(string[] args)
            {
                //...
            }
        }
    }
    
  5. In the ProcessRepositories method, call the GitHub endpoint that returns a list of all repositories under the .NET foundation organization:

    private static async Task ProcessRepositories()
    {
        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 stringTask = client.GetStringAsync("https://api.github.com/orgs/dotnet/repos");
    
        var msg = await stringTask;
        Console.Write(msg);
    }
    

    This code:

    • Sets up HTTP headers for all requests:
      • An Accept header to accept JSON responses
      • A User-Agent header. These headers are checked by the GitHub server code and are necessary to retrieve information from GitHub.
    • Calls HttpClient.GetStringAsync(String) to make a web request and retrieve the response. This method starts a task that makes the web request. When the request returns, the task reads the response stream and extracts the content from the stream. The body of the response is returned as a String, which is available when the task completes.
    • Awaits the task for the response string and prints the response to the console.
  6. Add two using directives at the top of the file:

    using System.Net.Http;
    using System.Net.Http.Headers;
    
  7. Build the app and run it.

    dotnet run
    

    There is no build warning because the ProcessRepositories now contains an await operator.

    The output is a long display of JSON text.

Deserialize the JSON Result

The following steps convert the JSON response into C# objects. You use the System.Text.Json.JsonSerializer class to deserialize JSON into objects.

  1. Create a file named repo.cs and add the following code:

    using System;
    
    namespace WebAPIClient
    {
        public class Repository
        {
            public string name { get; set; }
        }
    }
    

    The preceding code defines a class to represent the JSON object returned from the GitHub API. You'll use this class to display a list of repository names.

    The JSON for a repository object contains dozens of properties, but only the name property will be deserialized. The serializer automatically ignores JSON properties for which there is no match in the target class. This feature makes it easier to create types that work with only a subset of fields in a large JSON packet.

    The C# convention is to capitalize the first letter of property names, but the name property here starts with a lowercase letter because that matches exactly what's in the JSON. Later you'll see how to use C# property names that don't match the JSON property names.

  2. Use the serializer to convert JSON into C# objects. Replace the call to GetStringAsync(String) in the ProcessRepositories method with the following lines:

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

    The updated code replaces GetStringAsync(String) with GetStreamAsync(String). This serializer method uses a stream instead of a string as its source.

    The first argument to JsonSerializer.DeserializeAsync<TValue>(Stream, JsonSerializerOptions, CancellationToken) is an await expression. await expressions can appear almost anywhere in your code, even though up to now, you've only seen them as part of an assignment statement. The other two parameters, JsonSerializerOptions and CancellationToken, are optional and are omitted in the code snippet.

    The DeserializeAsync method is generic, which means you supply type arguments for what kind of objects should be created from the JSON text. In this example, you're deserializing to a List<Repository>, which is another generic object, a System.Collections.Generic.List<T>. The List<T> class stores a collection of objects. The type argument declares the type of objects stored in the List<T>. The type argument is your Repository class, because the JSON text represents a collection of repository objects.

  3. Add code to display the name of each repository. Replace the lines that read:

    var msg = await stringTask;
    Console.Write(msg);
    

    with the following code:

    foreach (var repo in repositories)
        Console.WriteLine(repo.name);
    
  4. Add the following using directives at the top of the file:

    using System.Collections.Generic;
    using System.Text.Json;
    
  5. Run the app.

    dotnet run
    

    The output is a list of the names of the repositories that are part of the .NET Foundation.

Configure deserialization

  1. In repo.cs, change the name property to Name and add a [JsonPropertyName] attribute to specify how this property appears in the JSON.

    [JsonPropertyName("name")]
    public string Name { get; set; }
    
  2. Add the System.Text.Json.Serialization namespace to the using directives:

    using System.Text.Json.Serialization;
    
  3. In Program.cs, update the code to use the new capitalization of the Name property:

    Console.WriteLine(repo.Name);
    
  4. Run the app.

    The output is the same.

Refactor the code

The ProcessRepositories method can do the async work and return a collection of the repositories. Change that method to return List<Repository>, and move the code that writes the information into the Main method.

  1. Change the signature of ProcessRepositories to return a task whose result is a list of Repository objects:

    private static async Task<List<Repository>> ProcessRepositories()
    
  2. Return the repositories after processing the JSON response:

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

    The compiler generates the Task<T> object for the return value because you've marked this method as async.

  3. Modify the Main method to capture the results and write each repository name to the console. The Main method now looks like this:

    public static async Task Main(string[] args)
    {
        var repositories = await ProcessRepositories();
    
        foreach (var repo in repositories)
            Console.WriteLine(repo.Name);
    }
    
  4. Run the app.

    The output is the same.

Deserialize more properties

The following steps add code to process more of the properties in the received JSON packet. You probably won't want to process every property, but adding a few more demonstrates other features of C#.

  1. Add the following properties to the Repository class definition:

    [JsonPropertyName("description")]
    public string Description { get; set; }
    
    [JsonPropertyName("html_url")]
    public Uri GitHubHomeUrl { get; set; }
    
    [JsonPropertyName("homepage")]
    public Uri Homepage { get; set; }
    
    [JsonPropertyName("watchers")]
    public int Watchers { get; set; }
    

    The Uri and int types have built-in functionality to convert to and from string representation. No extra code is needed to deserialize from JSON string format to those target types. If the JSON packet contains data that doesn't convert to a target type, the serialization action throws an exception.

  2. Update the Main method to display the property values:

    foreach (var repo in repositories)
    {
        Console.WriteLine(repo.Name);
        Console.WriteLine(repo.Description);
        Console.WriteLine(repo.GitHubHomeUrl);
        Console.WriteLine(repo.Homepage);
        Console.WriteLine(repo.Watchers);
        Console.WriteLine();
    }
    
  3. Run the app.

    The list now includes the additional properties.

Add a date property

The date of the last push operation is formatted in this fashion in the JSON response:

2016-02-08T21:27:00Z

This format is for Coordinated Universal Time (UTC), so the result of deserialization is a DateTime value whose Kind property is Utc.

To get a date and time represented in your time zone, you have to write a custom conversion method.

  1. In repo.cs, add a public property for the UTC representation of the date and time and a LastPush readonly property that returns the date converted to local time:

    [JsonPropertyName("pushed_at")]
    public DateTime LastPushUtc { get; set; }
    
    public DateTime LastPush => LastPushUtc.ToLocalTime();
    

    The LastPush property is defined using an expression-bodied member for the get accessor. There's no set accessor. Omitting the set accessor is one way to define a read-only property in C#. (Yes, you can create write-only properties in C#, but their value is limited.)

  2. Add another output statement in Program.cs: again:

    Console.WriteLine(repo.LastPush);
    
  3. Run the app.

    The output includes the date and time of the last push to each repository.

Next steps

In this tutorial, you created an app that makes web requests and parses the results. Your version of the app should now match the finished sample.

Learn more about how to configure JSON serialization in How to serialize and deserialize (marshal and unmarshal) JSON in .NET.