HttpClient
Async/Await
C#
.NET
Asynchronous Programming

HttpClient To Get List With Async/Await Operation

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Fetching a list from an HTTP API in C# is a normal HttpClient plus JSON-deserialization workflow. The async part is not optional decoration. It is the correct way to avoid blocking a thread while the network request is in flight. The reliable pattern is to make the request with await, validate the response, then deserialize the JSON into List<T> or another collection type.

Define a Model That Matches the JSON

Suppose the API returns JSON like this:

json
1[
2  { "id": 1, "name": "Ada" },
3  { "id": 2, "name": "Linus" }
4]

A matching C# model might be:

csharp
1public sealed class UserDto
2{
3    public int Id { get; set; }
4    public string Name { get; set; } = string.Empty;
5}

The property names should line up with the JSON payload, or you should configure the serializer accordingly.

Use GetFromJsonAsync for the Simple Case

In modern .NET, the easiest option is GetFromJsonAsync.

csharp
1using System;
2using System.Collections.Generic;
3using System.Net.Http;
4using System.Net.Http.Json;
5using System.Threading.Tasks;
6
7class Program
8{
9    private static readonly HttpClient Http = new HttpClient
10    {
11        BaseAddress = new Uri("https://api.example.com/")
12    };
13
14    static async Task Main()
15    {
16        List<UserDto>? users = await Http.GetFromJsonAsync<List<UserDto>>("users");
17
18        if (users is null)
19        {
20            Console.WriteLine("No users returned.");
21            return;
22        }
23
24        foreach (var user in users)
25        {
26            Console.WriteLine($"{user.Id}: {user.Name}");
27        }
28    }
29}

This is a good default when the endpoint returns JSON directly and you do not need special response inspection first.

Use GetAsync When You Need More Control

If you need to inspect status codes, headers, or errors before deserializing, use GetAsync and then read the content explicitly.

csharp
1using System.Collections.Generic;
2using System.Net.Http;
3using System.Net.Http.Json;
4using System.Threading.Tasks;
5
6public static async Task<List<UserDto>> FetchUsersAsync(HttpClient httpClient)
7{
8    using HttpResponseMessage response = await httpClient.GetAsync("users");
9    response.EnsureSuccessStatusCode();
10
11    List<UserDto>? users = await response.Content.ReadFromJsonAsync<List<UserDto>>();
12    return users ?? new List<UserDto>();
13}

This form is more verbose, but it is better when you care about failure handling or non-JSON edge cases.

Reuse HttpClient

One of the most important practical rules is to reuse HttpClient instead of creating a new instance per request.

Bad pattern:

csharp
1public async Task<List<UserDto>> FetchUsersAsync()
2{
3    using var httpClient = new HttpClient();
4    return await httpClient.GetFromJsonAsync<List<UserDto>>("https://api.example.com/users")
5        ?? new List<UserDto>();
6}

This looks harmless, but frequent short-lived HttpClient instances can contribute to socket exhaustion and unnecessary connection churn. Prefer a shared instance, dependency injection, or IHttpClientFactory in larger applications.

Handle Exceptions Deliberately

Real HTTP calls fail. Network outages, timeouts, invalid JSON, and non-success status codes all need deliberate handling.

csharp
1try
2{
3    List<UserDto> users = await FetchUsersAsync(Http);
4    Console.WriteLine($"Fetched {users.Count} users.");
5}
6catch (HttpRequestException ex)
7{
8    Console.WriteLine($"HTTP error: {ex.Message}");
9}
10catch (NotSupportedException ex)
11{
12    Console.WriteLine($"Unexpected content type: {ex.Message}");
13}
14catch (System.Text.Json.JsonException ex)
15{
16    Console.WriteLine($"Bad JSON: {ex.Message}");
17}

The exact exception strategy depends on your app, but pretending the request always succeeds is not acceptable production code.

Return the Right Collection Type

If the API returns an array, List<T> is usually fine. If it returns a wrapper object such as:

json
{ "items": [ ... ], "total": 42 }

then your model must match that shape.

csharp
1public sealed class UserResponse
2{
3    public List<UserDto> Items { get; set; } = new();
4    public int Total { get; set; }
5}

Do not deserialize straight into List<T> unless the JSON root actually is a JSON array.

Common Pitfalls

A common mistake is calling .Result or .Wait() instead of using await. That blocks the current thread and defeats the point of asynchronous I/O.

Another issue is creating a new HttpClient per request. Reuse matters for performance and connection management.

Developers also often deserialize into the wrong shape. If the JSON root is an object, List<T> is not the right target type.

Finally, do not skip response validation. If you do not inspect or enforce success status codes, deserialization errors can hide the real HTTP failure.

Summary

  • Use await with HttpClient to fetch lists without blocking threads.
  • 'GetFromJsonAsync<List<T>> is the simplest path when the endpoint returns a JSON array.'
  • Use GetAsync plus ReadFromJsonAsync when you need more response control.
  • Reuse HttpClient instead of creating one per request.
  • Make sure the C# model matches the actual JSON shape returned by the API.

Course illustration
Course illustration

All Rights Reserved.