在ASP.NET Core中用HttpClient(一)——獲取資料和內容

碼農譯站發表於2021-03-06

在本文中,我們將學習如何在ASP.NET Core中整合和使用HttpClient。在學習不同HttpClient功能的同時使用Web API的資源。如何從Web API獲取資料,以及如何直接使用HttpRequestMessage類來實現這些功能。在以後的文章中,我們將學習如何傳送POST、PUT和DELETE請求,以及如何使用HttpClient傳送PATCH請求。

要下載原始碼,可以訪問https://github.com/CodeMazeBlog/httpclient-aspnetcore/tree/fetching-data-with-httpclient以獲取專案。

概述

如果你開啟HttpClient儲存庫的主分支,您將發現兩個專案:CompanyEmployees和CompanyEmployees. client。第一個專案是ASP.NET Core Web API專案,它將是本教程的伺服器端專案。它由幾個專案組成:

API專案對於我們的文章和整個系列來說也不是最重要的。重要的是它使用SQL資料庫,所以你需要做的就是修改appsettings中的連線字串。然後執行Update-Migration命令。所有需要的資料都將被遷移到資料庫中。

然後,還有客戶端應用程式——ASP.NET Core控制檯應用程式。它有一個單獨的服務HttpClientCrudService,我們將在本文中修改它,一個單獨的介面IHttpClientServiceImplementation,所有HttpClient服務都將從它繼承,資料傳輸類和一個修改過的Program類:

讓我們來看看Program類的當前程式碼

class Program
{
    static async Task Main(string[] args)
    {
        var services = new ServiceCollection();
        ConfigureServices(services);
        var provider = services.BuildServiceProvider();
        try
        {
            await provider.GetRequiredService<IHttpClientServiceImplementation>()
                .Execute();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Something went wrong: {ex}");
        }
    }
    private static void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IHttpClientServiceImplementation, HttpClientCrudService>();
    }
}

這沒有什麼特別的。我們準備服務集合,將服務新增到IOC中,並從服務中執行預設方法。當我們在整個教程中新增不同的服務時,我們將擴充套件ConfigureServices方法。

關於HttpClient

我們不會深入研究關於HttpClient的理論,因為我們將從示例中學到很多東西,但是讓我們看看一些基礎知識。

HttpClient是一個類,它允許我們傳送HTTP請求,並從指定URI接收HTTP響應。我們可以使用這個類傳送各種HTTP請求,如GET、POST、PUT、DELETE、PATCH並來自伺服器的響應。

HttpClient使用HTTP訊息處理程式傳送請求並獲取響應。這是預設訊息處理程式的主要工作。如果我們閱讀微軟的文件,我們會讀到.NET Framework和.NET Core 2.0以及更早版本的預設是HttpClientHander。但是在.NET Core 2.1和更高版本中,預設的是SocketsHttpHandler。

但是,HttpClient不只是使用一個訊息處理程式。我們可以附加多個訊息處理程式並建立管道。有些處理程式只能操作請求的頭,有些可以處理超時,等等。

在ASP.NET Core中使用HttpClient傳送GET請求

現在,讓我們從修改HttpClientCrudService 類開始:

public class HttpClientCrudService : IHttpClientServiceImplementation
{
    private static readonly HttpClient _httpClient = new HttpClient();
    public HttpClientCrudService()
    {
        _httpClient.BaseAddress = new Uri("https://localhost:5001/api/");
        _httpClient.Timeout = new TimeSpan(0, 0, 30);
    }
    public async Task Execute()
    {
    }
}

這裡,我們建立了一個HttpClient屬性,初始化它,並在建構函式中進行配置,我們設定了API的URI及超時時間。當然,我們可以在這個配置中找到更多的屬性來使用,但是現在,這就足夠了。稍後,當我們開始學習HttpClientFactory時,我們將把這些配置移到別的地方。

現在,我們可以在這個類中新增一個新方法:

public async Task GetCompanies()
{
    var response = await _httpClient.GetAsync("companies");
    response.EnsureSuccessStatusCode();
    var content = await response.Content.ReadAsStringAsync();
    var companies = JsonSerializer.Deserialize<List<CompanyDto>>(content, _options);
}

在這個方法中,我們使用HttpClient中的GetAsync方法,並傳入地址。為了確保響應是成功的,我們呼叫EnsureSuccessStatusCode方法。一旦確定有成功狀態碼的響應,就可以將響應的內容作為字串讀取。最後,我們對資料列表進行反序列化。如你所見,我們使用了一個JsonSerializerOptions型別的引數,所以讓我們將它新增到我們的類中,並在Execute方法中呼叫這個方法:

private readonly JsonSerializerOptions _options; 

public HttpClientCrudService()
{
    _httpClient.BaseAddress = new Uri("https://localhost:5001/api/");
    _httpClient.Timeout = new TimeSpan(0, 0, 30);

    _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
}

public async Task Execute()
{
    await GetCompanies();
}

我們為JsonSerializer設定不區分大小寫的反序列化選項。沒有它,我們的響應就不會被正確地反序列化。

現在,我們可以在GetCompanies方法中新增一個斷點,啟動Web API專案,然後啟動客戶端:

正如我們所看到的,我們在companies變數中得到了結果。

支援不同的響應格式

在本例中,我們接收到一個JSON作為預設的響應格式。我們的API預設支援這種型別。但有些API不預設為JSON,它們可能支援XML作為預設的響應格式或任何其他格式。在這種情況下,我們的邏輯就行不通了。

除了JSON之外,我們的API還支援XML響應格式。讓我們看看如何在客戶端應用程式中明確地要求格式。

首先,Http請求和響應都包含一組頭,我們可以使用這些頭在客戶端和伺服器應用程式之間傳遞附加資訊。HTTP請求的通用頭是Accept。我們使用這個頭告訴伺服器客戶端將接受哪種媒體型別:accept: application/json, text/xml。

所以,讓我們看看如何在我們的請求中設定頭:

public HttpClientCrudService()
{
    _httpClient.BaseAddress = new Uri("https://localhost:5001/api/");
    _httpClient.Timeout = new TimeSpan(0, 0, 30);
    _httpClient.DefaultRequestHeaders.Clear();
    _httpClient.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));
    _httpClient.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("text/xml"));

    _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
}

這裡,我們使用DefaultRequestHeaders屬性並將其清除。然後,我們使用Accept屬性,由於它是一個集合,所以我們新增了兩個MediaTypeWithQualityHeaderValue物件。對於第一個物件,我們支援JSON格式,對於第二個物件,我們支援XML格式。為此,我們需要新增一個新的using語句:

using System.Net.Http.Headers;

現在,如果有一個像這樣的配置,必須在方法中新增一些額外的程式碼,以決定如何反序列化響應:

public async Task GetCompanies()
{
    var response = await _httpClient.GetAsync("companies");
    response.EnsureSuccessStatusCode();

    var content = await response.Content.ReadAsStringAsync();
            
    var companies = new List<CompanyDto>();

    if(response.Content.Headers.ContentType.MediaType == "application/json")
    {
        companies = JsonSerializer.Deserialize<List<CompanyDto>>(content, _options);
    }
    else if(response.Content.Headers.ContentType.MediaType == "text/xml")
    {
        var doc = XDocument.Parse(content);
        foreach (var element in doc.Descendants())
        {
            element.Attributes().Where(a => a.IsNamespaceDeclaration).Remove();
            element.Name = element.Name.LocalName;
        }

        var serializer = new XmlSerializer(typeof(List<CompanyDto>));
        companies = (List<CompanyDto>)serializer.Deserialize(new StringReader(doc.ToString()));
    }
}

由於我們同時支援JSON和XML格式,我們必須檢查哪個ContentType被應用到響應。如果是JSON,我們只需執行標準的反序列化。但如果是XML,我們將內容解析為XDocument。最後,我們建立一個新的XmlSerializer並反序列化XDocument。

此時,如果我們啟動兩個應用程式,並在方法中放置一個斷點,我們將看到我們的預設格式是JSON:

HttpClient中的優先順序

在我們的Accept頭設定中,我們支援兩種具有相同優先順序的格式。優先順序的最大值為1。但是,我們可以為這兩個標頭檔案中的一個設定較低的首選項——值必須在0到1之間。有較高值的請求將有更高優先順序。

所以,讓我們在建構函式中降低JSON Accept頭的優先順序:

_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Accept.Add(
    new MediaTypeWithQualityHeaderValue("application/json", 0.9));
_httpClient.DefaultRequestHeaders.Accept.Add(
    new MediaTypeWithQualityHeaderValue("text/xml"));

正如我們所看到的,MediaTypeWithQualityHeaderValue建構函式接受另一個引數。我們將它的值設定為0.9。因為我們沒有為XML Accept頭新增任何值,所以預設值是1。現在,如果我們開始我們的應用程式,我們會發現XML是優先順序更高的格式:

因此,執行將跳過這一部分並執行我們的XML反序列化。讓我們在XDocument解析之前檢查響應體:

然後,讓我們在解析操作之後檢查doc變數:

我們可以看出區別。在解析操作之後,反序列化就成功完成了:

我們已經看到了如何在請求中向HTTP Accept頭新增首選項。但現在問題出現了。如果我們使用部分請求頭,該怎麼辦?

使用HttpRequestMessage類傳送HTTP請求

在這個實現中,我們對每個請求使用相同的頭配置。因此,如果想傳送預設為JSON格式的HTTP請求,我們不能使用這個類中的HTTP配置來實現。這是因為我們將XML格式設定為預設格式。這意味著我們必須提供一個不同的解決方案。

如果我們仔細考慮這一點,我們可以得出結論:BaseAddress和Timeout屬性與HttpClient相關,但Accept頭的屬性連線到請求本身。同樣,當我們使用GetAsync方法時,它在內部使用GET HTTP方法建立了一個新的HttpRequestMessage。也就是說,我們可以建立自己的HttpRequestMessage併為該請求提供報頭。

最佳實踐是在HttpClient例項上設定預設配置,在HTTP請求本身上設定請求配置。當然,如果我們總是希望使用JSON格式作為Accept報頭,我們可以在HttpClient設定它。

實現

現在,讓我們看看如何使用HttpRequestMessage類來實現HTTP請求。

首先,讓我們從建構函式中移除accept標頭檔案的配置:

public HttpClientCrudService()
{
    _httpClient.BaseAddress = new Uri("https://localhost:5001/api/");
    _httpClient.Timeout = new TimeSpan(0, 0, 30);
    _httpClient.DefaultRequestHeaders.Clear();
    _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
}

然後,我們可以將GetCompanies方法恢復到之前:

public async Task GetCompanies()
{
    var response = await _httpClient.GetAsync("companies");
    response.EnsureSuccessStatusCode();
    var content = await response.Content.ReadAsStringAsync();        
    var companies = JsonSerializer.Deserialize<List<CompanyDto>>(content, _options);
}

最後,我們可以新增新方法:

public async Task GetCompaniesWithXMLHeader()
{
    var request = new HttpRequestMessage(HttpMethod.Get, "companies");
    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml"));
    var response = await _httpClient.SendAsync(request);
    response.EnsureSuccessStatusCode();
    var content = await response.Content.ReadAsStringAsync();
    var doc = XDocument.Parse(content);
    foreach (var element in doc.Descendants())
    {
        element.Attributes().Where(a => a.IsNamespaceDeclaration).Remove();
        element.Name = element.Name.LocalName;
    }
    var serializer = new XmlSerializer(typeof(List<CompanyDto>));
    var companies = (List<CompanyDto>)serializer.Deserialize(new StringReader(doc.ToString()));
}

因此,我們使用HttpRequestMessage類建立了一個新請求,該類提供了HTTP Method作為引數和API的地址。然後,向請求新增報頭,並呼叫SendAsync方法傳送請求。提取內容之後,重複前面方法中所做的步驟。我們還要做最後一件事。

讓我們確保一旦客戶端應用程式啟動,這個方法就會被呼叫:

public async Task Execute(){    //await GetCompanies();    await GetCompaniesWithXMLHeader();}

正如我們之前所做的,我們將在這個方法中放置一個斷點並啟動兩個應用程式:

如你所見,我們得到的結果與前面相同,但這次我們使用HttpRequestMessage的單獨方法傳送帶有XML Accept頭的HTTP請求。

結論

在本文中,我們討論了HttpClient,以及如何在我們的ASP.NET Core中使用它處理來自Web API的資料。

原文連結:https://code-maze.com/fetching-data-with-httpclient-in-aspnetcore/

 

相關文章