組合出好維護的 Http Request 這件事一直困擾著我,最近發現了一枚小工具 Flurl,使用 Flent 語法結構組合出維護性高的 Http Request
開發環境
- Rider 2020.3.2
- .NET 5 / .NET Framework 4.8
- Flurl 3.0.1
Flurl 是甚麼?
Flurl 專案使用 Fluent Pattern (方法鏈) 設計 API,針對 string / Uri 兩個型別進行擴充,主要有以下 Library
- Flurl:用 Fluent 優雅的組合 Url
- Flurl.Http:發送 Http Request,底層使用 HttpClient
- HttpTest:測試
專案:https://github.com/tmenier/Flurl
從官網的簡介一眼就能看的出來用途
實作
Server
新增一個 Web API / .NET5 專案
然後將範本產生出的代碼改成以下
[HttpGet]
public IActionResult Get(int lessTemperature)
{
var rng = new Random();
var sources = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
var content = sources.Where(p => p.TemperatureC < lessTemperature).ToList();
var result = new ObjectResult(content)
{
StatusCode = 200,
};
return result;
}
這個只是為了讓 Client 測試,你可以選擇用 Web API 2 / .NET Framework 4.8
Client
新一個 WinFrom / .NET 5 的專案並命名為 Client.NET5
在 Terminal 輸入以下
dotnet add Client.NET5 package Flurl --version 3.0.1
dotnet add Client.NET5 package Flurl.Http --version 3.0.1
在 WinForm 使用 HttpClient 呼叫 Server 的 API,以往的寫法如下
public partial class Form1 : Form
{
private static readonly string BasicUrl = "https://localhost:44333/";
private static readonly HttpClient Client;
static Form1()
{
Client = new HttpClient
{
BaseAddress = new Uri(BasicUrl)
};
}
public Form1()
{
this.InitializeComponent();
}
}
用 HttpClient.GetAsync 取得 Web API 資源
private async void button1_Click(object sender, EventArgs e)
{
var url = "WeatherForecast?lessTemperature=11";
var response = await Client.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
MessageBox.Show(content);
}
換成 Flurl 之後就變成這樣,為了演示我把組合 Url 跟送出 Http Request 的寫法分開
private async void button2_Click(object sender, EventArgs e)
{
var url = "WeatherForecast";
var requestUrl = BasicUrl.AppendPathSegment(url)
.SetQueryParam("lessTemperature", 11);
var content = await requestUrl.GetAsync().ReceiveString();
MessageBox.Show(content);
}
合併之後變得更好閱讀了
private async void Get()
{
var url = "WeatherForecast";
var content = await BasicUrl.AppendPathSegment(url)
.SetQueryParam("lessTemperature", 11)
.GetAsync()
.ReceiveString()
;
MessageBox.Show(content);
}
使用注意事項
Non-2XX Status
Flurl.Http 預設針對非 200 系列的 HttpStatus 會拋出 Flurl.Http.FlurlHttpException 例外,所以得針對它攔截例外;假如非 200 的錯誤不希望拋出例外的話,可以這樣做
url.AllowHttpStatus("400-404,6xx").GetAsync();
url.AllowAnyHttpStatus().GetAsync();
更多的資源請參考:https://flurl.dev/docs/error-handling/
Singleton HttpClient
底層所依賴的 HttpClent 遵守一個服務使用相同的實例,避免通訊耗盡
DNS Change
針對及時反應 DNS 的異動進行處理,在應用程式啟動時設定:
通過 FlurlHttp 的全域配置
FlurlHttp.Configure(settings => settings.ConnectionLeaseTimeout = TimeSpan.FromMinutes(2));
參考:https://github.com/tmenier/Flurl/issues/222
.NET 框架底下可以使用的物件有以下
.NET Framework 2.0 以後,使用 ServicePoint.ConnectionLeaseTimeout
ServicePointManager.FindServicePoint(new Uri(BasicUrl))
.ConnectionLeaseTimeout = (int) TimeSpan.FromMinutes(10).TotalMilliseconds;
ServicePointManager.DnsRefreshTimeout = (int)TimeSpan.FromMinutes(10).TotalMilliseconds;
在 .NET Core 2.1 以後使用 SocketsHttpHandler.PooledConnectionLifetime
搭配 Flurl.Http + SocketsHttpHandler
public class ConnectionLifetimeHttpClientFactory : DefaultHttpClientFactory
{
public override HttpMessageHandler CreateMessageHandler()
{
var socketsHandler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 10
};
return socketsHandler;
}
}
套用 ConnectionLifetimeHttpClientFactory
FlurlHttp.Configure(settings => settings.HttpClientFactory = new ConnectionLifetimeHttpClientFactory());
參考:https://github.com/tmenier/Flurl/issues/418
專案位置
https://github.com/yaochangyu/sample.dotblog/tree/master/Http%20Client/Lab.FluentHttpRequest
參考
更多的資源請參考:https://flurl.dev/docs/client-lifetime/#httpclient-lifetime-management
DNS Issues:https://github.com/tmenier/Flurl/issues?q=dns
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET