ASP.NET Core基礎知識(十四)【發出 HTTP 請求】

風靈使發表於2019-02-20

可以註冊 IHttpClientFactory並將其用於配置和建立應用中的 HttpClient例項。 這能帶來以下好處:

  • 提供一箇中心位置,用於命名和配置邏輯 HttpClient 例項。 例如,可以註冊 github 客戶端,並將它配置為訪問 GitHub。 可以註冊一個預設客戶端用於其他用途。
  • 通過委託 HttpClient 中的處理程式整理出站中介軟體的概念,並提供適用於基於 Polly 的中介軟體的擴充套件來利用概念。
  • 管理基礎 HttpClientMessageHandler 例項的池和生存期,避免在手動管理 HttpClient 生存期時出現常見的 DNS 問題。
  • (通過 ILogger)新增可配置的記錄體驗,以處理工廠建立的客戶端傳送的所有請求。

系統必備

面向.NET Framework 的專案要求安裝 Microsoft.Extensions.Http NuGet 包。 面向 .NET Core 且引用 Microsoft.AspNetCore.App 元包的專案已經包括 Microsoft.Extensions.Http 包。

消耗模式

在應用中可以通過以下多種方式使用 IHttpClientFactory

  • 基本用法
  • 命名客戶端
  • 型別化客戶端
  • 生成的客戶端

它們之間不存在嚴格的優先順序。 最佳方法取決於應用的約束條件。

基本用法

Startup.ConfigureServices 方法中,通過在 IServiceCollection 上呼叫 AddHttpClient 擴充套件方法可以註冊 IHttpClientFactory

services.AddHttpClient();

註冊後,在可以使用依賴關係注入 (DI) 注入服務的任何位置,程式碼都能接受 IHttpClientFactoryIHttpClientFactory 可以用於建立 HttpClient 例項:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://api.github.com/repos/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 適合重構現有應用。 這不會影響 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/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>();
        }
    }
}

在上述程式碼中,請求不需要指定主機名。 可以僅傳遞路徑,因為採用了為客戶端配置的基址。

型別化客戶端

型別化客戶端提供與命名客戶端一樣的功能,不需要將字串用作金鑰。 型別化客戶端方法在使用客戶端時提供 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/aspnet/docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

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

        return result;
    }
}

在上述程式碼中,配置轉移到了型別化客戶端中。 HttpClient 物件公開為公共屬性。 可以定義公開 HttpClient 功能的特定於 API 的方法。 GetAspNetDocsIssues 方法從 GitHub 儲存庫封裝查詢和分析最新待解決問題所需的程式碼。

要註冊型別化客戶端,可在 Startup.ConfigureServices 中使用通用的 xref:Microsoft.Extensions.DependencyInjection.HttpClientFactoryServiceCollectionExtensions.AddHttpClient* 擴充套件方法,指定型別化客戶端類:

services.AddHttpClient<GitHubService>();

使用 DI 將型別客戶端註冊為暫時客戶端。 可以直接插入或使用型別化客戶端:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }            
    }
}

根據你的喜好,可以在 Startup.ConfigureServices 中註冊時指定型別化客戶端的配置,而不是在型別化客戶端的建構函式中指定:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

可以將 HttpClient 完全封裝在型別化客戶端中。 不是將它公開為屬性,而是可以提供公共方法,用於在內部呼叫 HttpClient

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

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

        return result;
    }
}

在上述程式碼中,HttpClient 儲存未私有欄位。 進行外部呼叫的所有訪問都經由 GetRepos 方法。

生成的客戶端

IHttpClientFactory 可結合其他第三方庫(例如 Refit)使用。 Refit 是.NET 的 REST 庫。 它將 REST API 轉換為實時介面。 RestService 動態生成該介面的實現,使用 HttpClient 進行外部 HTTP 呼叫。

定義了介面和答覆來代表外部 API 及其響應:

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

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

可以新增型別化客戶端,使用 Refit 生成實現:

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

    services.AddMvc();
}

可以在必要時使用定義的介面,以及由 DIRefit 提供的實現:

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

出站請求中介軟體

