「譯」使用 System.Net.Http.Json 在 HttpClient 中高效處理Json


在這篇文章,我將介紹一個名為 System.Net.Http.Json 的擴展庫,它最近添加到了 .NET 中,我們看一下這個庫能夠給我們解決什么問題,今天會介紹下如何在代碼中使用。

在此之前我們是如何處理

JSON是一種普遍和流行的串行化格式數據來發送現代web api,我經常在我的項目中使用HttpClient 調用外部資源, 當 content type 是 “application/json”, 我拿到Json的響應內容后,我需要手動處理響應,通常會驗證響應狀態代碼是否為200,檢查內容是不是為空,然后再試圖從響應內容流反序列化

如果我們使用 Newtonsoft.Json, 代碼可能是像下邊這樣

private static async Task<User> 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<User>(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來支持數據的高效反序列化,這是一個方便的庫,我用了幾年了

如果我們在項目中使用這個庫,上面的代碼可以減少一些

private static async Task<User> 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<User>();
    }
    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 相比更簡潔

private static async Task<User> 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<User>(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 合並到項目,准備接下來的 Blazor,現在已經是.NET 5 BCL(基礎庫)的一部分,所以這是我為什么一直在提 System.Net.Http.Json,現在你可以在 Nuget 下載安裝,接下來,我會探討下支持的主要的API和使用場景。

使用 HttpClient 發送和接收Json數據

下邊的一些代碼和示例我已經上傳到了這里 https://github.com/stevejgordon/SystemNetHttpJsonSamples

這第一步是包添加到您的項目,你可以使用NuGet包管理器或者下邊的命令行安裝

dotnet add package System.Net.Http.Json

使用 HttpClient 獲取Json數據

讓我們先看一個擴展方法HttpClient,這很簡單

private static async Task<User> GetJsonHttpClient(string uri, HttpClient httpClient)
{
    try
    {
        return await httpClient.GetFromJsonAsync<User>(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/<something>+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 提供的擴展方法

private static async Task<User> 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<User>();
        }
        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數據,看一下下邊我們的兩種實現

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

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

最后

歡迎掃碼關注我們的公眾號,專注國外優秀博客的翻譯和開源項目分享,也可以添加QQ群 897216102


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM