在 ASP.NET Core 中使用 IHttpClientFactory 发出 HTTP 请求

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

作者:Kirk LarkinSteve GordonGlenn CondronRyan Nowak

可以注册 IHttpClientFactory 并将其用于配置和创建应用中的 HttpClient 实例。 IHttpClientFactory 的优势如下:

  • 提供一个中心位置,用于命名和配置逻辑 HttpClient 实例。 例如,可注册和配置名为 github 的客户端,使其访问 GitHub。 可以注册一个默认客户端用于一般性访问。
  • 通过 HttpClient 中的委托处理程序来编码出站中间件的概念。 提供基于 Polly 的中间件的扩展,以利用 HttpClient 中的委托处理程序。
  • 管理基础 HttpClientMessageHandler 实例的池和生存期。 自动管理可避免手动管理 HttpClient 生存期时出现的常见 DNS(域名系统)问题。
  • (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。

此主题版本中的示例代码使用 System.Text.Json 来对 HTTP 响应中返回的 JSON 内容进行反序列化。 对于使用 Json.NETReadAsAsync<T> 的示例,请使用版本选择器选择此主题的 2.x 版本。

消耗模式

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

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

基本用法

通过在 Program.cs 中调用 AddHttpClient 来注册 IHttpClientFactory

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHttpClient();

可以使用依赖项注入 (DI) 来请求 IHttpClientFactory。 以下代码使用 IHttpClientFactory 来创建 HttpClient 实例:

public class BasicModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public BasicModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { HeaderNames.Accept, "application/vnd.github.v3+json" },
                { HeaderNames.UserAgent, "HttpRequestsSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

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

命名客户端

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

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

Program.cs 中注册时指定命名 HttpClient 的配置:

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // using Microsoft.Net.Http.Headers;
    // The GitHub API requires two headers.
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.UserAgent, "HttpRequestsSample");
});

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

  • 基址为 https://api.github.com/
  • 使用 GitHub API 需要的两个标头。

CreateClient

每次调用 CreateClient 时:

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

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

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public NamedClientModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpClient = _httpClientFactory.CreateClient("GitHub");
        var httpResponseMessage = await httpClient.GetAsync(
            "repos/dotnet/AspNetCore.Docs/branches");

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

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

类型化客户端

类型化客户端:

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

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

public class GitHubService
{
    private readonly HttpClient _httpClient;

    public GitHubService(HttpClient httpClient)
    {
        _httpClient = httpClient;

        _httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    }

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
        await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
            "repos/dotnet/AspNetCore.Docs/branches");
}

在上述代码中:

  • 配置转移到了类型化客户端中。
  • 提供的 HttpClient 实例存储为私有字段。

可以创建特定于 API 的方法来公开 HttpClient 功能。 例如,创建 GetAspNetCoreDocsBranches 方法来封装代码以检索文档 GitHub 分支。

以下代码调用 Program.cs 中的 AddHttpClient 来注册 GitHubService 类型的客户端类:

builder.Services.AddHttpClient<GitHubService>();

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

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

可以直接插入或使用类型化客户端:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public TypedClientModel(GitHubService gitHubService) =>
        _gitHubService = gitHubService;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
        }
        catch (HttpRequestException)
        {
            // ...
        }
    }
}

也可以在 Program.cs 中注册时指定类型化客户端的配置,而不是在类型化客户端的构造函数中指定:

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // ...
});

生成的客户端

IHttpClientFactory 可结合第三方库(例如 Refit)使用。 Refit 是适用于 .NET 的 REST 库。 它将 REST API 转换为实时接口。 调用 AddRefitClient 以生成接口的动态实现,该接口使用 HttpClient 进行外部 HTTP 调用。

自定义接口表示外部 API:

public interface IGitHubClient
{
    [Get("/repos/dotnet/AspNetCore.Docs/branches")]
    Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}

调用 AddRefitClient 生成动态实现,然后调用 ConfigureHttpClient 配置底层 HttpClient

builder.Services.AddRefitClient<IGitHubClient>()
    .ConfigureHttpClient(httpClient =>
    {
        httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    });

使用 DI 访问 IGitHubClient 的动态实现:

public class RefitModel : PageModel
{
    private readonly IGitHubClient _gitHubClient;

    public RefitModel(IGitHubClient gitHubClient) =>
        _gitHubClient = gitHubClient;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
        }
        catch (ApiException)
        {
            // ...
        }
    }
}

发出 POST、PUT 和 DELETE 请求

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

  • POST
  • PUT
  • 删除
  • PATCH

有关受支持的 HTTP 谓词的完整列表,请参阅 HttpMethod

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

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json); // using static System.Net.Mime.MediaTypeNames;

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

    httpResponseMessage.EnsureSuccessStatusCode();
}

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

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

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

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

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json);

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

    httpResponseMessage.EnsureSuccessStatusCode();
}

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

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

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

    httpResponseMessage.EnsureSuccessStatusCode();
}

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

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

出站请求中间件

HttpClient 具有委托处理程序的概念,这些委托处理程序可以链接在一起,处理出站 HTTP 请求。 IHttpClientFactory

  • 简化定义应用于各命名客户端的处理程序。
  • 支持注册和链接多个处理程序,以生成出站请求中间件管道。 每个处理程序都可以在出站请求前后执行工作。 此模式:
    • 类似于 ASP.NET Core 中的入站中间件管道。
    • 提供一种机制来管理有关 HTTP 请求的横切关注点,例如:
      • caching
      • 错误处理
      • 序列化
      • 日志记录

创建委托处理程序:

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(
                    "The API key header X-API-KEY is required.")
            };
        }

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

上述代码检查请求中是否存在 X-API-KEY 标头。 如果缺失 X-API-KEY,则返回 BadRequest

可使用 Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler 将多个处理程序添加到 HttpClient 的配置中:

builder.Services.AddTransient<ValidateHeaderHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<ValidateHeaderHandler>();

在上述代码中通过 DI 注册了 ValidateHeaderHandler。 注册后可以调用 AddHttpMessageHandler,传入标头的类型。

可以按处理程序应该执行的顺序注册多个处理程序。 每个处理程序都会覆盖下一个处理程序,直到最终 HttpClientHandler 执行请求:

builder.Services.AddTransient<SampleHandler1>();
builder.Services.AddTransient<SampleHandler2>();

builder.Services.AddHttpClient("MultipleHttpMessageHandlers")
    .AddHttpMessageHandler<SampleHandler1>()
    .AddHttpMessageHandler<SampleHandler2>();

在前面的代码中,SampleHandler1 先运行,再运行 SampleHandler2

在出站请求中间件中使用 DI

IHttpClientFactory 创建新的委托处理程序时,它使用 DI 来完成处理程序的构造函数参数。 IHttpClientFactory 为每个处理程序创建单独的 DI 范围,当处理程序使用限定范围的服务时,这可能导致意外的行为。

