Use IHttpClientFactory to make HTTP requests in ASP.NET Core

Posted by radman08 on Fri, 22 Oct 2021 18:42:27 +0200

IHttpClientFactory can be registered and used to configure and create HttpClient instances in applications. The advantages of IHttpClientFactory are as follows:

  • Provides a central location for naming and configuring logical HttpClient instances. For example, a client named GitHub can be registered and configured to access GitHub. You can register a default client for general access.
  • Encode the concept of outbound middleware through a delegate handler in HttpClient. Provides extensions to Polly-based middleware to take advantage of delegate handlers in HttpClient.
  • Manages the pool and lifetime of the underlying HttpClientMessageHandler instance. Automatic management avoids common DNS (Domain Name System) problems when manually managing HttpClient lifetimes.
  • Add a configurable logging experience (via ILogger) to handle all requests sent by clients created by the factory.

The sample code in this theme version uses System.Text.Json to deserialize the JSON content returned in the HTTP response. For examples using Json.NET and ReadAsync<T>, use the Version Selector to select version 2.x for this topic.

Consumption mode

IHttpClientFactory can be used in applications in several ways:

  • Basic Usage
  • Named Client
  • Typed Client
  • Generated Client

The best method depends on the application requirements.

Basic Usage

You can register the IHttpClientFactory by calling AddHttpClient:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

You can request the IHttpClientFactory using Dependency Injection (DI). The following code uses IHttpClientFactory to create an HttpClient instance:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

As in the previous example, using IHttpClientFactory is a good way to reconstruct existing applications. This does not affect how the HttpClient is used. Where HttpClient instances are created in existing applications, replace these matches with calls to CreateClient.

Named Client

Named clients are a good choice in the following situations:

  • Applications require many different uses of the HttpClient.
  • Many HttpClient s have different configurations.

You can specify the configuration of the named HttpClient when registering in Startup.ConfigureServices:

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

In the above code, the client configuration is as follows:

  • Base address is https://api.github.com/ .
  • Two headers required to use the GitHub API.

CreateClient

Each time CreateClient is called:

  • Create a new instance of HttpClient.
  • Invoke configuration operation.

To create a named client, pass its name to the CreateClient:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

In the above code, the request does not need to specify a host name. Code can only pass paths because it uses a base address configured for the client.

Typed Client

Typed Client:

  • Provides the same functionality as a named client without using strings as keys.
  • Provide IntelliSense and compiler help when using the client.
  • Provides a single location to configure and interact with a specific HttpClient. For example, you can use a single typed client:
    • For a single backend endpoint.
    • Encapsulates all the logic for processing the endpoint.
  • Uses DI and can be injected where needed in the application.

Typed clients accept the HttpClient parameter in the constructor:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        return await Client.GetFromJsonAsync<IEnumerable<GitHubIssue>>(
          "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
    }
}

In the above code:

  • Configuration is transferred to typed clients.
  • The HttpClient object is exposed as a public property.

API-specific methods can be created to expose HttpClient functionality. For example, create the GetAspNetDocsIssues method to encapsulate code to retrieve unresolved problems.

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

services.AddHttpClient<GitHubService>();

Use DI to register type clients as temporary clients. In the code above, AddHttpClient registers GitHubService as a temporary service. This registration uses the factory method to do the following:

  1. Create an instance of HttpClient.
  2. Create an instance of GitHubService and pass an instance of HttpClient into its constructor.

You can insert or use typed clients directly:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

Instead of specifying in the constructor of a typed client, you can specify the configuration of the typed client when registering in Startup.ConfigureServices:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient can be encapsulated in a typed client, defining a method to call an HttpClient instance internally instead of exposing it as a property:

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

In the above code, the HttpClient is stored in a private field. Access the HttpClient through the public GetRepos method.

Generated Client

IHttpClientFactory can be used in conjunction with third-party libraries such as Refit. Refit is the REST library for.NET. It converts the REST API into a real-time interface. RestService dynamically generates the implementation of the interface and makes external HTTP calls using the HttpClient.

Interfaces and replies are defined to represent external API s and their responses:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

You can add typed clients and use Refit to generate implementations:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

You can use defined interfaces, as necessary, and implementations provided by DI and Refit:

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

Issue POST, PUT, and DELETE requests

In the previous example, all HTTP requests use the GET HTTP predicate. HttpClient also supports other HTTP predicates, including:

  • POST
  • PUT
  • DELETE
  • PATCH

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

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

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

In the previous code, the CreateItemAsync method:

  • Serialize the TodoItem parameter to JSON using System.Text.Json. This configures the serialization process using an instance of JsonSerializerOptions.
  • Create an instance of StringContent to package the serialized JSON for sending in the body of an HTTP request.
  • Call PostAsync to send JSON content to the specified URL. This is the relative URL added to HttpClient.BaseAddress.
  • If the response status code does not indicate success, a call to EnsureSuccessStatusCode throws an exception.

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