HttpClient 已經具有委託處理程式的概念,這些委託處理程式可以連結在一起,處理出站 HTTP 請求。 IHttpClientFactory 可以輕鬆定義處理程式並應用於每個命名客戶端。 它支援註冊和連結多個處理程式,以生成出站請求中介軟體管道。 每個處理程式都可以在出站請求前後執行工作。 此模式類似於 ASP.NET Core 中的入站中介軟體管道。 此模式提供了一種用於管理圍繞 HTTP 請求的橫切關注點的機制,包括快取、錯誤處理、序列化以及日誌記錄。

要建立處理程式,請定義一個派生自 DelegatingHandler的類。 重寫 SendAsync 方法,在將請求傳遞至管道中的下一個處理程式之前執行程式碼:

public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

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

上述程式碼定義了基本處理程式。 它檢查請求中是否包含 X-API-KEY 頭。 如果標頭缺失,它可以避免 HTTP 呼叫,並返回合適的響應。

在註冊期間可將一個或多個標頭新增到 HttpClient 的配置。 此任務通過 IHttpClientBuilder上的擴充套件方法完成。

services.AddTransient<ValidateHeaderHandler>();

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

在上述程式碼中通過 DI 註冊了 ValidateHeaderHandlerIHttpClientFactory 為每個處理程式建立單獨的 DI 作用域。 處理程式可依賴於任何作用域的服務。 處理程式依賴的服務會在處置處理程式時得到處置。

註冊後可以呼叫 AddHttpMessageHandler,傳入標頭的型別。

可以按處理程式應該執行的順序註冊多個處理程式。 每個處理程式都會覆蓋下一個處理程式,直到最終 HttpClientHandler 執行請求:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

使用以下方法之一將每個請求狀態與訊息處理程式共享:

  • 使用 HttpRequestMessage.Properties 將資料傳遞到處理程式。
  • 使用 IHttpContextAccessor 訪問當前請求。
  • 建立自定義 AsyncLocal 儲存物件以傳遞資料。

使用基於 Polly 的處理程式

IHttpClientFactory 與一個名為 Polly 的熱門第三方庫整合。 Polly 是適用於 .NET 的全面恢復和臨時故障處理庫。 開發人員通過它可以表達策略,例如以流暢且執行緒安全的方式處理重試、斷路器、超時、Bulkhead 隔離和回退。

提供了擴充套件方法,以實現將 Polly 策略用於配置的 HttpClient 例項。 Microsoft.Extensions.Http.Polly NuGet 包中提供 Polly 擴充套件。 Microsoft.AspNetCore.App 元包中不包括此包。 若要使用擴充套件,專案中應該包括顯式 <PackageReference />

<Project Sdk="Microsoft.NET.Sdk.Web">
  
  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>
  
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="2.1.1" />
  </ItemGroup>
  
</Project>

還原此包後,可以使用擴充套件方法來支援將基於 Polly 的處理程式新增至客戶端。

處理臨時故障

大多數常見錯誤在暫時執行外部 HTTP 呼叫時發生。 包含了一種簡便的擴充套件方法,該方法名為 AddTransientHttpErrorPolicy,允許定義策略來處理臨時故障。 使用這種擴充套件方法配置的策略可以處理 HttpRequestException、HTTP 5xx 響應以及 HTTP 408 響應。

AddTransientHttpErrorPolicy 擴充套件可在 Startup.ConfigureServices 內使用。 該擴充套件可以提供 PolicyBuilder 物件的訪問許可權,該物件配置為處理表示可能的臨時故障的錯誤:

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

上述程式碼中定義了 WaitAndRetryAsync 策略。 請求失敗後最多可以重試三次,每次嘗試間隔 600 ms。

動態選擇策略

存在其他擴充套件方法,可以用於新增基於 Polly 的處理程式。 這類擴充套件的其中一個是 AddPolicyHandler,它具備多個過載。 一個過載允許在定義要應用的策略時檢查該請求:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

在上述程式碼中,如果出站請求為 GET,則應用 10 秒超時。 其他所有 HTTP 方法應用 30 秒超時。

新增多個 Polly 處理程式

巢狀 Polly 策略以增強功能是很常見的:

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