例如,请考虑下面的接口及其实现,它将任务表示为带有标识符 OperationId 的操作:

public interface IOperationScoped
{
    string OperationId { get; }
}

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

顾名思义,使用限定范围的生存期向 DI 注册 IOperationScoped

builder.Services.AddScoped<IOperationScoped, OperationScoped>();

以下委托处理程序消耗并使用 IOperationScoped 来设置传出请求的 X-OPERATION-ID 标头:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationScoped;

    public OperationHandler(IOperationScoped operationScoped) =>
        _operationScoped = operationScoped;

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

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

HttpRequestsSample 下载中,导航到 /Operation 并刷新页面。 每个请求的请求范围值发生更改,但处理程序范围值仅每 5 秒钟更改一次。

处理程序可依赖于任何作用域的服务。 处理程序依赖的服务会在处置处理程序时得到处置。

使用以下方法之一将每个请求状态与消息处理程序共享:

使用基于 Polly 的处理程序

IHttpClientFactory 与第三方库 Polly 集成。 Polly 是适用于 .NET 的全面恢复和临时故障处理库。 开发人员通过它可以表达策略,例如以流畅且线程安全的方式处理重试、断路器、超时、Bulkhead 隔离和回退。

提供了扩展方法,以实现将 Polly 策略用于配置的 HttpClient 实例。 Polly 扩展支持将基于 Polly 的处理程序添加到客户端。 Polly 需要 Microsoft.Extensions.Http.Polly NuGet 包。

处理临时故障

错误通常在暂时执行外部 HTTP 调用时发生。 AddTransientHttpErrorPolicy 允许定义一个策略来处理暂时性错误。 使用 AddTransientHttpErrorPolicy 配置的策略处理以下响应:

AddTransientHttpErrorPolicy 提供对 PolicyBuilder 对象的访问权限,该对象配置为处理表示可能的临时故障的错误:

builder.Services.AddHttpClient("PollyWaitAndRetry")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.WaitAndRetryAsync(
            3, retryNumber => TimeSpan.FromMilliseconds(600)));

上述代码中定义了 WaitAndRetryAsync 策略。 请求失败后最多可以重试三次,每次尝试间隔 600 ms。

动态选择策略

提供了扩展方法来添加基于 Polly 的处理程序,例如 AddPolicyHandler。 以下 AddPolicyHandler 重载检查请求以确定要应用的策略:

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

builder.Services.AddHttpClient("PollyDynamic")
    .AddPolicyHandler(httpRequestMessage =>
        httpRequestMessage.Method == HttpMethod.Get ? timeoutPolicy : longTimeoutPolicy);

在上述代码中,如果出站请求为 HTTP GET,则应用 10 秒超时。 其他所有 HTTP 方法应用 30 秒超时。

添加多个 Polly 处理程序

这对嵌套 Polly 策略很常见:

builder.Services.AddHttpClient("PollyMultiple")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.RetryAsync(3))
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

在上面的示例中:

  • 添加了两个处理程序。
  • 第一个处理程序使用 AddTransientHttpErrorPolicy 添加重试策略。 若请求失败,最多可重试三次。
  • 第二个 AddTransientHttpErrorPolicy 调用添加断路器策略。 如果尝试连续失败了 5 次,则会阻止后续外部请求 30 秒。 断路器策略处于监控状态。 通过此客户端进行的所有调用都共享同样的线路状态。

从 Polly 注册表添加策略

管理常用策略的一种方法是一次性定义它们并使用 PolicyRegistry 注册它们。 例如:

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

var policyRegistry = builder.Services.AddPolicyRegistry();

policyRegistry.Add("Regular", timeoutPolicy);
policyRegistry.Add("Long", longTimeoutPolicy);

builder.Services.AddHttpClient("PollyRegistryRegular")
    .AddPolicyHandlerFromRegistry("Regular");

builder.Services.AddHttpClient("PollyRegistryLong")
    .AddPolicyHandlerFromRegistry("Long");

在上述代码中:

  • 将两个策略 RegularLong 添加到 Polly 注册表。
  • AddPolicyHandlerFromRegistry 配置单个命名客户端以使用 Polly 注册表中的这些策略。

有关 IHttpClientFactory 和 Polly 集成的详细信息,请参阅 Polly Wiki

HttpClient 和生存期管理

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

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

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

处理程序的默认生存期为两分钟。 可在每个命名客户端上重写默认值:

builder.Services.AddHttpClient("HandlerLifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

HttpClient 实例通常可视为无需处置的 .NET 对象。 处置既取消传出请求,又保证在调用 Dispose 后无法使用给定的 HttpClient 实例。 IHttpClientFactory 跟踪和处置 HttpClient 实例使用的资源。

保持各个 HttpClient 实例长时间处于活动状态是在 IHttpClientFactory 推出前使用的常见模式。 迁移到 IHttpClientFactory 后,就无需再使用此模式。

IHttpClientFactory 的替代项

通过在启用了 DI 的应用中使用 IHttpClientFactory,可避免:

  • 通过共用 HttpMessageHandler 实例,解决资源耗尽问题。
  • 通过定期循环 HttpMessageHandler 实例,解决 DNS 过时问题。

此外,还有其他方法使用生命周期长的 SocketsHttpHandler 实例来解决上述问题。

  • 在应用启动时创建 SocketsHttpHandler 的实例,并在应用的整个生命周期中使用它。
  • 根据 DNS 刷新时间,将 PooledConnectionLifetime 配置为适当的值。
  • 根据需要,使用 new HttpClient(handler, disposeHandler: false) 创建 HttpClient 实例。

上述方法使用 IHttpClientFactory 解决问题的类似方式解决资源管理问题。

  • SocketsHttpHandlerHttpClient 实例之间共享连接。 此共享可防止套接字耗尽。
  • SocketsHttpHandler 会根据 PooledConnectionLifetime 循环连接,避免出现 DNS 过时问题。

Logging

通过 IHttpClientFactory 创建的客户端记录所有请求的日志消息。 在日志记录配置中启用合适的信息级别可以查看默认日志消息。 仅在跟踪级别包含附加日志记录(例如请求标头的日志记录)。

用于每个客户端的日志类别包含客户端名称。 例如,名为 MyNamedClient 的客户端记录类别为“System.Net.Http.HttpClient.MyNamedClient.LogicalHandler”的消息。 后缀为 LogicalHandler 的消息在请求处理程序管道外部发生。 在请求时,在管道中的任何其他处理程序处理请求之前记录消息。 在响应时,在任何其他管道处理程序接收响应之后记录消息。

日志记录还在请求处理程序管道内部发生。 在 MyNamedClient 示例中,这些消息的日志类别为“System.Net.Http.HttpClient.MyNamedClient.ClientHandler”。 在请求时,在所有其他处理程序运行后,以及刚好要发出请求之前记录消息。 在响应时,此日志记录包含响应在通过处理程序管道被传递回去之前的状态。

在管道内外启用日志记录,可以检查其他管道处理程序做出的更改。 这可能包含对请求标头的更改,或者对响应状态代码的更改。

通过在日志类别中包含客户端名称,可以对特定的命名客户端筛选日志。

配置 HttpMessageHandler

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

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

builder.Services.AddHttpClient("ConfiguredHttpMessageHandler")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            AllowAutoRedirect = true,
            UseDefaultCredentials = true
        });

Cookie

共用 HttpMessageHandler 实例将导致共享 CookieContainer 对象。 意外的 CookieContainer 对象共享通常会导致错误的代码。 对于需要 Cookie 的应用,请考虑执行以下任一操作:

  • 禁用自动 cookie 处理
  • 避免 IHttpClientFactory

调用 ConfigurePrimaryHttpMessageHandler 以禁用自动 cookie 处理:

builder.Services.AddHttpClient("NoAutomaticCookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            UseCookies = false
        });

在控制台应用中使用 IHttpClientFactory

在控制台中,将以下包引用添加到项目中:

如下示例中:

  • IHttpClientFactoryGitHubService 已在通用主机的服务容器中注册。
  • GitHubService 是从 DI 请求的,而 DI 又请求 IHttpClientFactory 的实例。
  • GitHubService 使用 IHttpClientFactory 创建 HttpClient 的实例,并用它检索文档 GitHub 分支。
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var host = new HostBuilder()
    .ConfigureServices(services =>
    {
        services.AddHttpClient();
        services.AddTransient<GitHubService>();
    })
    .Build();

try
{
    var gitHubService = host.Services.GetRequiredService<GitHubService>();
    var gitHubBranches = await gitHubService.GetAspNetCoreDocsBranchesAsync();

    Console.WriteLine($"{gitHubBranches?.Count() ?? 0} GitHub Branches");

    if (gitHubBranches is not null)
    {
        foreach (var gitHubBranch in gitHubBranches)
        {
            Console.WriteLine($"- {gitHubBranch.Name}");
        }
    }
}
catch (Exception ex)
{
    host.Services.GetRequiredService<ILogger<Program>>()
        .LogError(ex, "Unable to load branches from GitHub.");
}

public class GitHubService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public GitHubService(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { "Accept", "application/vnd.github.v3+json" },
                { "User-Agent", "HttpRequestsConsoleSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        httpResponseMessage.EnsureSuccessStatusCode();

        using var contentStream =
            await httpResponseMessage.Content.ReadAsStreamAsync();
        
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubBranch>>(contentStream);
    }
}

public record GitHubBranch(
    [property: JsonPropertyName("name")] string Name);

标头传播中间件

标头传播是一个 ASP.NET Core 中间件,可将 HTTP 标头从传入请求传播到传出 HttpClient 请求。 使用标头传播:

  • 安装 Microsoft.AspNetCore.HeaderPropagation 包。

  • Program.cs 中配置 HttpClient 和中间件管道:

    // Add services to the container.
    builder.Services.AddControllers();
    
    builder.Services.AddHttpClient("PropagateHeaders")
        .AddHeaderPropagation();
    
    builder.Services.AddHeaderPropagation(options =>
    {
        options.Headers.Add("X-TraceId");
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    app.UseHttpsRedirection();
    
    app.UseHeaderPropagation();
    
    app.MapControllers();
    
  • 使用配置的 HttpClient 实例发出出站请求,该实例包括添加的标头。

其他资源

作者:Kirk LarkinSteve GordonGlenn CondronRyan Nowak

可以注册 IHttpClientFactory 并将其用于配置和创建应用中的 HttpClient 实例。 IHttpClientFactory 的优势如下:

  • 提供一个中心位置,用于命名和配置逻辑 HttpClient 实例。 例如,可注册和配置名为 github 的客户端,使其访问 GitHub。 可以注册一个默认客户端用于一般性访问。
  • 通过 HttpClient 中的委托处理程序来编码出站中间件的概念。 提供基于 Polly 的中间件的扩展,以利用 HttpClient 中的委托处理程序。
  • 管理基础 HttpClientMessageHandler 实例的池和生存期。 自动管理可避免手动管理 HttpClient 生存期时出现的常见 DNS(域名系统)问题。
  • (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。

查看或下载示例代码如何下载)。

此主题版本中的示例代码使用 System.Text.Json 来对 HTTP 响应中返回的 JSON 内容进行反序列化。 对于使用 Json.NETReadAsAsync<T> 的示例,请使用版本选择器选择此主题的 2.x 版本。

消耗模式

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

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

基本用法

可以通过调用 AddHttpClient 来注册 IHttpClientFactory

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.

可以使用依赖项注入 (DI) 来请求 IHttpClientFactory。 以下代码使用 IHttpClientFactory 来创建 HttpClient 实例:

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>();
        }
    }
}

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

命名客户端

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

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

可以在 Startup.ConfigureServices 中注册时指定命名 HttpClient 的配置:

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");
});

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

  • 基址为 https://api.github.com/
  • 使用 GitHub API 需要的两个标头。

CreateClient

每次调用 CreateClient 时:

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

要创建命名客户端,请将其名称传递到 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>();
        }
    }
}

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

类型化客户端

类型化客户端:

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

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

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");
    }
}

在上述代码中:

  • 配置转移到了类型化客户端中。
  • HttpClient 对象公开为公共属性。

可以创建特定于 API 的方法来公开 HttpClient 功能。 例如,创建 GetAspNetDocsIssues 方法来封装代码以检索未解决的问题。

以下代码调用 Startup.ConfigureServices 中的 AddHttpClient 来注册类型化客户端类:

services.AddHttpClient<GitHubService>();

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

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

可以直接插入或使用类型化客户端:

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>();
        }
    }
}

可以在 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 封装在类型化客户端中, 定义一个在内部调用 HttpClient 实例的方法,而不是将其公开为属性:

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);
    }
}

在上述代码中,HttpClient 存储在私有字段中。 通过公共 GetRepos 方法访问 HttpClient

生成的客户端

IHttpClientFactory 可结合第三方库(例如 Refit)使用。 Refit 是适用于 .NET 的 REST 库。 它将 REST API 转换为实时接口。 RestService 动态生成该接口的实现,使用 HttpClient 进行外部 HTTP 调用。

定义了接口和答复来代表外部 API 及其响应:

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

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

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

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();
}

可以在必要时使用定义的接口,以及由 DI 和 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();
    }
}

发出 POST、PUT 和 DELETE 请求

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

  • POST
  • PUT
  • 删除
  • PATCH

有关受支持的 HTTP 谓词的完整列表,请参阅 HttpMethod

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

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();
}

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

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

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

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

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();
}

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

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

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

    httpResponse.EnsureSuccessStatusCode();
}

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

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

出站请求中间件

HttpClient 具有委托处理程序的概念,这些委托处理程序可以链接在一起,处理出站 HTTP 请求。 IHttpClientFactory

  • 简化定义应用于各命名客户端的处理程序。
  • 支持注册和链接多个处理程序,以生成出站请求中间件管道。 每个处理程序都可以在出站请求前后执行工作。 此模式:
    • 类似于 ASP.NET Core 中的入站中间件管道。
    • 提供一种机制来管理有关 HTTP 请求的横切关注点,例如:
      • caching
      • 错误处理
      • 序列化
      • 日志记录

创建委托处理程序:

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);
    }
}

上述代码检查请求中是否存在 X-API-KEY 标头。 如果缺失 X-API-KEY,则返回 BadRequest

可使用 Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler 将多个处理程序添加到 HttpClient 的配置中:

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.

在上述代码中通过 DI 注册了 ValidateHeaderHandler。 注册后可以调用 AddHttpMessageHandler,传入标头的类型。

可以按处理程序应该执行的顺序注册多个处理程序。 每个处理程序都会覆盖下一个处理程序,直到最终 HttpClientHandler 执行请求:

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>();

在出站请求中间件中使用 DI

IHttpClientFactory 创建新的委托处理程序时,它使用 DI 来完成处理程序的构造函数参数。 IHttpClientFactory 为每个处理程序创建单独的 DI 范围,当处理程序使用限定范围的服务时,这可能导致意外的行为。

例如,请考虑下面的接口及其实现,它将任务表示为带有标识符 OperationId 的操作:

public interface IOperationScoped 
{
    string OperationId { get; }
}

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

顾名思义,使用限定范围的生存期向 DI 注册 IOperationScoped

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();
}

以下委托处理程序消耗并使用 IOperationScoped 来设置传出请求的 X-OPERATION-ID 标头:

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);
    }
}

HttpRequestsSample下载] 中,导航到 /Operation 并刷新页面。 每个请求的请求范围值发生更改,但处理程序范围值仅每 5 秒钟更改一次。

处理程序可依赖于任何作用域的服务。 处理程序依赖的服务会在处置处理程序时得到处置。

使用以下方法之一将每个请求状态与消息处理程序共享:

使用基于 Polly 的处理程序

IHttpClientFactory 与第三方库 Polly 集成。 Polly 是适用于 .NET 的全面恢复和临时故障处理库。 开发人员通过它可以表达策略,例如以流畅且线程安全的方式处理重试、断路器、超时、Bulkhead 隔离和回退。

提供了扩展方法,以实现将 Polly 策略用于配置的 HttpClient 实例。 Polly 扩展支持将基于 Polly 的处理程序添加到客户端。 Polly 需要 Microsoft.Extensions.Http.Polly NuGet 包。

处理临时故障

错误通常在暂时执行外部 HTTP 调用时发生。 AddTransientHttpErrorPolicy 允许定义一个策略来处理暂时性错误。 使用 AddTransientHttpErrorPolicy 配置的策略处理以下响应:

AddTransientHttpErrorPolicy 提供对 PolicyBuilder 对象的访问权限,该对象配置为处理表示可能的临时故障的错误:

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

    // Remaining code deleted for brevity.

上述代码中定义了 WaitAndRetryAsync 策略。 请求失败后最多可以重试三次,每次尝试间隔 600 ms。

动态选择策略

提供了扩展方法来添加基于 Polly 的处理程序,例如 AddPolicyHandler。 以下 AddPolicyHandler 重载检查请求以确定要应用的策略:

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);

在上述代码中,如果出站请求为 HTTP GET,则应用 10 秒超时。 其他所有 HTTP 方法应用 30 秒超时。

添加多个 Polly 处理程序

这对嵌套 Polly 策略很常见:

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

在上面的示例中:

  • 添加了两个处理程序。
  • 第一个处理程序使用 AddTransientHttpErrorPolicy 添加重试策略。 若请求失败,最多可重试三次。
  • 第二个 AddTransientHttpErrorPolicy 调用添加断路器策略。 如果尝试连续失败了 5 次,则会阻止后续外部请求 30 秒。 断路器策略处于监控状态。 通过此客户端进行的所有调用都共享同样的线路状态。

从 Polly 注册表添加策略

管理常用策略的一种方法是一次性定义它们并使用 PolicyRegistry 注册它们。

在以下代码中:

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.

有关 IHttpClientFactory 和 Polly 集成的详细信息,请参阅 Polly Wiki

HttpClient 和生存期管理

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

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

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

处理程序的默认生存期为两分钟。 可在每个命名客户端上重写默认值:

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

    // Remaining code deleted for brevity.

HttpClient 实例通常可视为无需处置的 .NET 对象。 处置既取消传出请求,又保证在调用 Dispose 后无法使用给定的 HttpClient 实例。 IHttpClientFactory 跟踪和处置 HttpClient 实例使用的资源。

保持各个 HttpClient 实例长时间处于活动状态是在 IHttpClientFactory 推出前使用的常见模式。 迁移到 IHttpClientFactory 后,就无需再使用此模式。

IHttpClientFactory 的替代项

通过在启用了 DI 的应用中使用 IHttpClientFactory,可避免:

  • 通过共用 HttpMessageHandler 实例,解决资源耗尽问题。
  • 通过定期循环 HttpMessageHandler 实例,解决 DNS 过时问题。

此外,还有其他方法使用生命周期长的 SocketsHttpHandler 实例来解决上述问题。

  • 在应用启动时创建 SocketsHttpHandler 的实例,并在应用的整个生命周期中使用它。
  • 根据 DNS 刷新时间,将 PooledConnectionLifetime 配置为适当的值。
  • 根据需要,使用 new HttpClient(handler, disposeHandler: false) 创建 HttpClient 实例。

上述方法使用 IHttpClientFactory 解决问题的类似方式解决资源管理问题。

  • SocketsHttpHandlerHttpClient 实例之间共享连接。 此共享可防止套接字耗尽。
  • SocketsHttpHandler 会根据 PooledConnectionLifetime 循环连接,避免出现 DNS 过时问题。

Cookie

共用 HttpMessageHandler 实例将导致共享 CookieContainer 对象。 意外的 CookieContainer 对象共享通常会导致错误的代码。 对于需要 Cookie 的应用,请考虑执行以下任一操作:

  • 禁用自动 cookie 处理
  • 避免 IHttpClientFactory

调用 ConfigurePrimaryHttpMessageHandler 以禁用自动 cookie 处理:

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

Logging

通过 IHttpClientFactory 创建的客户端记录所有请求的日志消息。 在日志记录配置中启用合适的信息级别可以查看默认日志消息。 仅在跟踪级别包含附加日志记录(例如请求标头的日志记录)。

用于每个客户端的日志类别包含客户端名称。 例如,名为 MyNamedClient 的客户端记录类别为“System.Net.Http.HttpClient.MyNamedClient.LogicalHandler”的消息。 后缀为 LogicalHandler 的消息在请求处理程序管道外部发生。 在请求时,在管道中的任何其他处理程序处理请求之前记录消息。 在响应时,在任何其他管道处理程序接收响应之后记录消息。

日志记录还在请求处理程序管道内部发生。 在 MyNamedClient 示例中,这些消息的日志类别为“System.Net.Http.HttpClient.MyNamedClient.ClientHandler”。 在请求时,在所有其他处理程序运行后,以及刚好要发出请求之前记录消息。 在响应时,此日志记录包含响应在通过处理程序管道被传递回去之前的状态。

在管道内外启用日志记录,可以检查其他管道处理程序做出的更改。 这可能包含对请求标头的更改,或者对响应状态代码的更改。

通过在日志类别中包含客户端名称,可以对特定的命名客户端筛选日志。

配置 HttpMessageHandler

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

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

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

    // Remaining code deleted for brevity.

在控制台应用中使用 IHttpClientFactory

在控制台中,将以下包引用添加到项目中:

如下示例中:

  • IHttpClientFactory 已在泛型主机的服务容器中注册。
  • MyService 从服务创建客户端工厂实例,用于创建 HttpClientHttpClient 用于检索网页。
  • Main 可创建作用域来执行服务的 GetPage 方法,并将网页内容的前 500 个字符写入控制台。
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}";
            }
        }
    }
}

标头传播中间件

标头传播是一个 ASP.NET Core 中间件,可将 HTTP 标头从传入请求传播到传出 HTTP 客户端请求。 使用标头传播:

  • 引用 Microsoft.AspNetCore.HeaderPropagation 包。

  • Startup 中配置中间件和 HttpClient

    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();
        });
    }
    
  • 客户端在出站请求中包含配置的标头:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

其他资源

作者:Kirk LarkinSteve GordonGlenn CondronRyan Nowak

可以注册 IHttpClientFactory 并将其用于配置和创建应用中的 HttpClient 实例。 IHttpClientFactory 的优势如下:

  • 提供一个中心位置,用于命名和配置逻辑 HttpClient 实例。 例如,可注册和配置名为 github 的客户端,使其访问 GitHub。 可以注册一个默认客户端用于一般性访问。
  • 通过 HttpClient 中的委托处理程序来编码出站中间件的概念。 提供基于 Polly 的中间件的扩展,以利用 HttpClient 中的委托处理程序。
  • 管理基础 HttpClientMessageHandler 实例的池和生存期。 自动管理可避免手动管理 HttpClient 生存期时出现的常见 DNS(域名系统)问题。
  • (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。

查看或下载示例代码如何下载)。

此主题版本中的示例代码使用 System.Text.Json 来对 HTTP 响应中返回的 JSON 内容进行反序列化。 对于使用 Json.NETReadAsAsync<T> 的示例,请使用版本选择器选择此主题的 2.x 版本。

消耗模式

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

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

基本用法

可以通过调用 AddHttpClient 来注册 IHttpClientFactory

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.

可以使用依赖项注入 (DI) 来请求 IHttpClientFactory。 以下代码使用 IHttpClientFactory 来创建 HttpClient 实例:

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>();
        }
    }
}

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

命名客户端

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

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

可以在 Startup.ConfigureServices 中注册时指定命名 HttpClient 的配置:

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");
});

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

  • 基址为 https://api.github.com/
  • 使用 GitHub API 需要的两个标头。

CreateClient

每次调用 CreateClient 时:

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

要创建命名客户端,请将其名称传递到 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>();
        }
    }
}

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

类型化客户端

类型化客户端:

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

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

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()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

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

若要查看翻译为非英语语言的代码注释,请在 此 GitHub 讨论问题中告诉我们。

在上述代码中:

  • 配置转移到了类型化客户端中。
  • HttpClient 对象公开为公共属性。

可以创建特定于 API 的方法来公开 HttpClient 功能。 例如,创建 GetAspNetDocsIssues 方法来封装代码以检索未解决的问题。

以下代码调用 Startup.ConfigureServices 中的 AddHttpClient 来注册类型化客户端类:

services.AddHttpClient<GitHubService>();

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

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

可以直接插入或使用类型化客户端:

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>();
        }
    }
}

可以在 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 封装在类型化客户端中, 定义一个在内部调用 HttpClient 实例的方法,而不是将其公开为属性:

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);
    }
}

在上述代码中,HttpClient 存储在私有字段中。 通过公共 GetRepos 方法访问 HttpClient

生成的客户端

IHttpClientFactory 可结合第三方库(例如 Refit)使用。 Refit 是适用于 .NET 的 REST 库。 它将 REST API 转换为实时接口。 RestService 动态生成该接口的实现,使用 HttpClient 进行外部 HTTP 调用。

定义了接口和答复来代表外部 API 及其响应:

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

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

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

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();
}

可以在必要时使用定义的接口,以及由 DI 和 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();
    }
}

发出 POST、PUT 和 DELETE 请求

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

  • POST
  • PUT
  • 删除
  • PATCH

有关受支持的 HTTP 谓词的完整列表,请参阅 HttpMethod

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

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();
}

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

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

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

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

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();
}

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

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

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

    httpResponse.EnsureSuccessStatusCode();
}

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

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

出站请求中间件

HttpClient 具有委托处理程序的概念,这些委托处理程序可以链接在一起,处理出站 HTTP 请求。 IHttpClientFactory

  • 简化定义应用于各命名客户端的处理程序。
  • 支持注册和链接多个处理程序,以生成出站请求中间件管道。 每个处理程序都可以在出站请求前后执行工作。 此模式:
    • 类似于 ASP.NET Core 中的入站中间件管道。
    • 提供一种机制来管理有关 HTTP 请求的横切关注点,例如:
      • caching
      • 错误处理
      • 序列化
      • 日志记录

创建委托处理程序:

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);
    }
}

上述代码检查请求中是否存在 X-API-KEY 标头。 如果缺失 X-API-KEY,则返回 BadRequest

可使用 Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler 将多个处理程序添加到 HttpClient 的配置中:

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.

在上述代码中通过 DI 注册了 ValidateHeaderHandler。 注册后可以调用 AddHttpMessageHandler,传入标头的类型。

可以按处理程序应该执行的顺序注册多个处理程序。 每个处理程序都会覆盖下一个处理程序,直到最终 HttpClientHandler 执行请求:

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>();

在出站请求中间件中使用 DI

IHttpClientFactory 创建新的委托处理程序时,它使用 DI 来完成处理程序的构造函数参数。 IHttpClientFactory 为每个处理程序创建单独的 DI 范围,当处理程序使用限定范围的服务时,这可能导致意外的行为。

例如,请考虑下面的接口及其实现,它将任务表示为带有标识符 OperationId 的操作:

public interface IOperationScoped 
{
    string OperationId { get; }
}

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

顾名思义,使用限定范围的生存期向 DI 注册 IOperationScoped

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();
}