The following example demonstrates an HTTP PUT request:

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

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

The following example demonstrates an HTTP DELETE request:

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}

In the previous code, the DeleteItemAsync method called DeleteAsync. Since HTTP DELETE requests usually do not contain body, the DeleteAsync method does not provide an overload that accepts HttpContent instances.

To learn more about how to use different HTTP predicates for HttpClient, see HttpClient.

Outbound Request Middleware

HttpClient has the concept of delegate handlers that can be linked together to handle outbound HTTP requests. IHttpClientFactory:

  • Simplify the definition of the handler to be applied to each named client.
  • Support for registering and linking multiple handlers to generate outbound request middleware pipelines. Each handler can perform work before and after an outbound request. This mode:
    -Similar to inbound middleware pipeline in ASP.NET Core.
    - Provides a mechanism to manage cross-cutting concerns about HTTP requests, such as:
    -Cache
    -Error handling
    -Serialization
    -Logging

Create a delegate handler:

  • Derived from DelegatingHandler.
  • Override SendAsync. Execute code before passing the request to the next handler in the pipeline:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

The code above checks for X-API-KEY headers in the request. If X-API-KEY is missing, BadRequest is returned.

Multiple handlers can be added to the configuration of the HttpClient using Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

ValidateHeaderHandler is registered with DI in the above code. After registration, you can call AddHttpMessageHandler, passing in the type of header.

Multiple handlers can be registered in the order they should be executed. Each handler overrides the next handler until the final HttpClientHandler executes the request:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Use DI in Outbound Request Middleware

When IHttpClientFactory creates a new delegate handler, it uses DI to complete the handler's constructor parameters. IHttpClientFactory creates a separate DI range for each handler, which can cause unexpected behavior when the handler uses a scoped service.

For example, consider the following interface and its implementation, which represents a task as an operation with an identifier OperationId:

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

As the name implies, register IOperationScoped with DI using a limited range of lifetimes:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

The following delegate handlers consume and use IOperationScoped to set the X-OPERATION-ID header for outgoing requests:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

In HttpRequestsSample Download, navigate to/Operation and refresh the page. The request range value for each request changes, but the handler range value only changes every 5 seconds.

Processors can depend on services from any scope. Services on which handlers depend are disposed when handlers are disposed.

Share each request status with the message handler using one of the following methods:

  • Use HttpRequestMessage.Properties to pass data to the handler.
  • Use IHttpContextAccessor to access the current request.
  • Create a custom AsyncLocal <T>storage object to transfer data.

Using Polly-based handlers

IHttpClientFactory is integrated with third-party library Polly. Polly is a comprehensive recovery and temporary failover library for.NET. It allows developers to express policies such as retries, circuit breakers, timeouts, Bulkhead isolation, and fallbacks in a smooth and thread-safe manner.

Provides an extension to enable the use of Polly policy for configuring HttpClient instances. The Polly extension supports adding Polly-based processors to clients. Polly requires the Microsoft.Extensions.Http.Polly NuGet package.

Handle temporary failures

Errors usually occur when an external HTTP call is temporarily executed. AddTransientHttpErrorPolicy allows you to define a policy to handle transient errors. Use the policy configured by AddTransientHttpErrorPolicy to process the following responses:

  • HttpRequestException
  • HTTP 5xx
  • HTTP 408

AddTransientHttpErrorPolicy provides access to the PolicyBuilder object, which is configured to handle errors that indicate possible temporary failures:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

The WaitAndRetryAsync policy is defined in the code above. Up to three retries can be made after a request fails, with an interval of 600 ms.

Dynamic Selection Policy

Extension methods are provided to add Polly-based handlers, such as AddPolicyHandler. The following AddPolicyHandler overload check request to determine the policy to be applied:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

In the above code, if the outbound request is HTTP GET, a 10-second timeout is applied. All other HTTP methods apply a 30-second timeout.

Add multiple Polly handlers

This is common for nested Polly policies:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

In the example above:

  • Two handlers were added.
  • The first handler uses AddTransientHttpErrorPolicy to add a retry policy. If the request fails, you can retry it up to three times.
  • The second AddTransientHttpErrorPolicy call adds a breaker policy. If the attempt fails five times in a row, subsequent external requests are blocked for 30 seconds. The breaker policy is under monitoring. All calls made through this client share the same line state.

Add Policy from Polly Registry

One way to manage common policies is to define them at once and register them with PolicyRegistry.

In the following code:

  • General and Long policies were added.
  • AddPolicyHandlerFromRegistry adds General and Long policies from the registry.
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

For more information on IHttpClientFactory and Polly integration, see Polly Wiki.

HttpClient and lifetime management

