可以注冊 IHttpClientFactory 并將其用于配置和創(chuàng)建應(yīng)用中的 HttpClient 實例。 這能帶來以下好處:
面向.NET Framework 的項目要求安裝 Microsoft.Extensions.Http NuGet 包。 面向 .NET Core 且引用 Microsoft.AspNetCore.App 元包的項目已經(jīng)包括 Microsoft.Extensions.Http 包。
在應(yīng)用中可以通過以下多種方式使用 IHttpClientFactory:
它們之間不存在嚴(yán)格的優(yōu)先級。 最佳方法取決于應(yīng)用的約束條件。
在 Startup.ConfigureServices 方法中,通過在 IServiceCollection 上調(diào)用 AddHttpClient 擴(kuò)展方法可以注冊 IHttpClientFactory。
C#
services.AddHttpClient();
注冊后,在可以使用依賴關(guān)系注入 (DI) 注入服務(wù)的任何位置,代碼都能接受 IHttpClientFactory。 IHttpClientFactory 可以用于創(chuàng)建 HttpClient 實例:
C#
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/aspnet/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 適合重構(gòu)現(xiàn)有應(yīng)用。 這不會影響 HttpClient 的使用方式。 在當(dāng)前創(chuàng)建 HttpClient 實例的位置,使用對 CreateClient 的調(diào)用替換這些匹配項。
如果應(yīng)用需要有許多不同的 HttpClient 用法(每種用法的配置都不同),可以視情況使用命名客戶端。 可以在 HttpClient 中注冊時指定命名 Startup.ConfigureServices 的配置。
C#
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");
});
上面的代碼調(diào)用 AddHttpClient,同時提供名稱“github”。 此客戶端應(yīng)用了一些默認(rèn)配置,也就是需要基址和兩個標(biāo)頭來使用 GitHub API。
每次調(diào)用 CreateClient 時,都會創(chuàng)建 HttpClient 的新實例,并調(diào)用配置操作。
要使用命名客戶端,可將字符串參數(shù)傳遞到 CreateClient。 指定要創(chuàng)建的客戶端的名稱:
C#
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/aspnet/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>();
}
}
}
在上述代碼中,請求不需要指定主機(jī)名。 可以僅傳遞路徑,因為采用了為客戶端配置的基址。
類型化客戶端提供與命名客戶端一樣的功能,不需要將字符串用作密鑰。 類型化客戶端方法在使用客戶端時提供 IntelliSense 和編譯器幫助。 它們提供單個地址來配置特定 HttpClient 并與其進(jìn)行交互。 例如,單個類型化客戶端可能用于單個后端終結(jié)點,并封裝此終結(jié)點的所有處理邏輯。 另一個優(yōu)勢是它們使用 DI 且可以被注入到應(yīng)用中需要的位置。
類型化客戶端在構(gòu)造函數(shù)中接收 HttpClient 參數(shù):
C#
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/aspnet/docs/issues?state=open&sort=created&direction=desc");
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadAsAsync<IEnumerable<GitHubIssue>>();
return result;
}
}
在上述代碼中,配置轉(zhuǎn)移到了類型化客戶端中。 HttpClient 對象公開為公共屬性。 可以定義公開 HttpClient 功能的特定于 API 的方法。 GetAspNetDocsIssues 方法從 GitHub 存儲庫封裝查詢和分析最新待解決問題所需的代碼。
要注冊類型化客戶端,可在 Startup.ConfigureServices 中使用通用的 AddHttpClient 擴(kuò)展方法,指定類型化客戶端類:
C#
services.AddHttpClient<GitHubService>();
使用 DI 將類型客戶端注冊為暫時客戶端。 可以直接插入或使用類型化客戶端:
C#
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>();
}
}
}
根據(jù)你的喜好,可以在 Startup.ConfigureServices 中注冊時指定類型化客戶端的配置,而不是在類型化客戶端的構(gòu)造函數(shù)中指定:
C#
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 完全封裝在類型化客戶端中。 不是將它公開為屬性,而是可以提供公共方法,用于在內(nèi)部調(diào)用 HttpClient。
C#
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 存儲未私有字段。 進(jìn)行外部調(diào)用的所有訪問都經(jīng)由 GetRepos 方法。
IHttpClientFactory 可結(jié)合其他第三方庫(例如 Refit)使用。 Refit 是.NET 的 REST 庫。 它將 REST API 轉(zhuǎn)換為實時接口。 RestService 動態(tài)生成該接口的實現(xiàn),使用 HttpClient 進(jìn)行外部 HTTP 調(diào)用。
定義了接口和答復(fù)來代表外部 API 及其響應(yīng):
C#
public interface IHelloClient
{
[Get("/helloworld")]
Task<Reply> GetMessageAsync();
}
public class Reply
{
public string Message { get; set; }
}
可以添加類型化客戶端,使用 Refit 生成實現(xiàn):
C#
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 提供的實現(xiàn):
C#
[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 已經(jīng)具有委托處理程序的概念,這些委托處理程序可以鏈接在一起,處理出站 HTTP 請求。 IHttpClientFactory 可以輕松定義處理程序并應(yīng)用于每個命名客戶端。 它支持注冊和鏈接多個處理程序,以生成出站請求中間件管道。 每個處理程序都可以在出站請求前后執(zhí)行工作。 此模式類似于 ASP.NET Core 中的入站中間件管道。 此模式提供了一種用于管理圍繞 HTTP 請求的橫切關(guān)注點的機(jī)制,包括緩存、錯誤處理、序列化以及日志記錄。
要創(chuàng)建處理程序,請定義一個派生自 DelegatingHandler 的類。 重寫 SendAsync 方法,在將請求傳遞至管道中的下一個處理程序之前執(zhí)行代碼:
C#
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 頭。 如果標(biāo)頭缺失,它可以避免 HTTP 調(diào)用,并返回合適的響應(yīng)。
在注冊期間可將一個或多個標(biāo)頭添加到 HttpClient 的配置。 此任務(wù)通過 IHttpClientBuilder 上的擴(kuò)展方法完成。
C#
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。 IHttpClientFactory 為每個處理程序創(chuàng)建單獨的 DI 作用域。 處理程序可依賴于任何作用域的服務(wù)。 處理程序依賴的服務(wù)會在處置處理程序時得到處置。
注冊后可以調(diào)用 AddHttpMessageHandler,傳入標(biāo)頭的類型。
可以按處理程序應(yīng)該執(zhí)行的順序注冊多個處理程序。 每個處理程序都會覆蓋下一個處理程序,直到最終 HttpClientHandler 執(zhí)行請求:
C#
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>();
使用以下方法之一將每個請求狀態(tài)與消息處理程序共享:
IHttpClientFactory 與一個名為 Polly 的熱門第三方庫集成。 Polly 是適用于 .NET 的全面恢復(fù)和臨時故障處理庫。 開發(fā)人員通過它可以表達(dá)策略,例如以流暢且線程安全的方式處理重試、斷路器、超時、Bulkhead 隔離和回退。
提供了擴(kuò)展方法,以實現(xiàn)將 Polly 策略用于配置的 HttpClient 實例。Microsoft.Extensions.Http.Polly NuGet 包中提供 Polly 擴(kuò)展。 Microsoft.AspNetCore.App 元包中不包括此包。 若要使用擴(kuò)展,項目中應(yīng)該包括顯式 <PackageReference />。
C#
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="2.2.0" />
</ItemGroup>
</Project>
還原此包后,可以使用擴(kuò)展方法來支持將基于 Polly 的處理程序添加至客戶端。
大多數(shù)常見錯誤在暫時執(zhí)行外部 HTTP 調(diào)用時發(fā)生。 包含了一種簡便的擴(kuò)展方法,該方法名為 AddTransientHttpErrorPolicy,允許定義策略來處理臨時故障。 使用這種擴(kuò)展方法配置的策略可以處理 HttpRequestException、HTTP 5xx 響應(yīng)以及 HTTP 408 響應(yīng)。
AddTransientHttpErrorPolicy 擴(kuò)展可在 Startup.ConfigureServices 內(nèi)使用。 該擴(kuò)展可以提供 PolicyBuilder 對象的訪問權(quán)限,該對象配置為處理表示可能的臨時故障的錯誤:
C#
services.AddHttpClient<UnreliableEndpointCallerService>()
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));
上述代碼中定義了 WaitAndRetryAsync 策略。 請求失敗后最多可以重試三次,每次嘗試間隔 600 ms。
存在其他擴(kuò)展方法,可以用于添加基于 Polly 的處理程序。 這類擴(kuò)展的其中一個是 AddPolicyHandler,它具備多個重載。 一個重載允許在定義要應(yīng)用的策略時檢查該請求:
C#
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);
在上述代碼中,如果出站請求為 GET,則應(yīng)用 10 秒超時。 其他所有 HTTP 方法應(yīng)用 30 秒超時。
嵌套 Polly 策略以增強(qiáng)功能是很常見的:
C#
services.AddHttpClient("multiplepolicies")
.AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
.AddTransientHttpErrorPolicy(
p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
在上述示例中,添加兩個處理程序。 第一個使用 AddTransientHttpErrorPolicy 擴(kuò)展添加重試策略。若請求失敗,最多可重試三次。 第二個調(diào)用 AddTransientHttpErrorPolicy 添加斷路器策略。 如果嘗試連續(xù)失敗了五次,則會阻止后續(xù)外部請求 30 秒。 斷路器策略處于監(jiān)控狀態(tài)。 通過此客戶端進(jìn)行的所有調(diào)用都共享同樣的線路狀態(tài)。
管理常用策略的一種方法是一次性定義它們并使用 PolicyRegistry 注冊它們。 提供了一種擴(kuò)展方法,可以使用注冊表中的策略添加處理程序:
C#
var registry = services.AddPolicyRegistry();
registry.Add("regular", timeout);
registry.Add("long", longTimeout);
services.AddHttpClient("regulartimeouthandler")
.AddPolicyHandlerFromRegistry("regular");
在上面的代碼中,兩個策略在 PolicyRegistry 添加到 ServiceCollection 中時進(jìn)行注冊。 若要使用注冊表中的策略,請使用 AddPolicyHandlerFromRegistry 方法,同時傳遞要應(yīng)用的策略的名稱。
要進(jìn)一步了解 IHttpClientFactory 和 Polly 集成,請參考 Polly Wiki。
每次對 IHttpClientFactory 調(diào)用 CreateClient 都會返回一個新 HttpClient 實例。 每個命名的客戶端都具有一個 HttpMessageHandler。 工廠管理 HttpMessageHandler 實例的生存期。
IHttpClientFactory 將工廠創(chuàng)建的 HttpMessageHandler 實例匯集到池中,以減少資源消耗。 新建 HttpClient 實例時,可能會重用池中的 HttpMessageHandler 實例(如果生存期尚未到期的話)。
由于每個處理程序通常管理自己的基礎(chǔ) HTTP 連接,因此需要池化處理程序。 創(chuàng)建超出必要數(shù)量的處理程序可能會導(dǎo)致連接延遲。 部分處理程序還保持連接無期限地打開,這樣可以防止處理程序?qū)?DNS 更改作出反應(yīng)。
處理程序的默認(rèn)生存期為兩分鐘。 可在每個命名客戶端上重寫默認(rèn)值。 要重寫該值,請在創(chuàng)建客戶端時在返回的 IHttpClientBuilder 上調(diào)用 SetHandlerLifetime:
C#
services.AddHttpClient("extendedhandlerlifetime")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
無需處置客戶端。 處置既取消傳出請求,又保證在調(diào)用 Dispose 后無法使用給定的 HttpClient 實例。 IHttpClientFactory 跟蹤和處置 HttpClient 實例使用的資源。 HttpClient 實例通??梢暈闊o需處置的 .NET 對象。
保持各個 HttpClient 實例長時間處于活動狀態(tài)是在 IHttpClientFactory 推出前使用的常見模式。遷移到 IHttpClientFactory 后,就無需再使用此模式。
通過 IHttpClientFactory 創(chuàng)建的客戶端記錄所有請求的日志消息。 在日志記錄配置中啟用合適的信息級別可以查看默認(rèn)日志消息。 僅在跟蹤級別包含附加日志記錄(例如請求標(biāo)頭的日志記錄)。
用于每個客戶端的日志類別包含客戶端名稱。 例如,名為“MyNamedClient”的客戶端使用 System.Net.Http.HttpClient.MyNamedClient.LogicalHandler 類別來記錄消息。 后綴為 LogicalHandler 的消息在請求處理程序管道外部發(fā)生。 在請求時,在管道中的任何其他處理程序處理請求之前記錄消息。 在響應(yīng)時,在任何其他管道處理程序接收響應(yīng)之后記錄消息。
日志記錄還在請求處理程序管道內(nèi)部發(fā)生。 在“MyNamedClient”示例中,這些消息是針對日志類別 System.Net.Http.HttpClient.MyNamedClient.ClientHandler 進(jìn)行記錄。 在請求時,在所有其他處理程序運行后,以及剛好在通過網(wǎng)絡(luò)發(fā)出請求之前記錄消息。 在響應(yīng)時,此日志記錄包含響應(yīng)在通過處理程序管道被傳遞回去之前的狀態(tài)。
在管道內(nèi)外啟用日志記錄,可以檢查其他管道處理程序做出的更改。 例如,其中可能包含對請求標(biāo)頭的更改,或者對響應(yīng)狀態(tài)代碼的更改。
通過在日志類別中包含客戶端名稱,可以在必要時對特定的命名客戶端篩選日志。
控制客戶端使用的內(nèi)部 HttpMessageHandler 的配置是有必要的。
在添加命名客戶端或類型化客戶端時,會返回 IHttpClientBuilder。ConfigurePrimaryHttpMessageHandler 擴(kuò)展方法可以用于定義委托。 委托用于創(chuàng)建和配置客戶端使用的主要 HttpMessageHandler:
C#
services.AddHttpClient("configured-inner-handler")
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler()
{
AllowAutoRedirect = false,
UseDefaultCredentials = true
};
});
更多建議: