使用 .NET 的 IHttpClientFactory

本文介绍如何使用 IHttpClientFactory 创建具有各种 .NET 基础知识的 HttpClient 类型,例如依赖关系注入 (DI)、日志记录和配置。 HttpClient 类型是在 2012 年发布的 .NET Framework 4.5 中引入的。 换句话说,它已经存在一段时间了。 HttpClient 用于从由 Uri 标识的网络资源发出 HTTP 请求和处理 HTTP 响应。 HTTP 协议占所有 Internet 流量的绝大部分。

根据推动最佳做法的新式应用程序开发原则,IHttpClientFactory 充当工厂抽象,可以使用自定义配置创建 HttpClient 实例。 .NET Core 2.1 中引入了 IHttpClientFactory。 常见的基于 HTTP 的 .NET 工作负载可以轻松利用可复原和瞬态故障处理第三方中间件。

注意

如果你的应用需要 Cookie,最好不要在应用中使用 IHttpClientFactory。 有关管理客户端的替代方法,请参阅使用 HTTP 客户端的指南

重要

IHttpClientFactory 创建的 HttpClient 实例的生存期管理与手动创建的实例完全不同。 策略是使用由 IHttpClientFactory 创建的短期客户端,或设置了 PooledConnectionLifetime 的长期客户端。 有关详细信息,请参阅 HttpClient 生存期管理部分和使用 HTTP 客户端的指南

IHttpClientFactory 类型

本文中的所有示例源代码都依赖于 Microsoft.Extensions.Http NuGet 包。 此外,向免费的 {JSON} 占位符 API 发出 HTTP GET 请求以获取用户 Todo 对象。

调用任何 AddHttpClient 扩展方法时,将 IHttpClientFactory 和相关服务添加到 IServiceCollectionIHttpClientFactory 类型具有以下优点:

  • HttpClient 类公开为 DI 就绪类型。
  • 提供一个中心位置,用于命名和配置逻辑 HttpClient 实例。
  • 通过 HttpClient 中的委托处理程序来编码出站中间件的概念。
  • 提供基于 Polly 的中间件的扩展方法,以利用 HttpClient 中的委托处理程序。
  • 管理基础 HttpClientHandler 实例的缓存和生存期。 自动管理可避免手动管理 HttpClient 生存期时出现的常见域名系统 (DNS) 问题。
  • (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。

消耗模式

在应用中可以通过以下多种方式使用 IHttpClientFactory

最佳方法取决于应用要求。

基本用法

若要注册 IHttpClientFactory,请调用 AddHttpClient

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient();
builder.Services.AddTransient<TodoService>();

using IHost host = builder.Build();

使用服务可能需要 IHttpClientFactory 作为带有 DI 的构造函数参数。 以下代码使用 IHttpClientFactory 来创建 HttpClient 实例:

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

namespace BasicHttp.Example;

public sealed class TodoService(
    IHttpClientFactory httpClientFactory,
    ILogger<TodoService> logger)
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        using HttpClient client = httpClientFactory.CreateClient();
        
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo types
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"https://jsonplaceholder.typicode.com/todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

像前面的示例一样,使用 IHttpClientFactory 是重构现有应用的好方法。 这不会影响 HttpClient 的使用方式。 在现有应用中创建 HttpClient 实例的位置,使用对 CreateClient 的调用替换这些匹配项。

命名客户端

在以下情况下,命名客户端是一个不错的选择:

  • 应用需要 HttpClient 的许多不同用法。
  • 许多 HttpClient 实例具有不同的配置。

可以在 IServiceCollection 上注册时指定命名 HttpClient 的配置:

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

string? httpClientName = builder.Configuration["TodoHttpClientName"];
ArgumentException.ThrowIfNullOrEmpty(httpClientName);

builder.Services.AddHttpClient(
    httpClientName,
    client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

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

在上述代码中,客户端配置如下:

  • "TodoHttpClientName" 下的配置中提取的名称。
  • 基址为 https://jsonplaceholder.typicode.com/
  • 一个 "User-Agent" 标头。

可以使用配置来指定 HTTP 客户端名称,这有助于避免在添加和创建时误命名客户端。 在本例中,appsettings.json 文件用于配置 HTTP 客户端名称:

{
    "TodoHttpClientName": "JsonPlaceholderApi"
}

可以轻松扩展此配置,并存储有关你希望 HTTP 客户端如何工作的更多详细信息。 有关详细信息,请参阅 .NET 中的配置

创建客户端

每次调用 CreateClient 时:

  • 创建 HttpClient 的新实例。
  • 调用配置操作。

要创建命名客户端,请将其名称传递到 CreateClient 中:

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

namespace NamedHttp.Example;

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

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

    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        string? httpClientName = _configuration["TodoHttpClientName"];
        using HttpClient client = _httpClientFactory.CreateClient(httpClientName ?? "");

        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            _logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

在上述代码中,HTTP 请求不需要指定主机名。 代码可以仅传递路径,因为采用了为客户端配置的基址。

类型化客户端

类型化客户端:

  • 提供与命名客户端一样的功能,不需要将字符串用作密钥。
  • 在使用客户端时提供 IntelliSense 和编译器帮助。
  • 提供单个位置来配置特定 HttpClient 并与其进行交互。 例如,可以使用单个类型化客户端:
    • 对于单个后端终结点。
    • 封装处理终结点的所有逻辑。
  • 使用 DI 且可以被注入到应用中需要的位置。

类型化客户端在构造函数中接受 HttpClient 参数:

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

namespace TypedHttp.Example;

public sealed class TodoService(
    HttpClient httpClient,
    ILogger<TodoService> logger) : IDisposable
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await httpClient.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }

    public void Dispose() => httpClient?.Dispose();
}

在上述代码中:

  • 该配置是在将类型化的客户端添加到服务集合时设置的。
  • HttpClient 被分配为类范围变量(字段),并与公开的 API 一起使用。

可以创建特定于 API 的方法来公开 HttpClient 功能。 例如,GetUserTodosAsync 方法可封装代码以检索特定于用户的 Todo 对象。

以下代码会调用 AddHttpClient 来注册类型化客户端类:

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>(
    client =>
    {
        // Set the base address of the typed client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

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

使用 DI 将类型客户端注册为暂时客户端。 在上述代码中,AddHttpClientTodoService 注册为暂时性服务。 此注册使用工厂方法执行以下操作:

  1. 创建 HttpClient 的实例。
  2. 创建 TodoService 的实例,将 HttpClient 的实例传入其构造函数。

重要

在单一实例服务中使用类型化客户端可能很危险。 有关详细信息,请参阅避免在单一实例服务中使用类型化客户端部分。

注意

在使用 AddHttpClient<TClient> 方法注册类型化客户端时,该 TClient 类型必须具有接受 HttpClient 参数的构造函数。 此外,不应将 TClient 类型单独注册到 DI 容器。

命名客户端和类型化客户端

命名客户端和类型化客户端各有利弊。 有一种方法可以将这两种客户端类型结合起来,以获得两全其美的效果。

主要用例如下:使用相同的类型化客户端,但针对不同的域。 例如,你有一个主要服务和一个辅助服务,并且它们公开的功能完全相同。 这意味着你可以使用相同类型的客户端来包装 HttpClient 用法以发出请求、处理响应和处理错误。 将使用完全相同的代码,但具有不同的配置(例如不同的基地址、超时和凭据)。

以下示例使用类型化客户端部分下显示的同一 TodoService 类型化客户端。

首先注册命名客户端和类型化客户端。

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>("primary"
    client =>
    {
        // Configure the primary typed client
        client.BaseAddress = new Uri("https://primary-host-address.com/");
        client.Timeout = TimeSpan.FromSeconds(3);
    });

// Register the same typed client but with different settings
builder.Services.AddHttpClient<TodoService>("secondary"
    client =>
    {
        // Configure the secondary typed client
        client.BaseAddress = new Uri("https://secondary-host-address.com/");
        client.Timeout = TimeSpan.FromSeconds(10);
    });

在上述代码中:

  • 第一个 AddHttpClient 调用会在 primary 名称下注册一个 TodoService 类型化客户端。 底层的 HttpClient 指向主服务并且有很短的超时时间。
  • 第二个 AddHttpClient 调用会在 secondary 名称下注册一个 TodoService 类型化客户端。 底层的 HttpClient 指向辅助服务并且有较长的超时时间。
using IHost host = builder.Build();

// Fetch an IHttpClientFactory instance to create a named client
IHttpClientFactory namedClientFactory =
    host.Services.GetRequiredService<IHttpClientFactory>();

// Fetch an ITypedHttpClientFactory<TodoService> instance to create a named and typed client
ITypedHttpClientFactory<TodoService> typedClientFactory  =
    host.Services.GetRequiredService<ITypedHttpClientFactory<TodoService>>();

// Create a TodoService instance against the primary host
var primaryClient = namedClientFactory.CreateClient("primary");
var todoService = typedClientFactory.CreateClient(primaryClient);

在上述代码中:

  • 从 DI 容器中检索 IHttpClientFactory 实例,以便能够通过其 CreateClient 方法创建命名客户端。
  • 从 DI 容器中检索 ITypedHttpClientFactory<TodoService> 实例,以便能够通过其 CreateClient 方法创建类型化客户端。
    • CreateClient 重载会接收一个命名 HttpClient(具有正确的配置)作为其参数。
    • 将创建的 todoService 配置为使用主服务。

注意

IHttpClientFactory 类型驻留在 System.Net.Http 命名空间内,而 ITypedHttpClientFactory 类型驻留在 Microsoft.Extensions.Http 内。

重要

使用实现类(在前面的示例中为 TodoService)作为 ITypedHttpClientFactory 的类型参数。 即使你也有抽象(如 ITodoService 接口),你仍然必须使用实现。 如果意外使用了抽象 (ITodoService),那么当你调用其 CreateClient 时,它将抛出 InvalidOperationException

try
{
    Todo[] todos = await todoService.GetUserTodosAsync(4);
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
    // The request timed out against the primary host

    // Create a TodoService instance against the secondary host
    var fallbackClient = namedClientFactory.CreateClient("secondary");
    var todoFallbackService = typedClientFactory.CreateClient(fallbackClient);

    // Issue request against the secondary host
    Todo[] todos = await todoFallbackService.GetUserTodosAsync(4);
}

在上述代码中:

  • 它会尝试向主要服务发出请求。
  • 如果请求超时(时间超过 3 秒),则会抛出内部带有 TimeoutExceptionTaskCanceledException
  • 如果超时,则会创建并使用新的客户端,而该客户端现在会面向辅助服务。

生成的客户端

IHttpClientFactory 可结合第三方库(例如 Refit)使用。 Refit 是.NET 的 REST 库。 它允许声明性 REST API 定义,将接口方法映射到终结点。 RestService 动态生成该接口的实现,使用 HttpClient 进行外部 HTTP 调用。

请考虑以下 record 类型:

namespace Shared;

public record class Todo(
    int UserId,
    int Id,
    string Title,
    bool Completed);

以下示例依赖于 Refit.HttpClientFactory NuGet 包,并且是一个简单的接口:

using Refit;
using Shared;

namespace GeneratedHttp.Example;

public interface ITodoService
{
    [Get("/todos?userId={userId}")]
    Task<Todo[]> GetUserTodosAsync(int userId);
}

前面的 C# 接口:

  • 定义一个名为 GetUserTodosAsync 的方法,该方法返回一个 Task<Todo[]> 实例。
  • 使用外部 API 的路径和查询字符串声明 Refit.GetAttribute 属性。

可以添加类型化客户端,使用 Refit 生成实现:

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddRefitClient<ITodoService>()
    .ConfigureHttpClient(client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

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

可以在必要时使用定义的接口,以及由 DI 和 Refit 提供的实现。

发出 POST、PUT 和 DELETE 请求

在前面的示例中,所有 HTTP 请求均使用 GET HTTP 谓词。 HttpClient 还支持其他 HTTP 谓词,其中包括:

  • POST
  • PUT
  • DELETE
  • PATCH

有关受支持的 HTTP 谓词的完整列表,请参阅 HttpMethod。 有关发出 HTTP 请求的详细信息,请参阅使用 HttpClient 发送请求

下面的示例演示如何发出 HTTP POST 请求:

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

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

    httpResponse.EnsureSuccessStatusCode();
}

在前面的代码中,CreateItemAsync 方法:

  • 使用 System.Text.JsonItem 参数序列化为 JSON。 这将使用 JsonSerializerOptions 的实例来配置序列化过程。
  • 创建 StringContent 的实例,以打包序列化的 JSON 以便在 HTTP 请求的正文中发送。
  • 调用 PostAsync 将 JSON 内容发送到指定的 URL。 这是添加到 HttpClient.BaseAddress 的相对 URL。
  • 如果响应状态代码不指示成功,则调用 EnsureSuccessStatusCode 引发异常。

HttpClient 还支持其他类型的内容。 例如,MultipartContentStreamContent。 有关受支持的内容的完整列表,请参阅 HttpContent

下面的示例演示了一个 HTTP PUT 请求:

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

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

    httpResponse.EnsureSuccessStatusCode();
}

前面的代码与 POST 示例非常相似。 UpdateItemAsync 方法调用 PutAsync 而不是 PostAsync

下面的示例演示了一个 HTTP DELETE 请求:

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

    httpResponse.EnsureSuccessStatusCode();
}

在前面的代码中,DeleteItemAsync 方法调用 DeleteAsync。 由于 HTTP DELETE 请求通常不包含正文,因此 DeleteAsync 方法不提供接受 HttpContent 实例的重载。

要详细了解如何将不同的 HTTP 谓词用于 HttpClient,请参阅 HttpClient

HttpClient 生存期管理

每次对 IHttpClientFactory 调用 CreateClient 都会返回一个新 HttpClient 实例。 每个客户端名称创建一个 HttpClientHandler 实例。 工厂管理 HttpClientHandler 实例的生存期。

IHttpClientFactory 将缓存工厂创建的 HttpClientHandler 实例,以减少资源消耗。 创建新的 HttpClient 实例时,可能会重用缓存中的 HttpClientHandler 实例(如果生存期尚未到期的话)。

由于每个处理程序通常管理自己的基础 HTTP 连接池,因此需要缓存处理程序。 创建超出必要数量的处理程序可能会导致套接字耗尽和连接延迟。 部分处理程序还保持连接无期限地打开,这样可以防止处理程序对 DNS 更改作出反应。

处理程序的默认生存期为两分钟。 要替代默认值,请在 IServiceCollection 上为每个客户端调用 SetHandlerLifetime

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

重要

IHttpClientFactory 创建的 HttpClient 实例是短期的。

  • 回收和重新创建 HttpMessageHandler 在其生存期到期时对于 IHttpClientFactory 至关重要,可确保处理程序对 DNS 更改做出反应。 HttpClient 在创建特定处理程序实例时与之绑定,因此应及时请求新的 HttpClient 实例,以确保客户端将获取更新后的处理程序。

  • 释放工厂创建的此类 HttpClient 实例不会导致套接字耗尽,因为它的处置不会触发 HttpMessageHandler 的处置。 IHttpClientFactory 跟踪和释放用于创建 HttpClient 实例的资源,特别是 HttpMessageHandler 实例,只要其生存期到期,并且 HttpClient 不再使用这些实例。

长时间使单个 HttpClient 实例保持活动状态是一种常见模式,可用作 IHttpClientFactory 的替代项,但是,此模式需要其他设置,例如 PooledConnectionLifetime。 可以使用具有 PooledConnectionLifetime 的长期客户端,也可以使用由 IHttpClientFactory 创建的短期客户端。 有关应用中使用的策略的信息,请参阅使用 HTTP 客户端的指南

配置 HttpMessageHandler

控制客户端使用的内部 HttpMessageHandler 的配置是有必要的。

在添加命名客户端或类型化客户端时,会返回 IHttpClientBuilderConfigurePrimaryHttpMessageHandler 扩展方法可以用于在 IServiceCollection 上定义委托。 委托用于创建和配置客户端使用的主要 HttpMessageHandler

.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new HttpClientHandler
    {
        AllowAutoRedirect = false,
        UseDefaultCredentials = true
    };
});

通过配置 HttClientHandler在处理,可以在处理程序的各种其他属性中为 HttpClient 实例指定代理。 有关详细信息,请参阅每个客户端的代理

其他配置

有几个额外的配置选项可用于控制 IHttpClientHandler

方法 说明
AddHttpMessageHandler 为已命名的 HttpClient 添加附加消息处理程序。
AddTypedClient 配置 TClient 与已命名的 HttpClient(与 IHttpClientBuilder 关联)之间的绑定。
ConfigureHttpClient 添加用于配置已命名的 HttpClient 的委托。
ConfigureHttpMessageHandlerBuilder 添加一个委托,该委托将用于使用 HttpMessageHandlerBuilder 为已命名的 HttpClient 配置消息处理程序。
ConfigurePrimaryHttpMessageHandler 从已命名的 HttpClient 的依赖关系注入容器中配置主要 HttpMessageHandler
RedactLoggedHeaders 设置其值应在记录之前进行修正的 HTTP 标头名称的集合。
SetHandlerLifetime 设置可重复使用 HttpMessageHandler 实例的时长。 每个已命名的客户端都可自行配置处理程序生存期值。