以下委托处理程序消耗并使用 IOperationScoped 来设置传出请求的 X-OPERATION-ID 标头:

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);
    }
}

HttpRequestsSample下载] 中,导航到 /Operation 并刷新页面。 每个请求的请求范围值发生更改,但处理程序范围值仅每 5 秒钟更改一次。

处理程序可依赖于任何作用域的服务。 处理程序依赖的服务会在处置处理程序时得到处置。

使用以下方法之一将每个请求状态与消息处理程序共享:

使用基于 Polly 的处理程序

IHttpClientFactory 与第三方库 Polly 集成。 Polly 是适用于 .NET 的全面恢复和临时故障处理库。 开发人员通过它可以表达策略,例如以流畅且线程安全的方式处理重试、断路器、超时、Bulkhead 隔离和回退。

提供了扩展方法,以实现将 Polly 策略用于配置的 HttpClient 实例。 Polly 扩展支持将基于 Polly 的处理程序添加到客户端。 Polly 需要 Microsoft.Extensions.Http.Polly NuGet 包。

处理临时故障

错误通常在暂时执行外部 HTTP 调用时发生。 AddTransientHttpErrorPolicy 允许定义一个策略来处理暂时性错误。 使用 AddTransientHttpErrorPolicy 配置的策略处理以下响应:

AddTransientHttpErrorPolicy 提供对 PolicyBuilder 对象的访问权限,该对象配置为处理表示可能的临时故障的错误:

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

    // Remaining code deleted for brevity.

上述代码中定义了 WaitAndRetryAsync 策略。 请求失败后最多可以重试三次,每次尝试间隔 600 ms。

动态选择策略

提供了扩展方法来添加基于 Polly 的处理程序,例如 AddPolicyHandler。 以下 AddPolicyHandler 重载检查请求以确定要应用的策略:

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);

在上述代码中,如果出站请求为 HTTP GET,则应用 10 秒超时。 其他所有 HTTP 方法应用 30 秒超时。

添加多个 Polly 处理程序

这对嵌套 Polly 策略很常见:

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

在上面的示例中:

  • 添加了两个处理程序。
  • 第一个处理程序使用 AddTransientHttpErrorPolicy 添加重试策略。 若请求失败,最多可重试三次。
  • 第二个 AddTransientHttpErrorPolicy 调用添加断路器策略。 如果尝试连续失败了 5 次,则会阻止后续外部请求 30 秒。 断路器策略处于监控状态。 通过此客户端进行的所有调用都共享同样的线路状态。

从 Polly 注册表添加策略

管理常用策略的一种方法是一次性定义它们并使用 PolicyRegistry 注册它们。

在以下代码中:

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.

有关 IHttpClientFactory 和 Polly 集成的详细信息,请参阅 Polly Wiki

HttpClient 和生存期管理

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

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

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

处理程序的默认生存期为两分钟。 可在每个命名客户端上重写默认值:

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

    // Remaining code deleted for brevity.

HttpClient 实例通常可视为无需处置的 .NET 对象。 处置既取消传出请求,又保证在调用 Dispose 后无法使用给定的 HttpClient 实例。 IHttpClientFactory 跟踪和处置 HttpClient 实例使用的资源。

保持各个 HttpClient 实例长时间处于活动状态是在 IHttpClientFactory 推出前使用的常见模式。 迁移到 IHttpClientFactory 后,就无需再使用此模式。

IHttpClientFactory 的替代项

通过在启用了 DI 的应用中使用 IHttpClientFactory,可避免:

  • 通过共用 HttpMessageHandler 实例,解决资源耗尽问题。
  • 通过定期循环 HttpMessageHandler 实例,解决 DNS 过时问题。

此外,还有其他方法使用生命周期长的 SocketsHttpHandler 实例来解决上述问题。

  • 在应用启动时创建 SocketsHttpHandler 的实例,并在应用的整个生命周期中使用它。
  • 根据 DNS 刷新时间,将 PooledConnectionLifetime 配置为适当的值。
  • 根据需要,使用 new HttpClient(handler, disposeHandler: false) 创建 HttpClient 实例。

上述方法使用 IHttpClientFactory 解决问题的类似方式解决资源管理问题。

  • SocketsHttpHandlerHttpClient 实例之间共享连接。 此共享可防止套接字耗尽。
  • SocketsHttpHandler 会根据 PooledConnectionLifetime 循环连接,避免出现 DNS 过时问题。

Cookie

共用 HttpMessageHandler 实例将导致共享 CookieContainer 对象。 意外的 CookieContainer 对象共享通常会导致错误的代码。 对于需要 Cookie 的应用,请考虑执行以下任一操作:

  • 禁用自动 cookie 处理
  • 避免 IHttpClientFactory

调用 ConfigurePrimaryHttpMessageHandler 以禁用自动 cookie 处理:

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

Logging

通过 IHttpClientFactory 创建的客户端记录所有请求的日志消息。 在日志记录配置中启用合适的信息级别可以查看默认日志消息。 仅在跟踪级别包含附加日志记录(例如请求标头的日志记录)。

用于每个客户端的日志类别包含客户端名称。 例如,名为 MyNamedClient 的客户端记录类别为“System.Net.Http.HttpClient.MyNamedClient.LogicalHandler”的消息。 后缀为 LogicalHandler 的消息在请求处理程序管道外部发生。 在请求时,在管道中的任何其他处理程序处理请求之前记录消息。 在响应时,在任何其他管道处理程序接收响应之后记录消息。

日志记录还在请求处理程序管道内部发生。 在 MyNamedClient 示例中,这些消息的日志类别为“System.Net.Http.HttpClient.MyNamedClient.ClientHandler”。 在请求时,在所有其他处理程序运行后,以及刚好要发出请求之前记录消息。 在响应时,此日志记录包含响应在通过处理程序管道被传递回去之前的状态。

在管道内外启用日志记录,可以检查其他管道处理程序做出的更改。 这可能包含对请求标头的更改,或者对响应状态代码的更改。

通过在日志类别中包含客户端名称,可以对特定的命名客户端筛选日志。

配置 HttpMessageHandler

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

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

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

    // Remaining code deleted for brevity.

在控制台应用中使用 IHttpClientFactory

在控制台中,将以下包引用添加到项目中:

如下示例中:

  • IHttpClientFactory 已在泛型主机的服务容器中注册。
  • MyService 从服务创建客户端工厂实例,用于创建 HttpClientHttpClient 用于检索网页。
  • Main 可创建作用域来执行服务的 GetPage 方法,并将网页内容的前 500 个字符写入控制台。
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}";
            }
        }
    }
}

标头传播中间件

标头传播是一个 ASP.NET Core 中间件,可将 HTTP 标头从传入请求传播到传出 HTTP 客户端请求。 使用标头传播:

  • 引用 Microsoft.AspNetCore.HeaderPropagation 包。

  • Startup 中配置中间件和 HttpClient

    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();
        });
    }
    
  • 客户端在出站请求中包含配置的标头:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