在上述示例中,新增兩個處理程式。 第一個使用 AddTransientHttpErrorPolicy 擴充套件新增重試策略。 若請求失敗,最多可重試三次。 第二個呼叫 AddTransientHttpErrorPolicy 新增斷路器策略。 如果嘗試連續失敗了五次,則會阻止後續外部請求 30 秒。 斷路器策略處於監控狀態。 通過此客戶端進行的所有呼叫都共享同樣的線路狀態。

Polly 登錄檔新增策略

管理常用策略的一種方法是一次性定義它們並使用 PolicyRegistry 註冊它們。 提供了一種擴充套件方法,可以使用登錄檔中的策略新增處理程式:

var registry = services.AddPolicyRegistry();

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

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

在上面的程式碼中,兩個策略在 PolicyRegistry 新增到 ServiceCollection 中時進行註冊。 若要使用登錄檔中的策略,請使用 AddPolicyHandlerFromRegistry 方法,同時傳遞要應用的策略的名稱。

要進一步瞭解 IHttpClientFactory 和 Polly 整合,請參考 Polly Wiki

HttpClient 和生存期管理

每次對 IHttpClientFactory 呼叫 CreateClient 都會返回一個新 HttpClient 例項。 每個命名的客戶端都具有一個 HttpMessageHandler。 工廠管理 HttpMessageHandler 例項的生存期。

IHttpClientFactory 將工廠建立的 HttpMessageHandler 例項彙集到池中,以減少資源消耗。 新建 HttpClient 例項時,可能會重用池中的 HttpMessageHandler 例項(如果生存期尚未到期的話)。

由於每個處理程式通常管理自己的基礎 HTTP 連線,因此需要池化處理程式。 建立超出必要數量的處理程式可能會導致連線延遲。 部分處理程式還保持連線無期限地開啟,這樣可以防止處理程式對 DNS 更改作出反應。

處理程式的預設生存期為兩分鐘。 可在每個命名客戶端上重寫預設值。 要重寫該值,請在建立客戶端時在返回的 IHttpClientBuilder 上呼叫 SetHandlerLifetime

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

無需處置客戶端。 處置既取消傳出請求,又保證在呼叫 Dispose後無法使用給定的 HttpClient 例項。 IHttpClientFactory 跟蹤和處置 HttpClient 例項使用的資源。 HttpClient 例項通常可視為無需處置的 .NET 物件。

保持各個 HttpClient 例項長時間處於活動狀態是在 IHttpClientFactory 推出前使用的常見模式。 遷移到 IHttpClientFactory 後,就無需再使用此模式。

日誌記錄

通過 IHttpClientFactory 建立的客戶端記錄所有請求的日誌訊息。 在日誌記錄配置中啟用合適的資訊級別可以檢視預設日誌訊息。 僅在跟蹤級別包含附加日誌記錄(例如請求標頭的日誌記錄)。

用於每個客戶端的日誌類別包含客戶端名稱。 例如,名為“MyNamedClient”的客戶端使用 System.Net.Http.HttpClient.MyNamedClient.LogicalHandler 類別來記錄訊息。 字尾為 LogicalHandler 的訊息在請求處理程式管道外部發生。 在請求時,在管道中的任何其他處理程式處理請求之前記錄訊息。 在響應時,在任何其他管道處理程式接收響應之後記錄訊息。

日誌記錄還在請求處理程式管道內部發生。 在“MyNamedClient”示例中,這些訊息是針對日誌類別 System.Net.Http.HttpClient.MyNamedClient.ClientHandler 進行記錄。 在請求時,在所有其他處理程式執行後,以及剛好在通過網路發出請求之前記錄訊息。 在響應時,此日誌記錄包含響應在通過處理程式管道被傳遞回去之前的狀態。

在管道內外啟用日誌記錄,可以檢查其他管道處理程式做出的更改。 例如,其中可能包含對請求標頭的更改,或者對響應狀態程式碼的更改。

通過在日誌類別中包含客戶端名稱,可以在必要時對特定的命名客戶端篩選日誌。

配置 HttpMessageHandler

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

在新增命名客戶端或型別化客戶端時,會返回 IHttpClientBuilderConfigurePrimaryHttpMessageHandler擴充套件方法可以用於定義委託。 委託用於建立和配置客戶端使用的主要 HttpMessageHandler

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

相關文章