将 IHttpClientFactory 和 SocketsHttpHandler 一起使用

在 .NET Core 2.1 中添加了 HttpMessageHandlerSocketsHttpHandler 实现,该实现允许配置 PooledConnectionLifetime。 此设置用于确保处理程序对 DNS 更改做出反应,因此,使用 SocketsHttpHandler 被视为使用 IHttpClientFactory 替代项。 有关详细信息,请参阅使用 HTTP 客户端的指南

但是,SocketsHttpHandlerIHttpClientFactory 可以结合使用来提高可配置性。 通过使用这两个 API,可以同时受益于低级别(例如,使用 LocalCertificateSelectionCallback 进行动态证书选择)和高级别(例如,利用 DI 集成和多个客户端配置)上的可配置性。

若要同时使用这两个 API,请执行以下操作:

  1. 指定 SocketsHttpHandler 作为 PrimaryHandler 并设置其 PooledConnectionLifetime(例如,设置为 HandlerLifetime 中以前的值)。
  2. SocketsHttpHandler 处理连接池和回收一样,不再需要 IHttpClientFactory 级别的处理程序回收。 可以通过将 HandlerLifetime 设置为 Timeout.InfiniteTimeSpan 来禁用它。
services.AddHttpClient(name)
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new SocketsHttpHandler()
        {
            PooledConnectionLifetime = TimeSpan.FromMinutes(2)
        };
    })
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime

避免在单一实例服务中使用类型化客户端

使用命名客户端方法时,IHttpClientFactory 会注入到服务中,并且每次需要 HttpClient 实例时,都会通过调用 CreateClient 创建 HttpClient 实例。

但是,使用类型化客户端方法,类型化客户端通常是注入到服务中的暂时性对象。 这可能会导致问题,因为可以将类型化客户端注入到单一实例服务中。

重要

类型化客户端应该是短期客户端,与 IHttpClientFactory 创建的 HttpClient 实例的意义相同(有关详细信息,请参阅HttpClient生存期管理)。 创建类型化客户端实例后,IHttpClientFactory 就无法控制它。 如果在单一实例中捕获了类型化客户端实例,它可能会阻止它对 DNS 更改做出反应,从而破坏 IHttpClientFactory 的其中一个目的。

如果需要在单一实例服务中使用 HttpClient 实例,请考虑以下选项:

  • 请改用命名客户端方法,根据需要在单一实例服务中注入 IHttpClientFactory 并重新创建 HttpClient 实例。
  • 如果需要类型化客户端方法,请使用配置了 PooledConnectionLifetimeSocketsHttpHandler 作为主处理程序。 有关结合使用 SocketsHttpHandlerIHttpClientFactory 的详细信息,请参阅结合使用 IHttpClientFactory 和 SocketsHttpHandler 部分。

IHttpClientFactory 中的消息处理程序范围

IHttpClientFactory 为每个 HttpMessageHandler 实例创建单独的 DI 范围。 这些 DI 范围独立于应用程序 DI 范围(例如,ASP.NET 传入请求范围或用户创建的手动 DI 范围),因此它们不会共享区分范围的服务实例。 消息处理程序范围与处理程序生存期相关联,并且可能会超过应用程序范围,例如,这可能导致重用相同的 HttpMessageHandler 实例,在多个传入请求之间具有相同的注入区分范围的依赖关系。

显示两个应用程序 DI 范围和一个单独的消息处理程序范围的关系图

强烈建议用户不要在 HttpMessageHandler 实例中缓存与范围相关的信息(例如来自 HttpContext 的数据),并谨慎使用区分范围的依赖关系以避免泄漏敏感信息。

如果需要从消息处理程序访问应用 DI 范围(例如进行身份验证),请将范围感知逻辑封装在单独的暂时性 DelegatingHandler 中,并将其包装在来自 IHttpClientFactory 缓存的 HttpMessageHandler 实例周围。 要访问处理程序,可为任何已注册的命名客户端调用 IHttpMessageHandlerFactory.CreateHandler。 在这种情况下,你将使用构造的处理程序自行创建 HttpClient 实例。

关系图显示如何通过单独的暂时性消息处理程序和 IHttpMessageHandlerFactory 获取对应用 DI 范围的访问权限

以下示例演示了如何创建具有范围感知 DelegatingHandlerHttpClient

if (scopeAwareHandlerType != null)
{
    if (!typeof(DelegatingHandler).IsAssignableFrom(scopeAwareHandlerType))
    {
        throw new ArgumentException($"""
            Scope aware HttpHandler {scopeAwareHandlerType.Name} should
            be assignable to DelegatingHandler
            """);
    }

    // Create top-most delegating handler with scoped dependencies
    scopeAwareHandler = (DelegatingHandler)_scopeServiceProvider.GetRequiredService(scopeAwareHandlerType); // should be transient
    if (scopeAwareHandler.InnerHandler != null)
    {
        throw new ArgumentException($"""
            Inner handler of a delegating handler {scopeAwareHandlerType.Name} should be null.
            Scope aware HttpHandler should be registered as Transient.
            """);
    }
}

// Get or create HttpMessageHandler from HttpClientFactory
HttpMessageHandler handler = _httpMessageHandlerFactory.CreateHandler(name);

if (scopeAwareHandler != null)
{
    scopeAwareHandler.InnerHandler = handler;
    handler = scopeAwareHandler;
}

HttpClient client = new(handler);

进一步的解决方法可以遵循一个扩展方法,以通过有权访问当前应用范围的暂时性服务注册范围感知 DelegatingHandler 和替代默认的 IHttpClientFactory 注册:

public static IHttpClientBuilder AddScopeAwareHttpHandler<THandler>(
    this IHttpClientBuilder builder) where THandler : DelegatingHandler
{
    builder.Services.TryAddTransient<THandler>();
    if (!builder.Services.Any(sd => sd.ImplementationType == typeof(ScopeAwareHttpClientFactory)))
    {
        // Override default IHttpClientFactory registration
        builder.Services.AddTransient<IHttpClientFactory, ScopeAwareHttpClientFactory>();
    }

    builder.Services.Configure<ScopeAwareHttpClientFactoryOptions>(
        builder.Name, options => options.HttpHandlerType = typeof(THandler));

    return builder;
}

有关详细信息,请参阅完整示例

另请参阅