文章目錄
在 .NET Core 與 .NET Framework 上使用 HttpClientFactory
之前筆記 探討 HttpClient 可能的問題 與 HttpClient 無法反應 DNS 異動的解決方式 的出現是因為工作任務需要將一些重要訊息傳送至 Slack 而留意到 .NET Core 使用的 HttpClientFactory 是改善過去 HttpClient 存在的一些問題,為了可以更完整理解 HttpClient 缺憾做得的一些紀錄
既然對於 HttpClient 過去的問題有些認識後,當然還是得來搞清楚 HttpClientFactory 的內容囉
關於 HttpClientFactory
- 設計理念:
- 提供一個集中位置來命名和設定 HttpClient instance
- 透過委派 DelegatingHandlers 來實現 outgoing 的 middleware
- 統一管理 HttpClientMessageHandler (HttpClientHandler 的基底類別) 的生命周期與連線
- 從 .NET Core 2.1 開始加入,用來建立及管理 HttpClient instance
- HttpClient 底層使用的 HttpClientHandler 的生命周期及 DNS 過期問題會統一由 HttpClientFactory 管理
透過重複使用 HttpClient 底層 connection 來避免 socket 耗盡
過去在 .NET Framework 上可以透過 singleton 或是 static instance 來避免問題發生
透過增加
PooledConnectionLifetime
屬性來處理ServicePointManager.ConnectionLeaseTimeout
過去在 .NET Framework 上可以透過指定
DefaultRequestHeaders.ConnectionClose
設定為true
或是修改ServicePointManager.ConnectionLeaseTimeout
解決,詳細內容可以參考 HttpClient 無法反應 DNS 異動的解決方式每次都取得新的 HttpClient instance 但成本最高的底層 HttpClientHandler 與 connection 則依生命周期決定由 pool 中或是建立新的 instance
HttpClientHandler 預設的存活時間為 2 分鐘
- HttpClientHandler 到期時不會立即被 dispose 而是移至過期的 pool 避免再被新建立的 HttpClient 取用並等待到使用該 HttpClientHandler 中的 HttpClient 結束工作後,自然被 gc 掉
每個具名 HttpClient 可以擁有單獨設定 SetHandlerLifetime 可以指定 Handler 過期時間,這也是 DNS 異動被採用的時間,不得低於 1 秒
前提設定
- Visual Studio 2017 15.9.4
.NET Core 2.2.101
Program.cs
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); }
Startup.cs
public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } }
.NET Framework 4.7.2
詳細使用方法請參考 在 ASP.NET MVC 5 中使用 ASP.NET Core Dependency Injection 與 HttpClientFactory
Startup.cs
public partial class Startup { public void Configuration(IAppBuilder app) { var services = new ServiceCollection(); ConfigureAuth(app); ConfigureServices(services); var resolver = new DefaultDependencyResolver(services.BuildServiceProvider()); DependencyResolver.SetResolver(resolver); } public void ConfigureServices(IServiceCollection services) { services.AddControllersAsServices(typeof(Startup).Assembly.GetExportedTypes() .Where(t => !t.IsAbstract && !t.IsGenericTypeDefinition) .Where(t => typeof(IController).IsAssignableFrom(t) || t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))); } }
DefaultDependencyResolver.cs
public class DefaultDependencyResolver : IDependencyResolver { protected IServiceProvider serviceProvider; public DefaultDependencyResolver(IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider; } public object GetService(Type serviceType) { return this.serviceProvider.GetService(serviceType); } public IEnumerable<object> GetServices(Type serviceType) { return this.serviceProvider.GetServices(serviceType); } }
ServiceProviderExtensions.cs
public static class ServiceProviderExtensions { public static IServiceCollection AddControllersAsServices(this IServiceCollection services, IEnumerable<Type> controllerTypes) { foreach (var type in controllerTypes) { services.AddTransient(type); } return services; } }
使用方式
.NET Core 因已預設參考
Microsoft.AspNetCore.App
的 metapackage,已內建Microsoft.Extensions.Http
plugin,故無須額外安裝套件.NET Framework 需另外安裝
Microsoft.Extensions.Http
NuGet 套件這邊使用
Microsoft.Extensions.Http 2.2.0
Package Manger
Install-Package Microsoft.Extensions.Http
.NET CLI
dotnet add package Microsoft.Extensions.Http
基本用法
最適合用來重構既有系統,有相同的 HttpClient 使用方式,只有將建立 HttpClient instance 使用
CreateClient
取代即可在
Startup.cs
的ConfigureServices
方法中透過IServiceCollection
呼叫擴充方法:AddHttpClient
來進行註冊public void ConfigureServices(IServiceCollection services) { services.AddHttpClient(); }
在需要使用 HttpClient 的 class 中透過 .NET Core 的建構式注入
private readonly IHttpClientFactory _clientFactory; public HomeController(IHttpClientFactory clientFactory) { _clientFactory = clientFactory; }
需要使用 HttpClient 直接透過
CreateClient
方法取得即可var client = _clientFactory.CreateClient();
使用具名 HttpClient
透過在
Startup.ConfigureServices
註冊時指定 client 的名稱及基本設定在
Startup.cs
註冊 HttpClientFactory 時指定名稱及預做基本設定services.AddHttpClient("yowkoblog", c => { c.BaseAddress = new Uri("/"); });
在需要使用 HttpClient 的 class 中透過 .NET Core 的建構式注入 (與
基本用法
相同)private readonly IHttpClientFactory _clientFactory; public HomeController(IHttpClientFactory clientFactory) { _clientFactory = clientFactory; }
需要使用 HttpClient 直接透過
CreateClient
方法並指定名稱 (‘yowkoblog’) 即可 (與基本用法
接近)//名稱與 services.AddHttpClient 註冊時相同 var client = _clientFactory.CreateClient("yowkoblog");
使用具型別的 HttpClient
將 client 的基本設定、使用方法與處理邏輯封裝在特定類別中
方法一:基本設定寫在建構式中
建立類別
public class YowkoBlogService { public HttpClient Client { get; } public YowkoBlogService(HttpClient client) { client.BaseAddress = new Uri(""); Client = client; } public async Task<string> GetPosts() { var response = await Client.GetAsync("/"); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); return result; } }
在
Startup.cs
使用自訂類別來註冊 HttpClientFactoryservices.AddHttpClient<YowkoBlogService>();
在需要使用 HttpClient 的 class 中透過 .NET Core 的建構式注入 (與
基本用法
相同)private readonly YowkoBlogService _yowkoBlogService; public HomeController(YowkoBlogService yowkoBlogService) { _yowkoBlogService = yowkoBlogService; }
直接呼叫自訂類別中的自訂方法即可
var result = await _yowkoBlogService.GetPosts();
方法二:基本設定寫在
Startup.cs
的註冊中自訂類別
public class YowkoBlogService { public HttpClient Client { get; } public YowkoBlogService(HttpClient client) { Client = client; } public async Task<string> GetPosts() { var response = await Client.GetAsync("/"); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); return result; } }
在
Startup.cs
使用自訂類別來註冊 HttpClientFactoryservices.AddHttpClient<YowkoBlogService>(c => { c.BaseAddress = new Uri(""); });
其他動作與
方法一
相同
方法三:將 HttpClient 完全封裝不對外公開
自訂類別
public class YowkoBlogService { private readonly HttpClient Client; public YowkoBlogService(HttpClient client) { Client = client; } public async Task<string> GetPosts() { var response = await Client.GetAsync("/"); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); return result; } }
在
Startup.cs
使用自訂類別來註冊 HttpClientFactoryservices.AddHttpClient<YowkoBlogService>(c => { c.BaseAddress = new Uri(""); });
其他動作與
方法一
相同
使用 refit 套件來產生 HttpClient
可以將 REST API 透過 interface 來呈現
安裝 refit
Package Manager
Install-Package refit
.NET CLI
dotnet add package refit
建立 interface
public interface IJsonbinClient { [Get("/_/me")] Task<User> GetMeAsync(); }
取得結果 class
public class AboutMe { public string email { get; set; } public string githubId { get; set; } public string username { get; set; } public Requests requests { get; set; } public Accounttype accountType { get; set; } public DateTime updated { get; set; } public DateTime created { get; set; } public string[] _public { get; set; } public string apikey { get; set; } public string publicId { get; set; } } public class Requests { public int PATCH { get; set; } public int POST { get; set; } public int GET { get; set; } public int PUT { get; set; } } public class Accounttype { public string name { get; set; } }
在
Startup.cs
註冊並透過 refit 動態產生 IJsonbinClient 實作services.AddHttpClient("JSonbin", c => { c.BaseAddress = new Uri("https://jsonbin.org/"); c.DefaultRequestHeaders.Add("authorization", "token {api token}"); }) .AddTypedClient(c => Refit.RestService.For<IJsonbinClient>(c));
在需要使用 HttpClient 的 class 中透過 .NET Core 的建構式注入 (與
基本用法
相同)private readonly IJsonbinClient _jsonbinClient; public HomeController(IJsonbinClient jsonbinClient) { _jsonbinClient = jsonbinClient; }
直接呼叫自訂類別中的自訂方法即可
var result =await _yowkoBlogService.GetPosts();
Outgoing Request middleware
middleware 在 .NET Core 中佔有舉足輕重的地位,許多設計都有 middleware 的影子,HttpClientFactory 便是其一,與 ASP.NET Core middleware 相似 outgoing Request middleware 可以在具名 HttpClient 上註冊與套用多個 handler 並用來管理 cache、error handling 、serialization 與 logging
新增自訂 OuterHandler 、InnerHandler 皆繼承自
DelegatingHandler
OuterHandler.cs
public class OuterHandler : DelegatingHandler { private readonly ILogger<OuterHandler> _logger; public OuterHandler(ILogger<OuterHandler> logger) { _logger = logger; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { _logger.LogInformation($"OuterHandler Call Start"); var response = await base.SendAsync(request, cancellationToken); _logger.LogInformation($"OuterHandler Call End"); return response; } }
InnerHandler.cs
public class InnerHandler : DelegatingHandler { private readonly ILogger<InnerHandler> _logger; public InnerHandler(ILogger<InnerHandler> logger) { _logger = logger; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { _logger.LogInformation($"InnerHandler Call Start"); var response = await base.SendAsync(request, cancellationToken); _logger.LogInformation($"InnerHandler Call End"); return response; } }
在
Startup.cs
的ConfigureServices
方法中註冊自訂 Handlerservices.AddTransient<OuterHandler>(); services.AddTransient<InnerHandler>(); services.AddHttpClient("yowkoblog", c => { c.BaseAddress = new Uri(""); }) // 註冊的順序會影響執行的順序 .AddHttpMessageHandler<OuterHandler>() //發出 request 時第一個執行,取回 response 時最後一個執行,對 HttpClientHandler 是較外層 .AddHttpMessageHandler<InnerHandler>(); //發出 request 時最後執行,取回 response 時第一個執行,對 HttpClientHandler 是較內層
實際結果
完整流程
重試策略:使用 Polly
過去在使用 HttpClient 取得外部資料時,最難控制的大概就是網路問題及遠端資源的狀態,一般都是自行實做重試及錯誤處理,而這樣的問題在 IHttpClientFactory 透過整合 Polly 後而變得不同了
Polly 是綜合彈性和暫時故障處理的套件,允許開發人員使用流暢及 thread-safe 的方式來達成 Retry、鎔斷、Timeout、隔離、降級退回等策略
安裝
Microsoft.Extensions.Http.Polly
NuGet 套件Install-Package Microsoft.Extensions.Http.Polly
Package Manager
Install-Package Microsoft.Extensions.Http.Polly
.NET CLI
dotnet add package Microsoft.Extensions.Http.Polly
建立 Polly 重試策略
暫時性錯誤:透過
HandleTransientHttpError
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() { return HttpPolicyExtensions .HandleTransientHttpError()//遇到 HTTP 5xx .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)//或是得到 404 NoFound .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2,retryAttempt)));//重試六次,間隔秒數為 2 的 {重試次數} 次方:重試第一次間隔 2 的 1 次方、重試第三次間隔為 2 的 3 次方 }
預先定義一般策略
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)); var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(30));
在 Startup.cs 的 ConfigureServices 方法中透過上述的 Polly 重試策略來註冊 HttpClient
透過靜態方法來註冊
services.AddHttpClient() .AddPolicyHandler(GetRetryPolicy());
動態註冊策略
services.AddHttpClient() .AddPolicyHandler(request => request.Method == HttpMethod.Get ? timeout : longTimeout);
同時使用多種策略
services.AddHttpClient() .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))//至多重試三次 .AddTransientHttpErrorPolicy(p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));//若失敗五次,就暫停嘗試 30秒
具名註冊
為策略命名且定義,可以依不同的具名 client 指定不同策略
var registry = services.AddPolicyRegistry(); registry.Add("regular", timeout); registry.Add("long", longTimeout); services.AddHttpClient() .AddPolicyHandlerFromRegistry("regular");
實際效果
原本無法成功取得,重試第三次後正常
實作 jitter 策略
jitter 是讓重試間隔加入隨機的時間,避免大量重試行為同時發生
Random jitterer = new Random(); Policy .Handle<HttpResponseException>() //預設重試間隔為 2 的 {重試次數} 次方秒,再加入一個隨機時間以錯開重試動作 .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) + TimeSpan.FromMilliseconds(jitterer.Next(0, 100)));
心得
從 .NET Core 與 ASP.NET Core 問世以來,我不時會被官方文件搞混,尤其是這次的主角 HttpClientFactory 我特別有感覺:就是文件常常會有兩者混用的問題,以 HttpClientFactory 為例,到底是 .NET Core 2.1 加入還是 ASP.NET Core 2.1 加入,目標到底是 .NET Core 還是 ASP.NET Core
.NET Core
ASP.NET Core
這篇筆記關於 HttpClient 的部份雖然還不完整 (缺了 HttpMessageHandler、logging、lifetime 管理),但因為已經拖快兩個月了 最後決定先完成基本常用功能的介紹其他功能留在之後再補,不然我怕筆記可能永遠只會是草稿了 XD
參考資料
- HttpClientFactory in ASP.NET Core 2.1 (Part 1)
- The Outgoing Request Middleware Pipeline with Handlers
- Initiate HTTP requests
- Use HttpClientFactory from .NET 4.6.2
- Use HttpClientFactory to implement resilient HTTP requests
- Singleton HttpClient doesn’t respect DNS changes
- Implement HTTP call retries with exponential backoff with HttpClientFactory and Polly policies
- 探討 HttpClient 可能的問題
- HttpClient 無法反應 DNS 異動的解決方式
- 在 ASP.NET MVC 5 中使用 ASP.NET Core Dependency Injection 與 HttpClientFactory
文章作者 Yowko Tsai
上次更新 2021-08-25
授權合約
本部落格 (Yowko's Notes) 所有的文章內容(包含圖片),任何轉載行為,必須通知並獲本部落格作者 (Yowko Tsai) 的同意始得轉載,且轉載皆須註明出處與作者。
Yowko's Notes 由 Yowko Tsai 製作,以創用CC 姓名標示-非商業性-相同方式分享 3.0 台灣 授權條款 釋出。