HTTP with .NET

In this article, you'll learn how to use the IHttpClientFactory and the HttpClient types with various .NET fundamentals, such as dependency injection (DI), logging, and configuration. The HttpClient type was introduced in .NET Framework 4.5, which was released in 2012. In other words, it's been around for a while. HttpClient is used for making HTTP requests and handling HTTP responses from web resources identified by a Uri. The HTTP protocol makes up the vast majority of all internet traffic.

With modern application development principles driving best practices, the IHttpClientFactory serves as a factory abstraction that can create HttpClient instances with custom configurations. IHttpClientFactory was introduced in .NET Core 2.1. Common HTTP-based .NET workloads can take advantage of resilient and transient-fault-handling third-party middleware with ease.

Explore the IHttpClientFactory type

All of the sample source code in this article relies on the Microsoft.Extensions.Http NuGet package. Additionally, The Internet Chuck Norris Database free API is used to make HTTP GET requests for "nerdy" jokes.

When you call any of the AddHttpClient extension methods, you're adding the IHttpClientFactory and related services to the IServiceCollection. The IHttpClientFactory type offers the following benefits:

  • Exposes the HttpClient class as a DI-ready type.
  • Provides a central location for naming and configuring logical HttpClient instances.
  • Codifies the concept of outgoing middleware via delegating handlers in HttpClient.
  • Provides extension methods for Polly-based middleware to take advantage of delegating handlers in HttpClient.
  • Manages the pooling and lifetime of underlying HttpClientHandler instances. Automatic management avoids common Domain Name System (DNS) problems that occur when manually managing HttpClient lifetimes.
  • Adds a configurable logging experience (via ILogger) for all requests sent through clients created by the factory.

Consumption patterns

There are several ways IHttpClientFactory can be used in an app:

The best approach depends upon the app's requirements.

Basic usage

To register the IHttpClientFactory, call AddHttpClient:

using BasicHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHttpClient();
        services.AddTransient<JokeService>();
    })
    .Build();

Consuming services can require the IHttpClientFactory as a constructor parameter with DI. The following code uses IHttpClientFactory to create an HttpClient instance:

using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace BasicHttp.Example;

public class JokeService
{
    private readonly IHttpClientFactory _httpClientFactory = null!;
    private readonly ILogger<JokeService> _logger = null!;

    public JokeService(
        IHttpClientFactory httpClientFactory,
        ILogger<JokeService> logger) =>
        (_httpClientFactory, _logger) = (httpClientFactory, logger);

    public async Task<string> GetRandomJokeAsync()
    {
        // Create the client
        HttpClient client = _httpClientFactory.CreateClient();

        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into ChuckNorrisJoke type
            ChuckNorrisJoke? result = await client.GetFromJsonAsync<ChuckNorrisJoke>(
                "https://api.icndb.com/jokes/random?limitTo=[nerdy]",
                DefaultJsonSerialization.Options);

            if (result?.Value?.Joke is not null)
            {
                return result.Value.Joke;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return "Oops, something has gone wrong - that's not funny at all!";
    }
}

Using IHttpClientFactory like in the preceding example is a good way to refactor an existing app. It has no impact on how HttpClient is used. In places where HttpClient instances are created in an existing app, replace those occurrences with calls to CreateClient.

Named clients

Named clients are a good choice when:

  • The app requires many distinct uses of HttpClient.
  • Many HttpClient instances have different configuration.

Configuration for a named HttpClient can be specified during registration in ConfigureServices:

using NamedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
        string httpClientName = context.Configuration["JokeHttpClientName"];
        services.AddHttpClient(
            httpClientName,
            client =>
            {
                // Set the base address of the named client.
                client.BaseAddress = new Uri("https://api.icndb.com/");

                // Add a user-agent default request header.
                client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
            });
        services.AddTransient<JokeService>();
    })
    .Build();

In the preceding code, the client is configured with:

  • A name that's pulled from the configuration under the "JokeHttpClientName".
  • The base address https://api.icndb.com/.
  • A "User-Agent" header.

You can use configuration to specify HTTP client names, which is helpful to avoid misnaming clients when adding and creating. In this example, the appsettings.json file is used to configure the HTTP client name:

{
    "JokeHttpClientName": "ChuckNorrisJokeApi"
}

It's easy to extend this configuration and store more details about how you'd like your HTTP client to function. For more information, see Configuration in .NET.

Create client

Each time CreateClient is called:

  • A new instance of HttpClient is created.
  • The configuration action is called.

To create a named client, pass its name into CreateClient:

using System.Net.Http.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Shared;

namespace NamedHttp.Example;

public class JokeService
{
    private readonly IHttpClientFactory _httpClientFactory = null!;
    private readonly IConfiguration _configuration = null!;
    private readonly ILogger<JokeService> _logger = null!;

    public JokeService(
        IHttpClientFactory httpClientFactory,
        IConfiguration configuration,
        ILogger<JokeService> logger) =>
        (_httpClientFactory, _configuration, _logger) =
            (httpClientFactory, configuration, logger);

    public async Task<string> GetRandomJokeAsync()
    {
        // Create the client
        string httpClientName = _configuration["JokeHttpClientName"];
        HttpClient client = _httpClientFactory.CreateClient(httpClientName);

        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into ChuckNorrisJoke type
            ChuckNorrisJoke? result = await client.GetFromJsonAsync<ChuckNorrisJoke>(
                "jokes/random?limitTo=[nerdy]",
                DefaultJsonSerialization.Options);

            if (result?.Value?.Joke is not null)
            {
                return result.Value.Joke;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return "Oops, something has gone wrong - that's not funny at all!";
    }
}

In the preceding code, the HTTP request doesn't need to specify a hostname. The code can pass just the path, since the base address configured for the client is used.

Typed clients

Typed clients:

  • Provide the same capabilities as named clients without the need to use strings as keys.
  • Provide IntelliSense and compiler help when consuming clients.
  • Provide a single location to configure and interact with a particular HttpClient. For example, a single typed client might be used:
    • For a single backend endpoint.
    • To encapsulate all logic dealing with the endpoint.
  • Work with DI and can be injected where required in the app.

A typed client accepts an HttpClient parameter in its constructor:

using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace TypedHttp.Example;

public sealed class JokeService
{
    private readonly HttpClient _httpClient = null!;
    private readonly ILogger<JokeService> _logger = null!;

    public JokeService(
        HttpClient httpClient,
        ILogger<JokeService> logger) =>
        (_httpClient, _logger) = (httpClient, logger);

    public async Task<string> GetRandomJokeAsync()
    {
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into ChuckNorrisJoke type
            ChuckNorrisJoke? result = await _httpClient.GetFromJsonAsync<ChuckNorrisJoke>(
                "https://api.icndb.com/jokes/random?limitTo=[nerdy]",
                DefaultJsonSerialization.Options);

            if (result?.Value?.Joke is not null)
            {
                return result.Value.Joke;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return "Oops, something has gone wrong - that's not funny at all!";
    }
}

In the preceding code:

  • The configuration is set when the typed client is added to the service collection.
  • The HttpClient is assigned as a class-scoped variable (field), and used with exposed APIs.

API-specific methods can be created that expose HttpClient functionality. For example, the GetRandomJokeAsync method encapsulates code to retrieve a random joke.

The following code calls AddHttpClient in ConfigureServices to register a typed client class:

using TypedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHttpClient<JokeService>(
            client =>
            {
                // Set the base address of the named client.
                client.BaseAddress = new Uri("https://api.icndb.com/");

                // Add a user-agent default request header.
                client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
            });
        services.AddTransient<JokeService>();
    })
    .Build();

The typed client is registered as transient with DI. In the preceding code, AddHttpClient registers JokeService as a transient service. This registration uses a factory method to:

  1. Create an instance of HttpClient.
  2. Create an instance of JokeService, passing in the instance of HttpClient to its constructor.

Tip

A call to AddHttpClient<TClient> doesn't add the TClient service to the IServiceCollection. You still need to explicitly add it with Add{ServiceLifetime}.

Generated clients

IHttpClientFactory can be used in combination with third-party libraries such as Refit. Refit is a REST library for .NET. It allows for declarative REST API definitions, mapping interface methods to endpoints. An implementation of the interface is generated dynamically by the RestService, using HttpClient to make the external HTTP calls.

Consider the following record types:

namespace Shared;

public record IdentifiableJokeValue(
    int Id, string Joke);
namespace Shared;

public record ChuckNorrisJoke(
    string Type,
    IdentifiableJokeValue Value);

The following example relies on the Refit.HttpClientFactory NuGet package, and is a simple interface:

using Refit;
using Shared;

namespace GeneratedHttp.Example;

public interface IJokeService
{
    [Get("/jokes/random?limitTo=[nerdy]")]
    Task<ChuckNorrisJoke> GetRandomJokeAsync();
}

The preceding C# interface:

  • Defines a method named GetRandomJokeAsync that returns a Task<ChuckNorrisJoke> instance.
  • Declares a Refit.GetAttribute attribute with the path and query string to the external API.

A typed client can be added, using Refit to generate the implementation:

using GeneratedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Refit;
using Shared;

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddRefitClient<IJokeService>()
            .ConfigureHttpClient(client =>
            {
                // Set the base address of the named client.
                client.BaseAddress = new Uri("https://api.icndb.com/");

                // Add a user-agent default request header.
                client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
            });
    })
    .Build();

The defined interface can be consumed where necessary, with the implementation provided by DI and Refit.

Make POST, PUT, and DELETE requests

In the preceding examples, all HTTP requests use the GET HTTP verb. HttpClient also supports other HTTP verbs, including:

  • POST
  • PUT
  • DELETE
  • PATCH

For a complete list of supported HTTP verbs, see HttpMethod.

The following example shows how to make an HTTP POST request:

public async Task CreateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, DefaultJsonSerialization.Options),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await _httpClient.PostAsync("/api/items", json);

    httpResponse.EnsureSuccessStatusCode();
}

In the preceding code, the CreateItemAsync method:

  • Serializes the Item parameter to JSON using System.Text.Json. This uses an instance of JsonSerializerOptions to configure the serialization process.
  • Creates an instance of StringContent to package the serialized JSON for sending in the HTTP request's body.
  • Calls PostAsync to send the JSON content to the specified URL. This is a relative URL that gets added to the HttpClient.BaseAddress.
  • Calls EnsureSuccessStatusCode to throw an exception if the response status code does not indicate success.

HttpClient also supports other types of content. For example, MultipartContent and StreamContent. For a complete list of supported content, see HttpContent.

The following example shows an HTTP PUT request:

public async Task UpdateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, DefaultJsonSerialization.Options),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await _httpClient.PutAsync($"/api/items/{item.Id}", json);

    httpResponse.EnsureSuccessStatusCode();
}

The preceding code is very similar to the POST example. The UpdateItemAsync method calls PutAsync instead of PostAsync.

The following example shows an HTTP DELETE request:

public async Task DeleteItemAsync(Guid id)
{
    using HttpResponseMessage httpResponse =
        await _httpClient.DeleteAsync($"/api/items/{id}");

    httpResponse.EnsureSuccessStatusCode();
}

In the preceding code, the DeleteItemAsync method calls DeleteAsync. Because HTTP DELETE requests typically contain no body, the DeleteAsync method doesn't provide an overload that accepts an instance of HttpContent.

To learn more about using different HTTP verbs with HttpClient, see HttpClient.

HttpClient lifetime management

A new HttpClient instance is returned each time CreateClient is called on the IHttpClientFactory. One HttpClientHandler instance is created per client. The factory manages the lifetimes of the HttpClientHandler instances.

IHttpClientFactory pools the HttpClientHandler instances created by the factory to reduce resource consumption. An HttpClientHandler instance may be reused from the pool when creating a new HttpClient instance if its lifetime hasn't expired.

Pooling of handlers is desirable as each handler typically manages its own underlying HTTP connection. Creating more handlers than necessary can result in connection delays. Some handlers also keep connections open indefinitely, which can prevent the handler from reacting to DNS changes.

The default handler lifetime is two minutes. To override the default value, call SetHandlerLifetime for each client, on the IServiceCollection:

services.AddHttpClient("Named.Client")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Important

You can generally treat HttpClient instances as objects that do not require disposal. Disposal cancels outgoing requests and guarantees the given HttpClient instance can't be used after calling Dispose. IHttpClientFactory tracks and disposes resources used by HttpClient instances.

Keeping a single HttpClient instance alive for a long duration is a common pattern used before the inception of IHttpClientFactory. This pattern becomes unnecessary after migrating to IHttpClientFactory.

Configure the HttpMessageHandler

It may be necessary to control the configuration of the inner HttpMessageHandler used by a client.

An IHttpClientBuilder is returned when adding named or typed clients. The ConfigurePrimaryHttpMessageHandler extension method can be used to define a delegate on the IServiceCollection. The delegate is used to create and configure the primary HttpMessageHandler used by that client:

services.AddHttpClient("Named.Client")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler
        {
            AllowAutoRedirect = false,
            UseDefaultCredentials = true
        };
    });

Additional configuration

There are several additional configuration options for controlling the IHttpClientHandler:

Method Description
AddHttpMessageHandler Adds an additional message handler for a named HttpClient.
AddTypedClient Configures the binding between the TClient and the named HttpClient associated with the IHttpClientBuilder.
ConfigureHttpClient Adds a delegate that will be used to configure a named HttpClient.
ConfigureHttpMessageHandlerBuilder Adds a delegate that will be used to configure message handlers using HttpMessageHandlerBuilder for a named HttpClient.
ConfigurePrimaryHttpMessageHandler Configures the primary HttpMessageHandler from the dependency injection container for a named HttpClient.
RedactLoggedHeaders Sets the collection of HTTP header names for which values should be redacted before logging.
SetHandlerLifetime Sets the length of time that a HttpMessageHandler instance can be reused. Each named client can have its own configured handler lifetime value.

See also