Each call to CreateClient on IHttpClientFactory returns a new HttpClient instance. Each named client creates an HttpMessageHandler. The factory manages the lifetime of the HttpMessageHandler instance.

IHttpClientFactory pools HttpMessageHandler instances created by factories to reduce resource consumption. When creating a new HttpClient instance, it is possible to reuse the HttpMessageHandler instance in the pool if the lifetime has not expired.

Since each handler usually manages its own underlying HTTP connection, a pooled handler is required. Creating more handlers than necessary can cause connection delays. Some handlers also keep connections open indefinitely, which prevents the handler from responding to DNS (Domain Name System) changes.

The default lifetime of the handler is two minutes. Defaults can be overridden on each named client:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

HttpClient instances can often be viewed as.NET objects that do not need to be disposed of. Disposal cancels the outgoing request and guarantees that the given HttpClient instance cannot be used after Dispose is called. IHttpClientFactory tracks and disposes of resources used by HttpClient instances.

Keeping each HttpClient instance active for a long time is a common pattern used before the launch of IHttpClientFactory. After migrating to IHttpClientFactory, you no longer need to use this pattern.

An alternative to IHttpClientFactory

By using IHttpClientFactory in DI-enabled applications, you can avoid:

  • Resolve resource exhaustion by sharing HttpMessageHandler instances.
  • Resolve DNS obsolescence by periodically looping HttpMessageHandler instances.

In addition, there are other ways to solve these problems using a long-life Sockets HttpHandler instance.

  • Create an instance of Sockets HttpHandler at application startup and use it throughout the lifecycle of the application.
  • Configure PooledConnectionLifetime to an appropriate value based on the DNS refresh time.
  • Create an HttpClient instance using the new HttpClient(handler, disposeHandler: false) as needed.

The above approach uses IHttpClientFactory to solve resource management problems in a similar way.

  • Sockets HttpHandler shares a connection between HttpClient instances. This share prevents sockets from running out.
  • Sockets HttpHandler will connect according to the PooledConnectionLifetime loop to avoid DNS outdated problems.

Cookies

Sharing HttpMessageHandler instances results in sharing CookieContainer objects. Unexpected CookieContainer object sharing often results in incorrect code. For applications that require cookie s, consider doing either of the following:

  • Disable automatic cookie processing
  • Avoid IHttpClientFactory

Call ConfigurePrimaryHttpMessageHandler to disable automatic cookie processing:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Logging

Clients created through IHttpClientFactory log all requests. Enable the appropriate level of information in the logging configuration to view the default log messages. Include additional log records only at the trace level (for example, log records for request headers).

The log category used for each client contains the client name. For example, a message named MyNamedClient with a client record category of System.Net.Http.HttpClient.MyNamedClient.LogicalHandler is recorded. Messages suffixed with LogicalHandler occur outside the request handler pipeline. At request time, log messages before any other handler in the pipeline processes the request. When responding, a message is logged after any other pipeline handler receives the response.

Logging also occurs inside the request handler pipeline. In the MyNamedClient example, the log category for these messages is "System.Net.Http.HttpClient.MyNamedClient.ClientHandler". At request time, log messages after all other handlers have run and just before the request is made. When responding, this log record contains the state of the response before it is passed back through the handler pipeline.

By enabling logging inside and outside the pipeline, you can check for changes made by other pipeline handlers. This may include a change to the request header or a change to the response status code.

By including client names in the log category, you can filter logs for specific named clients.

Configure HttpMessageHandler

It is necessary to control the configuration of the internal HttpMessageHandler used by clients.

IHttpClientBuilder is returned when a named or typed client is added. The ConfigurePrimaryHttpMessageHandler extension method can be used to define delegates. Delegate the primary HttpMessageHandler used to create and configure the client:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Use IHttpClientFactory in console applications

In the console, add the following package references to the project:

  • Microsoft.Extensions.Hosting
  • Microsoft.Extensions.Http

In the following example:

  • IHttpClientFactory is registered in the service container of the generic host.
  • MyService creates a client factory instance from the service to create an HttpClient. HttpClient is used to retrieve web pages.
  • Main creates a scope to execute the GetPage method of the service and writes the first 500 characters of the page content to the console.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Header Propagation Middleware

Header propagation is an ASP.NET Core middleware that propagates HTTP headers from incoming requests to outgoing HTTP client requests. Propagate using headers:

  • Reference to the Microsoft.AspNetCore.HeaderPropagation package.
  • Configure middleware and HttpClient in Startup:
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
    services.AddHeaderPropagation(options =>
    {
        options.Headers.Add("X-TraceId");
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseHeaderPropagation();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
  • Clients include configured headers in outbound requests:
var client = clientFactory.CreateClient("MyForwardingClient");
var response = client.GetAsync(...);

Topics: .NET Middleware http