Note
這不是這篇文章的最新版本。 關於目前版本,請參閱本文的 .NET 10 版本。
Warning
此版本的 ASP.NET Core 已不再支援。 欲了解更多資訊,請參閱.NET及.NET核心支援政策。 關於目前版本,請參閱本文的 .NET 10 版本。
由 Kirk Larkin、Steve Gordon、Glenn Condron 和 Ryan Nowak 撰寫。
你可以註冊 IHttpClientFactory,並用它來設定和建立應用程式中的 HttpClient 實例。 介面 IHttpClientFactory 提供以下優點:
提供一個集中位置,用於命名和設定邏輯
HttpClient執行個體。 例如,一個名為 github 的用戶端可以註冊並設定存取 GitHub。 預設用戶端可以註冊用於一般存取。透過委派處理器,
HttpClient實例規範化了外部中介軟體的概念。 提供適用於 Polly 型中介軟體的延伸模組,以利用HttpClient中的委派處理常式。管理基礎
HttpClientMessageHandler執行個體的共用和存留期。 自動管理可避免在手動管理HttpClient存留期時,所發生的常見網域名稱系統 (DNS) 問題。新增可設定的日誌機制(透過
ILogger),適用於工廠建立的所有客戶端之請求。
本文說明如何在 ASP.NET Core應用程式中使用 IHttpClientFactory 來發送 HTTP 請求。 範例程式碼版本用於 System.Text.Json 反序列化 HTTP 回應中回傳的 JSON 內容。 對於使用 Json.NET 和 ReadAsAsync<T> 的範例,請使用瀏覽器的 Version 選擇器選擇本條目的 2.x 版本。
檢視消費模式
有數種方式可將 IHttpClientFactory 用於應用程式:
最好的方法取決於應用程式的需求。
基本用法
在
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddHttpClient();
IHttpClientFactory可以使用相依性注入(DI)進行請求。 下列程式碼會使用 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);
}
}
}
在範例中,呼叫 to IHttpClientFactory 是重構現有應用程式的好方法。 這種做法不會影響使用方式 HttpClient 。
在現有 HttpClient 應用程式中建立實例的情況,請將這些實例替換為呼叫 CreateClient。
指定用戶端
命名用戶端適用於以下情境:
- 應用程式需要許多不同的
HttpClient用法。 - 有許多不同配置的
HttpClient實例。
在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實例會儲存為私人欄位。
可以建立公開 HttpClient 功能的 API 特定方法。 例如,GetAspNetCoreDocsBranches 方法封裝程式碼,用來從GitHub分支檢索文檔。
以下程式碼在 AddHttpClient 檔案中呼叫以註冊GitHubService型別的客戶端類別:
builder.Services.AddHttpClient<GitHubService>();
具類型化的客戶端會在依賴注入(DI)中註冊為瞬態。 在程式碼片段中,該 AddHttpClient 方法被 GitHubService 註冊為暫態服務。 此註冊會使用工廠方法來:
- 建立
HttpClient的執行個體。 - 建立一個 的
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
- DELETE
- 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.Json將TodoItem參數序列化為 JSON 格式。 - 建立一個 StringContent 實例,將序列化的 JSON 打包進 HTTP 請求主體。
- 呼叫 PostAsync 來將 JSON 內容傳送到指定的網址。 這個值是相對的 URL,將被添加到 HttpClient.BaseAddress。
- 呼叫 EnsureSuccessStatusCode 引發例外,以便在回應狀態碼不顯示成功時進行處理。
HttpClient 也支援其他型別的內容。 例如,MultipartContent 與 StreamContent。 如需完整的支援內容清單,請參閱 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 執行個體的多載。
若要深入了解搭配 HttpClient 使用不同的 HTTP 動詞,請參閱 HttpClient。
使用外部請求中介軟體
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(
"The API key header X-API-KEY is required.")
};
}
return await base.SendAsync(request, cancellationToken);
}
}
程式碼片段會檢查請求中是否包含 X-API-KEY 標頭。 若 X-API-KEY 缺少,代碼會回傳狀態 BadRequest 碼。
可以透過AddHttpMessageHandler在一個HttpClient實例的配置中新增多個處理器:
builder.Services.AddTransient<ValidateHeaderHandler>();
builder.Services.AddHttpClient("HttpMessageHandler")
.AddHttpMessageHandler<ValidateHeaderHandler>();
在程式碼片段中,該位址 ValidateHeaderHandler 已登記在 DI 系統。 註冊後,可以呼叫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..];
}
顧名思義, IOperationScoped 透過使用 有範圍 的壽命向 DI 註冊:
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 秒變更一次。
處理常式可以依賴任何範疇的服務。 當處理器被處置時,其相依的服務也會被處置。
使用下列其中一種方式來與訊息處理常式共用每個請求的狀態:
- 請使用 Options 將資料傳送到處理器。
- 請使用 IHttpContextAccessor 來存取當前的請求實例。
- 建立一個自訂 AsyncLocal<T> 的儲存物件來傳遞資料。
使用 Polly 為基礎的處理常式
介面 IHttpClientFactory 可整合第三方函式庫 Polly。 Polly 是一個完整的 .NET 韌性與瞬態故障處理函式庫。 它可讓開發人員以流暢且執行緒安全的方式表達策略,例如重試、斷路器、超時、艙壁隔離和回退。
提供擴充方法以便使用 Polly 原則與已設定的 HttpClient 執行個體。 Polly 延伸模組支援將以 Polly 為基礎的處理常式新增至用戶端。 Polly 需要Microsoft.Extensions.Http.Polly NuGet 套件。
處理暫時性錯誤
錯誤通常發生在外部 HTTP 呼叫是暫時性的時候。
AddTransientHttpErrorPolicy 允許定義原則來處理暫時性錯誤。 以該 AddTransientHttpErrorPolicy 方法配置的政策會處理以下回應:
- HttpRequestException
- HTTP 5xx
- HTTP 408
AddTransientHttpErrorPolicy提供對PolicyBuilder物件的存取權限,該物件已設定為處理代表可能暫時性故障的錯誤。
builder.Services.AddHttpClient("PollyWaitAndRetry")
.AddTransientHttpErrorPolicy(policyBuilder =>
policyBuilder.WaitAndRetryAsync(
3, retryNumber => TimeSpan.FromMilliseconds(600)));
在程式碼片段中,會定義一個 WaitAndRetryAsync 政策。 失敗的要求會重試最多三次,並且在嘗試之間會有 600 毫秒的延遲時間。
動態選取策略
提供擴充方法以加入基於 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呼叫會新增斷路器政策。 如果循序發生五次失敗的嘗試,進一步的外部要求會遭到封鎖 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");
程式碼片段為 Polly 註冊表新增兩個策略,Regular 和 Long。 程式碼呼叫AddPolicyHandlerFromRegistry來配置個別命名的客戶端,以使用這些來自 Polly 註冊表的政策。
如需 IHttpClientFactory 和 Polly 整合的詳細資訊,請參閱 Polly wiki。
HttpClient 和存留期管理
每次在 HttpClient 上呼叫 CreateClient,都會返回一個新的 IHttpClientFactory 實例。 為每個指定的客戶建立一個 HttpMessageHandler。 工廠負責管理 HttpMessageHandler 執行個體的存留期。
IHttpClientFactory 會將處理站所建立的 HttpMessageHandler 執行個體放入集區以減少資源耗用量。 如果HttpMessageHandler實例的壽命尚未過期,那麼在建立新HttpClient實例時,該實例可能會從池中重新使用。
將處理常式放入集區非常實用,因為處理常式通常會管理自己專屬的底層 HTTP 連線。 建立比所需數目更多的處理常式,可能會導致連線延遲。 有些處理常式也會保持連線無限期地開啟,這可能導致處理常式無法對 DNS 變更回應。
預設的處理器壽命是 2 分鐘。 預設值可依每個命名的用戶端來覆寫:
builder.Services.AddHttpClient("HandlerLifetime")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
HttpClient 實例通常可視為.NET物件,不需要處理。 處置會取消傳出的要求,並保證指定的 HttpClient 執行個體在呼叫 Dispose 之後無法使用。
IHttpClientFactory 會追蹤並處置 HttpClient 執行個體使用的資源。
在開始使用 HttpClient 之前,維持單一 IHttpClientFactory 執行個體長時間存續是一種常見模式。 在移轉到 IHttpClientFactory 之後,就不再需要此模式。
探索 IHttpClientFactory 的替代方案
在已啟用 DI 的應用程式中使用 IHttpClientFactory 可避免:
- 藉由集結
HttpMessageHandler實例來解決資源耗盡的問題。 - 解決過時的 DNS 問題,方法是定期循環
HttpMessageHandler實例。
還有其他方法可以利用長壽命 SocketsHttpHandler 實例來解決前述問題。
- 在應用程式啟動時建立
SocketsHttpHandler的實例,並將其用於應用程式生命週期。 - 根據 DNS 重新整理次數,將 PooledConnectionLifetime 設定為適當的值。
- 請根據需要使用
new HttpClient(handler, disposeHandler: false)建立HttpClient實例。
替代方法以類似的方式解決了IHttpClientFactory所解決的資源管理問題。
-
SocketsHttpHandler會共用HttpClient實例之間的連線。 此共享可防止通訊端耗盡。 -
SocketsHttpHandler會根據PooledConnectionLifetime循環使用連線,以避免發生過時的 DNS 問題。
日誌訊息與回應狀態
透過 IHttpClientFactory 建立的用戶端會記錄所有要求的記錄訊息。 在日誌設定中啟用相應的資訊層級,這樣你才能看到預設的日誌訊息。 其他記錄,例如請求標頭的記錄,僅在追蹤層次包含。
用於每個用戶端的記錄檔分類包含用戶端的名稱。 例如,一個名為 MyNamedClient 的用戶端會記錄類別為「System.Net.Http.HttpClient.MyNamedClient.LogicalHandler」的訊息。帶有 LogicalHandler 後綴的訊息會出現在請求處理流程之外。 在請求中,訊息會在管線中其他處理器對其處理之前被記錄。 在回應時,訊息會在其他管線處理者收到回應後被記錄。
記錄也會發生在請求處理管線中。 在 MyNamedClient 的例子中,這些訊息會以「System.Net.Http.HttpClient.MyNamedClient.ClientHandler」的日誌類別記錄。對於請求,日誌記錄會在所有其他處理器執行後、請求發送前立即進行。 在回應時,日誌會包含回應在返回處理流程前的狀態。
在管線內外啟用日誌記錄,這樣可以檢查其他管線處理常式所作的變更。 日誌可能包括請求標頭或回應狀態碼的變更。
在記錄分類中包含用戶端的名稱,可讓您進行特定具名用戶端的記錄檔篩選。
設定 HttpMessageHandler
可能有必要控制客戶端使用之內部HttpMessageHandler的配置。
新增具名或具型別客戶端時,會傳回 IHttpClientBuilder。
ConfigurePrimaryHttpMessageHandler 擴充方法可以用來定義委派。 使用委派來建立和設定由該用戶端使用的主要 HttpMessageHandler。
builder.Services.AddHttpClient("ConfiguredHttpMessageHandler")
.ConfigurePrimaryHttpMessageHandler(() =>
new HttpClientHandler
{
AllowAutoRedirect = true,
UseDefaultCredentials = true
});
處理 cookies
集區式 HttpMessageHandler 實例會導致共用 CookieContainer 物件。 未預期的 CookieContainer 物件共用通常會導致程式碼不正確。
對於需要 Cookie 的應用程式,請考慮:
- 停用自動 cookie 處理
- 避免
IHttpClientFactory
呼叫 ConfigurePrimaryHttpMessageHandler 以停用自動 cookie 處理:
builder.Services.AddHttpClient("NoAutomaticCookies")
.ConfigurePrimaryHttpMessageHandler(() =>
new HttpClientHandler
{
UseCookies = false
});
在主控台應用程式中使用 IHttpClientFactory
在主控台應用程式中,將下列套件參考新增至專案:
在以下範例中:
-
IHttpClientFactory 和
GitHubService已在泛型主機的服務容器中註冊。 -
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 package.
在
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 Larkin、Steve Gordon、Glenn Condron 和 Ryan Nowak 撰寫。
IHttpClientFactory 可以註冊並用於在應用中配置和創建 HttpClient 實例。
IHttpClientFactory 提供下列優點:
- 提供一個集中位置,用於命名和設定邏輯
HttpClient執行個體。 例如,一個名為 github 的用戶端可以註冊並設定為存取 GitHub。 預設用戶端可以註冊用於一般存取。 - 透過委派處理常式在
HttpClient中來定義傳出中介軟體的概念。 提供適用於 Polly 型中介軟體的延伸模組,以利用HttpClient中的委派處理常式。 - 管理基礎
HttpClientMessageHandler執行個體的共用和存留期。 自動管理可避免在手動管理HttpClient存留期時,所發生的常見網域名稱系統 (DNS) 問題。 - 針對透過工廠所建立之用戶端傳送的所有要求,新增可設定的日誌功能 (透過
ILogger)。
檢視或下載範例程式碼 (如何下載)。
本主題版本中的範例程式碼會使用 System.Text.Json 來還原序列化 HTTP 回應中所傳回的 JSON 內容。 對於使用 Json.NET 和 ReadAsAsync<T> 的範例,請使用版本選擇器選擇此主題的 2.x 版本。
消費模式
有數種方式可將 IHttpClientFactory 用於應用程式:
最好的方法取決於應用程式的需求。
基本用法
可以藉由呼叫 IHttpClientFactory 來註冊 AddHttpClient。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
// Remaining code deleted for brevity.
可以使用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都有不同的組態。
具名 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");
});
在上述程式碼中,會使用下列項目設定用戶端:
- 基底位址
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被公開為public屬性。
可以建立公開 HttpClient 功能的 API 特定方法。 例如,GetAspNetDocsIssues 方法會封裝程式碼,以擷取未解決的問題。
下列程式碼會在 AddHttpClient 中呼叫 Startup.ConfigureServices,以註冊類型化客戶端類別:
services.AddHttpClient<GitHubService>();
具類型化的客戶端會在依賴注入(DI)中註冊為瞬態。 在上述程式碼中,AddHttpClient 會將 GitHubService 註冊為暫時性服務。 此註冊會使用工廠方法來:
- 建立
HttpClient的執行個體。 - 建立
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 儲存於私用欄位。 對 HttpClient 的存取是透過公用 GetRepos 方法。
產生的用戶端
IHttpClientFactory 可和第三方程式庫一起使用,例如 Refit。 Refit 是一個REST 函式庫,適合.NET。 它將 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
- DELETE
- 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 方法:
- 使用
TodoItem將System.Text.Json參數序列化為 JSON。 這會使用 JsonSerializerOptions 的執行個體來設定序列化流程。 - 建立 StringContent 的實例來封裝序列化的 JSON,用於在 HTTP 請求正文中發送。
- 呼叫 PostAsync,將 JSON 內容傳送至指定的 URL。 這是新增至 HttpClient.BaseAddress 的相對 URL。
- 如果回應狀態碼未表示成功,則呼叫 EnsureSuccessStatusCode 以擲回例外狀況。
HttpClient 也支援其他型別的內容。 例如,MultipartContent 與 StreamContent。 如需完整的支援內容清單,請參閱 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 執行個體的多載。
若要深入了解搭配 HttpClient 使用不同的 HTTP 動詞,請參閱 HttpClient。
外發請求中介軟體
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 標頭是否在要求中。 如果 X-API-KEY 遺漏,則會傳回 BadRequest。
對於包含 HttpClient 和 Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler 的配置,您可以新增多個處理程式:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ValidateHeaderHandler>();
services.AddHttpClient("externalservice", c =>
{
// Assume this is an "external" service which requires an API KEY
c.BaseAddress = new Uri("https://localhost:5001/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();
// Remaining code deleted for brevity.
在上述程式碼,ValidateHeaderHandler 已向 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>();
在傳出請求的中介程式中使用 DI
IHttpClientFactory 建立新的委派處理常式時,會使用 DI 來完成處理常式的建構函式參數。
IHttpClientFactory 會為每個處理常式建立個別的 DI 範圍,當處理常式取用限定範圍的服務時,可能會導致出人意料的行為。
例如,請考慮下列表示任務的介面及其實作,其中任務作為具有識別碼 OperationId 的操作:
public interface IOperationScoped
{
string OperationId { get; }
}
public class OperationScoped : IOperationScoped
{
public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}
正如其名稱所建議,IOperationScoped 會使用限定範圍的存留期向 DI 註冊:
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 秒變更一次。
處理常式可以依賴任何範疇的服務。 當處理器被處置時,其相依的服務也會被處置。
使用下列其中一種方式來與訊息處理常式共用每個請求的狀態:
- 使用 HttpRequestMessage.Options 將資料傳送到處理器。
- 使用IHttpContextAccessor 來存取目前的請求。
- 建立自訂 AsyncLocal<T> 儲存體物件以傳遞資料。
使用 Polly 為基礎的處理常式
IHttpClientFactory 與協力廠商程式庫 Polly 整合。 Polly 是一個完整的 .NET 韌性與瞬態故障處理函式庫。 它可讓開發人員以流暢且執行緒安全的方式表達策略,例如重試、斷路器、超時、艙壁隔離和回退。
提供擴充方法以便使用 Polly 原則與已設定的 HttpClient 執行個體。 Polly 延伸模組支援將以 Polly 為基礎的處理常式新增至用戶端。 Polly 需要Microsoft.Extensions.Http.Polly NuGet 套件。
處理暫時性錯誤
錯誤通常發生在外部 HTTP 呼叫是暫時性的時候。
AddTransientHttpErrorPolicy 允許定義原則來處理暫時性錯誤。 以 AddTransientHttpErrorPolicy 設定的原則會處理下列回應:
- HttpRequestException
- HTTP 5xx
- HTTP 408
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 毫秒的延遲時間。
動態選取策略
提供擴充方法以新增 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呼叫會新增斷路器政策。 如果循序發生五次失敗的嘗試,進一步的外部要求會遭到封鎖 30 秒。 斷路器策略是有狀態的。 透過此用戶端的所有呼叫都會共用相同的線路狀態。
從 Polly 登錄新增原則
定期使用政策的管理方法之一是先定義,然後將其註冊到 PolicyRegistry。
在下列程式碼中:
- 會新增「一般」和「長」策略。
- AddPolicyHandlerFromRegistry 新增了來自註冊表的「一般」和「長」原則。
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 和存留期管理
每次在 HttpClient 上呼叫 CreateClient,都會返回一個新的 IHttpClientFactory 實例。 每個指定客戶都會建立一個 HttpMessageHandler 。 工廠負責管理 HttpMessageHandler 執行個體的存留期。
IHttpClientFactory 會將處理站所建立的 HttpMessageHandler 執行個體放入集區以減少資源耗用量。
HttpMessageHandler如果實例的壽命尚未過期,建立新HttpClient實例時,該實例可能會被從池中重複使用。
將處理常式放入集區非常實用,因為處理常式通常會管理自己專屬的底層 HTTP 連線。 建立比所需數目更多的處理常式,可能會導致連線延遲。 有些處理常式也會保持連線無限期地開啟,這可能導致處理常式無法對 DNS (網域名稱系統) 變更回應。
預設處理常式存留時間為兩分鐘。 預設值可依每個命名的用戶端來覆寫:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("extendedhandlerlifetime")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
// Remaining code deleted for brevity.
HttpClient 實例通常可以視為.NET物件,不需要釋放。 處置會取消傳出的要求,並保證指定的 HttpClient 執行個體在呼叫 Dispose 之後無法使用。
IHttpClientFactory 會追蹤並處置 HttpClient 執行個體使用的資源。
在開始使用 HttpClient 之前,維持單一 IHttpClientFactory 執行個體長時間存續是一種常見模式。 在移轉到 IHttpClientFactory 之後,就不再需要此模式。
IHttpClientFactory 的替代方案
在已啟用 DI 的應用程式中使用 IHttpClientFactory 可避免:
- 藉由集結
HttpMessageHandler實例來解決資源耗盡的問題。 - 解決過時的 DNS 問題,方法是定期循環
HttpMessageHandler實例。
有替代方案可以使用長期 SocketsHttpHandler 實例來解決上述問題。
- 在應用程式啟動時建立
SocketsHttpHandler的實例,並將其用於應用程式生命週期。 - 根據 DNS 重新整理次數,將 PooledConnectionLifetime 設定為適當的值。
- 視需要使用
HttpClient建立new HttpClient(handler, disposeHandler: false)實例。
上述方法可解決資源管理問題,IHttpClientFactory 會以類似方式解決。
-
SocketsHttpHandler會共用HttpClient實例之間的連線。 此共享可防止通訊端耗盡。 -
SocketsHttpHandler會根據PooledConnectionLifetime循環使用連線,以避免發生過時的 DNS 問題。
Cookies
集區式 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 配置。
新增具名或具型別客戶端時,會傳回 IHttpClientBuilder。
ConfigurePrimaryHttpMessageHandler 擴充方法可以用來定義委派。 使用委派來建立和設定由該用戶端使用的主要 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從服務中建立一個用來產生HttpClient的用戶端工廠實例。HttpClient會用來擷取網頁。 -
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 用戶端請求。 若要使用標頭傳播:
在
HttpClient中設定Startup和中介軟體:public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHttpClient("MyForwardingClient").AddHeaderPropagation(); services.AddHeaderPropagation(options => { options.Headers.Add("X-TraceId"); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseHeaderPropagation(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }已配置的標頭包含在用戶端的外發請求中:
var client = clientFactory.CreateClient("MyForwardingClient"); var response = client.GetAsync(...);
由 Kirk Larkin、Steve Gordon、Glenn Condron 和 Ryan Nowak 撰寫。
IHttpClientFactory 可以註冊並用於在應用中配置和創建 HttpClient 實例。
IHttpClientFactory 提供下列優點:
- 提供一個集中位置,用於命名和設定邏輯
HttpClient執行個體。 例如,一個名為 github 的用戶端可以註冊並設定為存取 GitHub。 預設用戶端可以註冊用於一般存取。 - 透過委派處理常式在
HttpClient中來定義傳出中介軟體的概念。 提供適用於 Polly 型中介軟體的延伸模組,以利用HttpClient中的委派處理常式。 - 管理基礎
HttpClientMessageHandler執行個體的共用和存留期。 自動管理可避免在手動管理HttpClient存留期時,所發生的常見網域名稱系統 (DNS) 問題。 - 針對透過工廠所建立之用戶端傳送的所有要求,新增可設定的日誌功能 (透過
ILogger)。
檢視或下載範例程式碼 (如何下載)。
本主題版本中的範例程式碼會使用 System.Text.Json 來還原序列化 HTTP 回應中所傳回的 JSON 內容。 對於使用 Json.NET 和 ReadAsAsync<T> 的範例,請使用版本選擇器選擇此主題的 2.x 版本。
消費模式
有數種方式可將 IHttpClientFactory 用於應用程式:
最好的方法取決於應用程式的需求。
基本用法
可以藉由呼叫 IHttpClientFactory 來註冊 AddHttpClient。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
// Remaining code deleted for brevity.
可以使用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都有不同的組態。
具名 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");
});
在上述程式碼中,會使用下列項目設定用戶端:
- 基底位址
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);
}
}
在上述程式碼中:
- 組態設定已移至具類型的客戶端。
- 物件
HttpClient被公開為public屬性。
可以建立公開 HttpClient 功能的 API 特定方法。 例如,GetAspNetDocsIssues 方法會封裝程式碼,以擷取未解決的問題。
下列程式碼會在 AddHttpClient 中呼叫 Startup.ConfigureServices,以註冊類型化客戶端類別:
services.AddHttpClient<GitHubService>();
具類型化的客戶端會在依賴注入(DI)中註冊為瞬態。 在上述程式碼中,AddHttpClient 會將 GitHubService 註冊為暫時性服務。 此註冊會使用工廠方法來:
- 建立
HttpClient的執行個體。 - 建立
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 儲存於私用欄位。 對 HttpClient 的存取是透過公用 GetRepos 方法。
產生的用戶端
IHttpClientFactory 可和第三方程式庫一起使用,例如 Refit。 Refit 是一個REST 函式庫,適合.NET。 它將 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
- DELETE
- 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 方法:
- 使用
TodoItem將System.Text.Json參數序列化為 JSON。 這會使用 JsonSerializerOptions 的執行個體來設定序列化流程。 - 建立 StringContent 的實例來封裝序列化的 JSON,用於在 HTTP 請求正文中發送。
- 呼叫 PostAsync,將 JSON 內容傳送至指定的 URL。 這是新增至 HttpClient.BaseAddress 的相對 URL。
- 如果回應狀態碼未表示成功,則呼叫 EnsureSuccessStatusCode 以擲回例外狀況。
HttpClient 也支援其他型別的內容。 例如,MultipartContent 與 StreamContent。 如需完整的支援內容清單,請參閱 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 執行個體的多載。
若要深入了解搭配 HttpClient 使用不同的 HTTP 動詞,請參閱 HttpClient。
外發請求中介軟體
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 標頭是否在要求中。 如果 X-API-KEY 遺漏,則會傳回 BadRequest。
對於包含 HttpClient 和 Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler 的配置,您可以新增多個處理程式:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ValidateHeaderHandler>();
services.AddHttpClient("externalservice", c =>
{
// Assume this is an "external" service which requires an API KEY
c.BaseAddress = new Uri("https://localhost:5001/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();
// Remaining code deleted for brevity.
在上述程式碼,ValidateHeaderHandler 已向 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>();
在傳出請求的中介程式中使用 DI
IHttpClientFactory 建立新的委派處理常式時,會使用 DI 來完成處理常式的建構函式參數。
IHttpClientFactory 會為每個處理常式建立個別的 DI 範圍,當處理常式取用限定範圍的服務時,可能會導致出人意料的行為。
例如,請考慮下列表示任務的介面及其實作,其中任務作為具有識別碼 OperationId 的操作:
public interface IOperationScoped
{
string OperationId { get; }
}
public class OperationScoped : IOperationScoped
{
public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}
正如其名稱所建議,IOperationScoped 會使用限定範圍的存留期向 DI 註冊:
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 秒變更一次。
處理常式可以依賴任何範疇的服務。 當處理器被處置時,其相依的服務也會被處置。
使用下列其中一種方式來與訊息處理常式共用每個請求的狀態:
- 使用 HttpRequestMessage.Properties 將資料傳送到處理器。
- 使用IHttpContextAccessor 來存取目前的請求。
- 建立自訂 AsyncLocal<T> 儲存體物件以傳遞資料。
使用 Polly 為基礎的處理常式
IHttpClientFactory 與協力廠商程式庫 Polly 整合。 Polly 是一個完整的 .NET 韌性與瞬態故障處理函式庫。 它可讓開發人員以流暢且執行緒安全的方式表達策略,例如重試、斷路器、超時、艙壁隔離和回退。
提供擴充方法以便使用 Polly 原則與已設定的 HttpClient 執行個體。 Polly 延伸模組支援將以 Polly 為基礎的處理常式新增至用戶端。 Polly 需要Microsoft.Extensions.Http.Polly NuGet 套件。
處理暫時性錯誤
錯誤通常發生在外部 HTTP 呼叫是暫時性的時候。
AddTransientHttpErrorPolicy 允許定義原則來處理暫時性錯誤。 以 AddTransientHttpErrorPolicy 設定的原則會處理下列回應:
- HttpRequestException
- HTTP 5xx
- HTTP 408
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 毫秒的延遲時間。
動態選取策略
提供擴充方法以新增 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呼叫會新增斷路器政策。 如果循序發生五次失敗的嘗試,進一步的外部要求會遭到封鎖 30 秒。 斷路器策略是有狀態的。 透過此用戶端的所有呼叫都會共用相同的線路狀態。
從 Polly 登錄新增原則
定期使用政策的管理方法之一是先定義,然後將其註冊到 PolicyRegistry。
在下列程式碼中:
- 會新增「一般」和「長」策略。
- AddPolicyHandlerFromRegistry 新增了來自註冊表的「一般」和「長」原則。
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 和存留期管理
每次在 HttpClient 上呼叫 CreateClient,都會返回一個新的 IHttpClientFactory 實例。 為每個指定的客戶建立一個 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物件,不需要釋放。 處置會取消傳出的要求,並保證指定的 HttpClient 執行個體在呼叫 Dispose 之後無法使用。
IHttpClientFactory 會追蹤並處置 HttpClient 執行個體使用的資源。
在開始使用 HttpClient 之前,維持單一 IHttpClientFactory 執行個體長時間存續是一種常見模式。 在移轉到 IHttpClientFactory 之後,就不再需要此模式。
IHttpClientFactory 的替代方案
在已啟用 DI 的應用程式中使用 IHttpClientFactory 可避免:
- 藉由集結
HttpMessageHandler實例來解決資源耗盡的問題。 - 解決過時的 DNS 問題,方法是定期循環
HttpMessageHandler實例。
有替代方案可以使用長期 SocketsHttpHandler 實例來解決上述問題。
- 在應用程式啟動時建立
SocketsHttpHandler的實例,並將其用於應用程式生命週期。 - 根據 DNS 重新整理次數,將 PooledConnectionLifetime 設定為適當的值。
- 視需要使用
HttpClient建立new HttpClient(handler, disposeHandler: false)實例。
上述方法可解決資源管理問題,IHttpClientFactory 會以類似方式解決。
-
SocketsHttpHandler會共用HttpClient實例之間的連線。 此共享可防止通訊端耗盡。 -
SocketsHttpHandler會根據PooledConnectionLifetime循環使用連線,以避免發生過時的 DNS 問題。
Cookies
集區式 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 配置。
新增具名或具型別客戶端時,會傳回 IHttpClientBuilder。
ConfigurePrimaryHttpMessageHandler 擴充方法可以用來定義委派。 使用委派來建立和設定由該用戶端使用的主要 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從服務中建立一個用來產生HttpClient的用戶端工廠實例。HttpClient會用來擷取網頁。 -
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 用戶端請求。 若要使用標頭傳播:
在
HttpClient中設定Startup和中介軟體:public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHttpClient("MyForwardingClient").AddHeaderPropagation(); services.AddHeaderPropagation(options => { options.Headers.Add("X-TraceId"); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseHeaderPropagation(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }已配置的標頭包含在用戶端的外發請求中:
var client = clientFactory.CreateClient("MyForwardingClient"); var response = client.GetAsync(...);
作者:Glenn Condron、Ryan Nowak 和 Steve Gordon
IHttpClientFactory 可以註冊並用於在應用中配置和創建 HttpClient 實例。 它提供下列優點:
- 提供一個集中位置,用於命名和設定邏輯
HttpClient執行個體。 例如,github 用戶端可以註冊並設定以存取 GitHub。 預設用戶端可以註冊用於其他用途。 - 透過委派
HttpClient中的處理常式來撰寫外寄中介軟體的概念,並提供延伸模組以便 Polly 架構中介軟體利用外寄中介軟體。 - 管理基礎
HttpClientMessageHandler執行個體的共用和存留期,以避免在手動管理HttpClient存留期時,發生的常見 DNS 問題。 - 針對透過工廠所建立之用戶端傳送的所有要求,新增可設定的日誌功能 (透過
ILogger)。
檢視或下載範例程式碼 \(英文\) (如何下載)
Prerequisites
針對 .NET Framework 的專案需要安裝 Microsoft。Extensions.Http NuGet 套件。 針對 .NET Core 並參考 Microsoft.AspNetCore.App metapackage 的專案已經包含 Microsoft.Extensions.Http 套件。
消費模式
有數種方式可將 IHttpClientFactory 用於應用程式:
它們並沒有嚴格優於另一個。 最好的方法取決於應用程式的條件約束。
基本用法
IHttpClientFactory 可以藉由在 AddHttpClient 方法內於 IServiceCollection 上呼叫 Startup.ConfigureServices 擴充方法來註冊。
services.AddHttpClient();
註冊之後,程式碼可以在任何可通過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)
{
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 被公開為public 屬性。 您可定義 API 特定的方法,其公開 HttpClient 功能。
GetAspNetDocsIssues 方法封裝了查詢並解析 GitHub 倉庫中最新未解決議題所需的程式碼。
若要註冊具型別用戶端,泛型 AddHttpClient 擴充方法可用於 Startup.ConfigureServices 內,並指定具型別用戶端類別:
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 是一個REST 函式庫,適合.NET。 它將 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>();
在上述程式碼,ValidateHeaderHandler 已向 DI 註冊。 處理常式必須在 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 韌性與瞬態故障處理函式庫。 它可讓開發人員以流暢且執行緒安全的方式表達策略,例如重試、斷路器、超時、艙壁隔離和回退。
提供擴充方法以便使用 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 毫秒的延遲時間。
動態選取策略
還有其他擴充方法可用來新增基於 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 和存留期管理
每次在 HttpClient 上呼叫 CreateClient,都會返回一個新的 IHttpClientFactory 實例。 每個指定客戶都有一個 HttpMessageHandler 。 工廠負責管理 HttpMessageHandler 執行個體的存留期。
IHttpClientFactory 會將處理站所建立的 HttpMessageHandler 執行個體放入集區以減少資源耗用量。 如果HttpMessageHandler實例的壽命尚未過期,那麼在建立新HttpClient實例時,該實例可能會從池中重新使用。
將處理常式放入集區非常實用,因為處理常式通常會管理自己專屬的底層 HTTP 連線。 建立比所需數目更多的處理常式,可能會導致連線延遲。 有些處理常式也會保持連線無限期地開啟,這可能導致處理常式無法對 DNS 變更回應。
預設處理常式存留時間為兩分鐘。 預設值可以依每個指定的客戶端來覆寫。 請在建立用戶端後所返回的 SetHandlerLifetime 上調用 IHttpClientBuilder 以覆蓋它。
services.AddHttpClient("extendedhandlerlifetime")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
不需要處置用戶端。 處置會取消傳出的要求,並保證指定的 HttpClient 執行個體在呼叫 Dispose 之後無法使用。
IHttpClientFactory 會追蹤並處置 HttpClient 執行個體使用的資源。
HttpClient 實例通常可視為不需丟棄的 .NET 物件。
在開始使用 HttpClient 之前,維持單一 IHttpClientFactory 執行個體長時間存續是一種常見模式。 在移轉到 IHttpClientFactory 之後,就不再需要此模式。
IHttpClientFactory 的替代方案
在已啟用 DI 的應用程式中使用 IHttpClientFactory 可避免:
- 藉由集結
HttpMessageHandler實例來解決資源耗盡的問題。 - 解決過時的 DNS 問題,方法是定期循環
HttpMessageHandler實例。
有替代方案可以使用長期 SocketsHttpHandler 實例來解決上述問題。
- 在應用程式啟動時建立
SocketsHttpHandler的實例,並將其用於應用程式生命週期。 - 根據 DNS 重新整理次數,將 PooledConnectionLifetime 設定為適當的值。
- 視需要使用
HttpClient建立new HttpClient(handler, disposeHandler: false)實例。
上述方法可解決資源管理問題,IHttpClientFactory 會以類似方式解決。
-
SocketsHttpHandler會共用HttpClient實例之間的連線。 此共享可防止通訊端耗盡。 -
SocketsHttpHandler會根據PooledConnectionLifetime循環使用連線,以避免發生過時的 DNS 問題。
Cookies
集區式 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配置。
新增具名或具型別客戶端時,會傳回 IHttpClientBuilder。
ConfigurePrimaryHttpMessageHandler 擴充方法可以用來定義委派。 使用委派來建立和設定由該用戶端使用的主要 HttpMessageHandler。
services.AddHttpClient("configured-inner-handler")
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler()
{
AllowAutoRedirect = false,
UseDefaultCredentials = true
};
});
在主控台應用程式中使用 IHttpClientFactory
在主控台應用程式中,將下列套件參考新增至專案:
在以下範例中:
- IHttpClientFactory 已在泛型主機的服務容器中註冊。
-
MyService從服務中建立一個用來產生HttpClient的用戶端工廠實例。HttpClient會用來擷取網頁。 - 會執行此服務的
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。
在
HttpClient中設定Startup和中介軟體: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(...);
相關內容
- 檢視或下載範例程式碼 \(英文\) (如何下載)