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

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

消耗模式

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

  • 基本用法
  • 命名客户端
  • 类型化客户端
  • 生成的客户端

基本用法

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

1
2
3
4
var builder = WebApplication.CreateBuilder(args);

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 的配置:

1
2
3
4
5
6
7
8
9
10
11
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");
});

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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");
}

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

1
builder.Services.AddHttpClient<GitHubService>();

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 中注册时指定类型化客户端的配置,而不是在类型化客户端的构造函数中指定:

1
2
3
4
5
6
builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");

// ...
});

生成的客户端

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

  自定义接口表示外部 API:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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 的动态实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
{
// ...
}
}
}

发出请求

GET

1
2
3
4
5
6
7
public async Task GetTodoItemsAsync()
{
using var httpResponseMessage =
await _httpClient.GetFromJsonAsync<IEnumerable<TodoItem>>("/api/TodoItems");

httpResponseMessage.EnsureSuccessStatusCode();
}

POST

1
2
3
4
5
6
7
8
9
10
11
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);

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

PUT

1
2
3
4
5
6
7
8
9
10
11
12
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。

DELETE

1
2
3
4
5
6
7
public async Task DeleteItemAsync(long itemId)
{
using var httpResponseMessage =
await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

httpResponseMessage.EnsureSuccessStatusCode();
}

   由于 HTTP DELETE 请求通常不包含正文,因此 DeleteAsync 方法不提供接受 HttpContent 实例的重载。

出站请求中间件

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

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

创建委托处理程序

  • 派生自DelegatingHandler
  • 重写 SendAsync。 在将请求传递至管道中的下一个处理程序之前执行代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 的配置中:

1
2
3
4
builder.Services.AddTransient<ValidateHeaderHandler>();

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

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

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

1
2
3
4
5
6
builder.Services.AddTransient<SampleHandler1>();
builder.Services.AddTransient<SampleHandler2>();

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

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

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

1
2
3
4
5
6
7
8
9
public interface IOperationScoped
{
string OperationId { get; }
}

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}
}

  处理程序可依赖于任何作用域的服务。 处理程序依赖的服务会在处置处理程序时得到处置。使用以下方法之一将每个请求状态与消息处理程序共享:

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

使用基于Polly的处理程序

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

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

标头传播中间件

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

  1. 安装 Microsoft.AspNetCore.HeaderPropagation 包。
  2. 在 Program.cs 中配置 HttpClient 和中间件管道:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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();