其他资源

作者:Glenn CondronRyan NowakSteve Gordon

可以注册 IHttpClientFactory 并将其用于配置和创建应用中的 HttpClient 实例。 这能带来以下好处:

  • 提供一个中心位置,用于命名和配置逻辑 HttpClient 实例。 例如,可注册和配置 github 客户端,使其访问 GitHub。 可以注册一个默认客户端用于其他用途。
  • 通过委托 HttpClient 中的处理程序整理出站中间件的概念,并提供适用于基于 Polly 的中间件的扩展来利用概念。
  • 管理基础 HttpClientMessageHandler 实例的池和生存期,避免在手动管理 HttpClient 生存期时出现常见的 DNS 问题。
  • (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。

查看或下载示例代码如何下载

先决条件

面向.NET Framework 的项目要求安装 Microsoft.Extensions.Http NuGet 包。 面向 .NET Core 且引用 Microsoft.AspNetCore.App 元包的项目已经包括 Microsoft.Extensions.Http 包。

消耗模式

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

它们之间不存在严格的优先级。 最佳方法取决于应用的约束条件。

基本用法

Startup.ConfigureServices 方法中,通过在 IServiceCollection 上调用 AddHttpClient 扩展方法可以注册 IHttpClientFactory

services.AddHttpClient();

注册后,在可以使用依赖关系注入 (DI) 注入服务的任何位置,代码都能接受 IHttpClientFactoryIHttpClientFactory 可用于创建 HttpClient 实例:

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)
        {
            Branches = await response.Content
                .ReadAsAsync<IEnumerable<GitHubBranch>>();
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }                               
    }
}

以这种方式使用 IHttpClientFactory 适合重构现有应用。 这不会影响 HttpClient 的使用方式。 在当前创建 HttpClient 实例的位置,使用对 CreateClient 的调用替换这些匹配项。

命名客户端

如果应用需要有许多不同的 HttpClient 用法(每种用法的配置都不同),可以视情况使用命名客户端。 可以在 HttpClient 中注册时指定命名 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");
});

上面的代码调用 AddHttpClient,同时提供名称“github”。 此客户端应用了一些默认配置,也就是需要基址和两个标头来使用 GitHub API。

每次调用 CreateClient 时,都会创建 HttpClient 的新实例,并调用配置操作。

要使用命名客户端,可将字符串参数传递到 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)
        {
            PullRequests = await response.Content
                .ReadAsAsync<IEnumerable<GitHubPullRequest>>();
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

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

类型化客户端

类型化客户端:

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

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

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()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<GitHubIssue>>();

        return result;
    }
}

在上述代码中,配置转移到了类型化客户端中。 HttpClient 对象公开为公共属性。 可以定义公开 HttpClient 功能的特定于 API 的方法。 GetAspNetDocsIssues 方法从 GitHub 存储库封装查询和分析最新待解决问题所需的代码。

要注册类型化客户端,可在 Startup.ConfigureServices 中使用通用的 AddHttpClient 扩展方法,指定类型化客户端类:

services.AddHttpClient<GitHubService>();

使用 DI 将类型客户端注册为暂时客户端。 可以直接插入或使用类型化客户端:

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>();
        }
    }
}

根据你的喜好,可以在 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 完全封装在类型化客户端中。 不是将它公开为属性,而是可以提供公共方法,用于在内部调用 HttpClient

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();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<string>>();

        return result;
    }
}

在上述代码中,HttpClient 存储未私有字段。 进行外部调用的所有访问都经由 GetRepos 方法。

生成的客户端

IHttpClientFactory 可结合其他第三方库(例如 Refit)使用。 Refit 是适用于 .NET 的 REST 库。 它将 REST API 转换为实时接口。 RestService 动态生成该接口的实现,使用 HttpClient 进行外部 HTTP 调用。

定义了接口和答复来代表外部 API 及其响应:

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

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

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

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

    services.AddMvc();
}

可以在必要时使用定义的接口,以及由 DI 和 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();
    }
}

出站请求中间件

HttpClient 已经具有委托处理程序的概念,这些委托处理程序可以链接在一起,处理出站 HTTP 请求。 IHttpClientFactory 可以轻松定义处理程序并应用于每个命名客户端。 它支持注册和链接多个处理程序,以生成出站请求中间件管道。 每个处理程序都可以在出站请求前后执行工作。 此模式类似于 ASP.NET Core 中的入站中间件管道。 此模式提供了一种用于管理围绕 HTTP 请求的横切关注点的机制,包括缓存、错误处理、序列化以及日志记录。

要创建处理程序,请定义一个派生自 DelegatingHandler 的类。 重写 SendAsync 方法,在将请求传递至管道中的下一个处理程序之前执行代码:

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);
    }
}

上述代码定义了基本处理程序。 它检查请求中是否包含 X-API-KEY 头。 如果标头缺失,它可以避免 HTTP 调用,并返回合适的响应。

在注册期间可将一个或多个标头添加到 HttpClient 的配置中。 此任务通过 IHttpClientBuilder 上的扩展方法完成。

services.AddTransient<ValidateHeaderHandler>();

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

在上述代码中通过 DI 注册了 ValidateHeaderHandler。 处理程序必须在 DI 中注册为暂时性服务且从不设置作用域。 如果该处理程序注册为作用域服务,并且处理程序依赖的任何服务都是可释放的:

  • 处理程序的服务可以在处理程序超出作用域之前被释放。
  • 已释放的处理程序服务可导致处理程序失败。

注册后,可以调用 AddHttpMessageHandler,传入处理程序类型。

可以按处理程序应该执行的顺序注册多个处理程序。 每个处理程序都会覆盖下一个处理程序,直到最终 HttpClientHandler 执行请求:

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>();

使用以下方法之一将每个请求状态与消息处理程序共享:

  • 使用 HttpRequestMessage.Properties 将数据传递到处理程序。
  • 使用 IHttpContextAccessor 访问当前请求。
  • 创建自定义 AsyncLocal 存储对象以传递数据。

使用基于 Polly 的处理程序

IHttpClientFactory 与一个名为 Polly 的热门第三方库集成。 Polly 是适用于 .NET 的全面恢复和临时故障处理库。 开发人员通过它可以表达策略,例如以流畅且线程安全的方式处理重试、断路器、超时、Bulkhead 隔离和回退。

提供了扩展方法,以实现将 Polly 策略用于配置的 HttpClient 实例。 Polly 扩展:

  • 支持将基于 Polly 的处理程序添加到客户端。
  • 安装了 Microsoft.Extensions.Http.Polly NuGet 包后可使用该扩展。 ASP.NET Core 共享框架中不包括该包。

处理临时故障

大多数常见错误在暂时执行外部 HTTP 调用时发生。 包含了一种简便的扩展方法,该方法名为 AddTransientHttpErrorPolicy,允许定义策略来处理临时故障。 使用这种扩展方法配置的策略可以处理 HttpRequestException、HTTP 5xx 响应以及 HTTP 408 响应。

AddTransientHttpErrorPolicy 扩展可在 Startup.ConfigureServices 内使用。 该扩展可以提供 PolicyBuilder 对象的访问权限,该对象配置为处理表示可能的临时故障的错误:

services.AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

上述代码中定义了 WaitAndRetryAsync 策略。 请求失败后最多可以重试三次,每次尝试间隔 600 ms。

动态选择策略

存在其他扩展方法,可以用于添加基于 Polly 的处理程序。 这类扩展的其中一个是 AddPolicyHandler,它具备多个重载。 一个重载允许在定义要应用的策略时检查该请求:

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);

在上述代码中,如果出站请求为 HTTP GET,则应用 10 秒超时。 其他所有 HTTP 方法应用 30 秒超时。

添加多个 Polly 处理程序

通过嵌套 Polly 策略来增强功能是很常见的:

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

在上述示例中,添加两个处理程序。 第一个使用 AddTransientHttpErrorPolicy 扩展添加重试策略。 若请求失败,最多可重试三次。 第二个调用 AddTransientHttpErrorPolicy 添加断路器策略。 如果尝试连续失败了五次,则会阻止后续外部请求 30 秒。 断路器策略处于监控状态。 通过此客户端进行的所有调用都共享同样的线路状态。

从 Polly 注册表添加策略

管理常用策略的一种方法是一次性定义它们并使用 PolicyRegistry 注册它们。 提供了一种扩展方法,可以使用注册表中的策略添加处理程序:

var registry = services.AddPolicyRegistry();

registry.Add("regular", timeout);
registry.Add("long", longTimeout);

services.AddHttpClient("regulartimeouthandler")
    .AddPolicyHandlerFromRegistry("regular");

在上面的代码中,两个策略在 PolicyRegistry 添加到 ServiceCollection 中时进行注册。 若要使用注册表中的策略,请使用 AddPolicyHandlerFromRegistry 方法,同时传递要应用的策略的名称。

要进一步了解 IHttpClientFactory 和 Polly 集成,请参考 Polly Wiki

HttpClient 和生存期管理

每次对 IHttpClientFactory 调用 CreateClient 都会返回一个新 HttpClient 实例。 每个命名的客户端都具有一个 HttpMessageHandler。 工厂管理 HttpMessageHandler 实例的生存期。

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

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

处理程序的默认生存期为两分钟。 可在每个命名客户端上重写默认值。 要重写该值,请在创建客户端时在返回的 IHttpClientBuilder 上调用 SetHandlerLifetime

services.AddHttpClient("extendedhandlerlifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

无需处置客户端。 处置既取消传出请求,又保证在调用 Dispose 后无法使用给定的 HttpClient 实例。 IHttpClientFactory 跟踪和处置 HttpClient 实例使用的资源。 HttpClient 实例通常可视为无需处置的 .NET 对象。

保持各个 HttpClient 实例长时间处于活动状态是在 IHttpClientFactory 推出前使用的常见模式。 迁移到 IHttpClientFactory 后,就无需再使用此模式。

IHttpClientFactory 的替代项

通过在启用了 DI 的应用中使用 IHttpClientFactory,可避免:

  • 通过共用 HttpMessageHandler 实例,解决资源耗尽问题。
  • 通过定期循环 HttpMessageHandler 实例,解决 DNS 过时问题。

此外,还有其他方法使用生命周期长的 SocketsHttpHandler 实例来解决上述问题。

  • 在应用启动时创建 SocketsHttpHandler 的实例,并在应用的整个生命周期中使用它。
  • 根据 DNS 刷新时间,将 PooledConnectionLifetime 配置为适当的值。
  • 根据需要,使用 new HttpClient(handler, disposeHandler: false) 创建 HttpClient 实例。

上述方法使用 IHttpClientFactory 解决问题的类似方式解决资源管理问题。

  • SocketsHttpHandlerHttpClient 实例之间共享连接。 此共享可防止套接字耗尽。
  • SocketsHttpHandler 会根据 PooledConnectionLifetime 循环连接,避免出现 DNS 过时问题。

Cookie

共用 HttpMessageHandler 实例将导致共享 CookieContainer 对象。 意外的 CookieContainer 对象共享通常会导致错误的代码。 对于需要 Cookie 的应用,请考虑执行以下任一操作:

  • 禁用自动 cookie 处理
  • 避免 IHttpClientFactory

调用 ConfigurePrimaryHttpMessageHandler 以禁用自动 cookie 处理:

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

Logging

通过 IHttpClientFactory 创建的客户端记录所有请求的日志消息。 在日志记录配置中启用合适的信息级别可以查看默认日志消息。 仅在跟踪级别包含附加日志记录(例如请求标头的日志记录)。

用于每个客户端的日志类别包含客户端名称。 例如,名为“MyNamedClient”的客户端使用 System.Net.Http.HttpClient.MyNamedClient.LogicalHandler 类别来记录消息。 后缀为 LogicalHandler 的消息在请求处理程序管道外部发生。 在请求时,在管道中的任何其他处理程序处理请求之前记录消息。 在响应时,在任何其他管道处理程序接收响应之后记录消息。

日志记录还在请求处理程序管道内部发生。 在“MyNamedClient”示例中,这些消息是针对日志类别 System.Net.Http.HttpClient.MyNamedClient.ClientHandler 进行记录。 在请求时,在所有其他处理程序运行后,以及刚好在通过网络发出请求之前记录消息。 在响应时,此日志记录包含响应在通过处理程序管道被传递回去之前的状态。

在管道内外启用日志记录,可以检查其他管道处理程序做出的更改。 例如,其中可能包含对请求标头的更改,或者对响应状态代码的更改。

通过在日志类别中包含客户端名称,可以在必要时对特定的命名客户端筛选日志。

配置 HttpMessageHandler

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

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

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

在控制台应用中使用 IHttpClientFactory

在控制台中,将以下包引用添加到项目中:

如下示例中:

  • IHttpClientFactory 已在泛型主机的服务容器中注册。
  • MyService 从服务创建客户端工厂实例,用于创建 HttpClientHttpClient 用于检索网页。
  • 执行服务的 GetPage 方法以将网页内容的前 500 个字符写入控制台。 有关从 Program.Main 调用服务的详细信息,请参阅 ASP.NET Core 中的依赖关系注入
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}";
            }
        }
    }
}

标头传播中间件

标头传播是一个社区支持的中间件,可将 HTTP 标头从传入请求传播到传出 HTTP 客户端请求。 使用标头传播:

  • 引用 HeaderPropagation 包的社区支持的端口。 ASP.NET Core 3.1 及更高版本支持 Microsoft.AspNetCore.HeaderPropagation

  • Startup 中配置中间件和 HttpClient

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseMvc();
    }
    
  • 客户端在出站请求中包含配置的标头:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

其他资源