「譯」使用 System.Net.Http.Json 高效處理Json
阿新 • • 發佈:2021-01-02
在這篇文章,我將介紹一個名為 System.Net.Http.Json 的擴充套件庫,它最近新增到了 .NET 中,我們看一下這個庫能夠給我們解決什麼問題,今天會介紹下如何在程式碼中使用。
### 在此之前我們是如何處理
JSON是一種普遍和流行的序列化格式資料來發送現代web api,我經常在我的專案中使用HttpClient 呼叫外部資源, 當 content type 是 “application/json”, 我拿到Json的響應內容後,我需要手動處理響應,通常會驗證響應狀態程式碼是否為200,檢查內容是不是為空,然後再試圖從響應內容流反序列化
如果我們使用 Newtonsoft.Json, 程式碼可能是像下邊這樣
```csharp
private static async Task StreamWithNewtonsoftJson(string uri, HttpClient httpClient)
{
using var httpResponse = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
httpResponse.EnsureSuccessStatusCode(); // throws if not 200-299
if (httpResponse.Content is object && httpResponse.Content.Headers.ContentType.MediaType == "application/json")
{
var contentStream = await httpResponse.Content.ReadAsStreamAsync();
using var streamReader = new StreamReader(contentStream);
using var jsonReader = new JsonTextReader(streamReader);
JsonSerializer serializer = new JsonSerializer();
try
{
return serializer.Deserialize(jsonReader);
}
catch(JsonReaderException)
{
Console.WriteLine("Invalid JSON.");
}
}
else
{
Console.WriteLine("HTTP Response was invalid and cannot be deserialised.");
}
return null;
}
```
雖然上面沒有大量的程式碼, 但是我們從外部服務接收JSON資料需要都編寫這些,在微服務環境中,這可能是在很多地方,不同的服務。
大家可能通常也會把 Json 序列化成 String,在 HttpClient 的 HttpContent 中呼叫`GetStringAsync` `ReadAsStringAsync`,可以直接使用 Newtonsoft.Json 和 System.Text.Json,現在的一個問題是我們需要多分配一個包含整個Json 資料的 String,這樣會存在浪費,因為我們看上面的程式碼已經有一個可用的響應流,可以直接反序列化到實體,通過使用流,也可以進一步提高效能,在我的另一篇文章裡, 可以利用HttpCompletionOption來改善HttpClient效能。
如果您在過去在專案中使用過 HttpClient 來處理返回的Json資料,那麼您可能已經使用了`Microsoft.AspNet.WebApi.Client`。我在過去使用過它,因為它提供了有用的擴充套件方法來支援從HttpResponseMessage上的內容流進行高效的JSON反序列化,這個庫依賴於Newtonsoft.Json檔案並使用其基於流的API來支援資料的高效反序列化,這是一個方便的庫,我用了幾年了
如果我們在專案中使用這個庫,上面的程式碼可以減少一些
```csharp
private static async Task WebApiClient(string uri, HttpClient httpClient)
{
using var httpResponse = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
httpResponse.EnsureSuccessStatusCode(); // throws if not 200-299
try
{
return await httpResponse.Content.ReadAsAsync();
}
catch // Could be ArgumentNullException or UnsupportedMediaTypeException
{
Console.WriteLine("HTTP Response was invalid or could not be deserialised.");
}
return null;
}
```
最近.NET 團隊引入了一個內建的JSON庫 `System.Text.Json`,這個庫是使用了最新的 .NET 的效能特性, 比如 Span, 低開銷, 能夠快速序列化和反序列化, 並且在.NET Core 3.0 整合到了 BCL(基礎庫), 所以你不需要引用一個額外的包在專案中
今天,我更傾向於使用 `System.Text.Json`,主要是在流處理,程式碼跟上面 Newtonsofe.Json 相比更簡潔
```csharp
private static async Task StreamWithSystemTextJson(string uri, HttpClient httpClient)
{
using var httpResponse = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
httpResponse.EnsureSuccessStatusCode(); // throws if not 200-299
if (httpResponse.Content is object && httpResponse.Content.Headers.ContentType.MediaType == "application/json")
{
var contentStream = await httpResponse.Content.ReadAsStreamAsync();
try
{
return await System.Text.Json.JsonSerializer.DeserializeAsync(contentStream, new System.Text.Json.JsonSerializerOptions { IgnoreNullValues = true, PropertyNameCaseInsensitive = true });
}
catch (JsonException) // Invalid JSON
{
Console.WriteLine("Invalid JSON.");
}
}
else
{
Console.WriteLine("HTTP Response was invalid and cannot be deserialised.");
}
return null;
}
```
因為我在專案中減少了第三方庫的依賴,並且有更好的效能,我更喜歡用 `System.Text.Json`,雖然這塊程式碼非常簡單,但是還有更好的方案,從簡潔程式碼的角度來看,到現在為止最好的選擇是使用 `Microsoft.AspNet.WebApi.Client` 提供的擴充套件方法。
### System.Net.Http.Json 介紹
我從今年2月份一直在關注這個庫,以及首次在 github 顯示的設計文件和問題,這些需求和建議的API都可以在設計文件中找到。
> 客戶端從網路上對 JSon 內容序列化和反序列化是非常常見的操作,特別是即將到來的Blazor環境,現在,傳送資料到服務端,需要寫多行繁瑣的程式碼,對使用者來說非常不方便,我們想對 HttpClient 擴充套件,允許做這些操作就像呼叫單個方法一樣簡單
你可以在github閱讀完整的設計文件,團隊希望構建一個更加方便的獨立釋出的庫,來在 HttpClient 和 System.Text.Json 使用,也可以在Blazor 中使用這些API。
這些初始化的工作已經由微軟的 [David Cantu ](https://github.com/Jozkee "David Cantu ") 合併到專案,準備接下來的 Blazor,現在已經是.NET 5 BCL(基礎庫)的一部分,所以這是我為什麼一直在提 `System.Net.Http.Json`,現在你可以在 Nuget 下載安裝,接下來,我會探討下支援的主要的API和使用場景。
### 使用 HttpClient 傳送和接收Json資料
下邊的一些程式碼和示例我已經上傳到了這裡 [https://github.com/stevejgordon/SystemNetHttpJsonSamples](https://github.com/stevejgordon/SystemNetHttpJsonSamples "https://github.com/stevejgordon/SystemNetHttpJsonSamples")
這第一步是包新增到您的專案,你可以使用NuGet包管理器或者下邊的命令列安裝
```csharp
dotnet add package System.Net.Http.Json
```
### 使用 HttpClient 獲取Json資料
讓我們先看一個擴充套件方法HttpClient,這很簡單
```csharp
private static async Task GetJsonHttpClient(string uri, HttpClient httpClient)
{
try
{
return await httpClient.GetFromJsonAsync(uri);
}
catch (HttpRequestException) // Non success
{
Console.WriteLine("An error occurred.");
}
catch (NotSupportedException) // When content type is not valid
{
Console.WriteLine("The content type is not supported.");
}
catch (JsonException) // Invalid JSON
{
Console.WriteLine("Invalid JSON.");
}
return null;
}
```
在程式碼第5行,傳入泛型呼叫 `GetFromJsonAsync` 來反序列化 Json 內容,方法傳入一個uri地址,這是我們所需要的,我們操作了一個 Http Get請求到服務端,然後獲取響應反序列化到 User 實體,這很簡潔,另外上邊有詳細的異常處理程式碼,在各種條件下來丟擲異常
跟最上面的程式碼一樣,使用 `EnsureSuccessStatusCode` 來判斷狀態碼是否成功,如果狀態碼在 200-299 之外,會丟擲異常
並且這個庫還會檢查是不是有效的媒體型別,比如 `application/json`, 如果媒體型別錯誤,將丟擲 NotSupportedException,這裡的檢查比我上邊手動處理的程式碼更加完整,如果媒體型別不是 `application/json`,則會對值進行基於Span的解析, 所以 `application/+json` 也是有效的格式
這種格式是現在經常使用的,另外一個例子,可以發現這個庫對於標準和細節的處理,`RFC7159` 標準 定義一種攜帶機器可讀的HTTP響應中的錯誤,比如 `application/problem+json`, 我手寫的程式碼沒有處理和匹配這些,因為 `System.Net.Http.Json` 已經做了這些工作
在內部,`ResponseHeadersRead HttpCompletionOption` 用來提升效率,我最近的文章有這個的介紹,這個庫已經處理好了 `HttpResponseMessage`,使用這個Option是必需的
### 轉碼
最後這個庫的實現細節, 包括支援程式碼轉換返回的資料,如果不是utf-8,utf-8應該在絕大多數情況下的標準,然而,如果 content-type 報頭中包含的字符集標識不同的編碼,將使用TranscodingStream 嘗試反序列化成 `utf-8`
### 從HttpContent 處理Json
在某些情況下,您可能想要傳送請求的自定義 Header , 或者你想反序列化之前檢查 Response Header,這也可以使用 `System.Net.Http.Json` 提供的擴充套件方法
```csharp
private static async Task GetJsonFromContent(string uri, HttpClient httpClient)
{
var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.TryAddWithoutValidation("some-header", "some-value");
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
// perhaps check some headers before deserialising
try
{
return await response.Content.ReadFromJsonAsync();
}
catch (NotSupportedException) // When content type is not valid
{
Console.WriteLine("The content type is not supported.");
}
catch (JsonException) // Invalid JSON
{
Console.WriteLine("Invalid JSON.");
}
}
return null;
}
```
### 傳送Json資料
最後一個示例我們使用 HttpClient 來發送Json資料,看一下下邊我們的兩種實現
```csharp
private static async Task PostJsonHttpClient(string uri, HttpClient httpClient)
{
var postUser = new User { Name = "Steve Gordon" };
var postResponse = await httpClient.PostAsJsonAsync(uri, postUser);
postResponse.EnsureSuccessStatusCode();
}
```
第一個方法是使用 `PostAsJsonAsync` 擴充套件方法,把物件序列化成 Json 請求到服務端,內部會建立一個 `HttpRequestMessage` 和 序列化成內容流
還有一種情況需要手動建立一個 `HttpRequestMessage`, 也許包括自定義請求頭,你可以直接建立 JsonContent
```csharp
private static async Task PostJsonContent(string uri, HttpClient httpClient)
{
var postUser = new User { Name = "Steve Gordon" };
var postRequest = new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = JsonContent.Create(postUser)
};
var postResponse = await httpClient.SendAsync(postRequest);
postResponse.EnsureSuccessStatusCode();
}
```
在上邊的程式碼中,我們建立了一個 JsonContent, 傳入一個物件然後序列化,JsonContent 是 `System.Net.Http.Json` 庫中的型別,內部它會使用 `System.Text.Json` 來進行序列化
### 總結
在這篇文章中,我們回顧了一些傳統的方法,可以用來從HttpResponseMessage 來反序列化物件,我們看到,當手動呼叫api來解析JSON, 我們首先需要考慮比如響應狀態是成功的, 並且是我們需要的媒體型別, `Microsoft.AspNet.WebApi.Client ` 提供的 ReadAsAsync 方法,內部是使用 Newtonsoft.Json 來基於流的反序列化
我們的結論是使用新的 `System.Net.Http.Json`, 它會使用 `System.Text.Json` 來進行Json的序列化和反序列化,不依賴於第三方庫 `Newtonsoft.Json`, 使用這個庫提供的擴充套件方法,通過很簡潔的程式碼就可以通過HttpClient 來發送和接收資料,並且有更好的效能表現,最後,你可以在這裡找到本文的一些程式碼 [https://github.com/stevejgordon/SystemNetHttpJsonSamples](https://github.com/stevejgordon/SystemNetHttpJsonSamples "https://github.com/stevejgordon/SystemNetHttpJsonSamples")
### 最後
歡迎掃碼關注我們的公眾號,專注國外優秀部落格的翻譯和開源專案分享,也可以新增QQ群 897216102