用戶向服務器發送HTTP請求應用程序頁面是一種非常可能的情況。當我們的應用程序處理請求時,用戶可以從該頁面離開。在這種情況下,我們希望取消HTTP請求,因為響應對該用戶不再重要。當然,這只是實際應用程序中可能發生的許多情況中的一種,我們希望取消請求。因在本文中,將學習如何使用CancellationToken取消客戶端中的HTTP請求。
使用CancellationToken取消使用HttpClient發送的請求
在介紹中,我們指出,如果用戶從頁面離開,他們就不再需要響應,因此取消該請求是一個很好的做法。但還有更多的原因。HttpClient正在處理異步任務,因此取消一個不再需要的任務將釋放我們用來運行任務的線程。這意味着該線程將被返回到一個線程池,在該線程池中,該線程可以用於其他一些工作。這肯定會提高應用程序的可伸縮性。
當然,我們不能就這樣取消請求。要執行這樣的操作,我們必須使用CancellationTokenSource和CancellationToken。
我們使用CancellationTokenSource來創建CancellationToken,並通知所有CancellationToken的消費者請求已被取消。在我們的例子中,HttpClient將使用CancellationToken並監聽通知。一旦收到請求取消通知,我們將使用HttpClient取消該請求。
使用HttpClient實現CancellationToken
我們要做的第一件事是為這個示例創建一個新service:
public class HttpClientCancellationService : IHttpClientServiceImplementation { private static readonly HttpClient _httpClient = new HttpClient(); private readonly JsonSerializerOptions _options; public HttpClientCancellationService() { _httpClient.BaseAddress = new Uri("https://localhost:5001/api/"); _httpClient.Timeout = new TimeSpan(0, 0, 30); _httpClient.DefaultRequestHeaders.Clear(); _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; } public async Task Execute() { throw new NotImplementedException(); } }
我們創建了一個HttpClient實例並為其提供配置。同樣,對JSON序列化也做同樣的事情。在下一篇文章中,我們將學習關於HttpClientFactory的知識,並了解如何將這個配置移動到一個單獨的位置,而不會在所有文件中重復它,還將學習如何解決HttpClient可能導致的問題。現在,我們將保持現狀。
現在,讓我們添加一個新方法來獲取所有的公司數據:
private async Task GetCompaniesAndCancel() { using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync>(stream, _options); } }
這也是上一篇文章中熟悉的代碼,這里不做解釋。現在,假設我們想取消這個請求。正如之前說過的,要取消一個請求,我們需要CancellationTokenSource。那么,讓我們來實現它:
private async Task GetCompaniesAndCancel() { var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.CancelAfter(2000); using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync>(stream, _options); } }
這里,我們創建了一個新的cancellationTokenSource對象。在創建對象之后,我們希望取消請求。這通常是由用戶執行的——通過按下取消按鈕或離開一個頁面,要取消請求,可以使用兩個方法:Cancel()和CancelAfter(),前者會立即取消請求。在本例中,我們使用CancelAfter方法並提供兩秒作為參數。最后,我們必須通知HttpClient取消操作。為此,我們提供一個取消令牌作為GetAsync的附加參數。
我們現在就可以測試一下。
測試取消請求
在啟動應用程序之前,我們需要確保應用程序啟動時調用了我們的方法。為此,我們必須修改Execute方法:
public async Task Execute() { await GetCompaniesAndCancel(); }
同時,我們必須在Program類中注冊這個服務:
private static void ConfigureServices(IServiceCollection services) { //services.AddScoped HttpClientCrudService>(); //services.AddScoped HttpClientPatchService>(); //services.AddScoped HttpClientStreamService>(); services.AddScoped HttpClientCancellationService>(); }
現在,讓我們啟動這兩個應用程序:
可以看到我們的請求被取消了。
通過共享CancellationToken改進解決方案
目前的實現對於我們的學習示例非常有用。但在實際的應用程序中,我們希望能夠通過將令牌傳遞給所有請求達到取消不同請求的目的。這將允許在需要時取消所有這些請求。此外,我們希望能夠從應用程序的不同部分訪問這個CancellationTokenSource,例如當用戶單擊取消按鈕或從頁面離開時。在這種情況下,我們不想把CancellationTokenSource隱藏在單個方法中。
private static readonly HttpClient _httpClient = new HttpClient(); private readonly JsonSerializerOptions _options; private readonly CancellationTokenSource _cancellationTokenSource; public HttpClientCancellationService() { _httpClient.BaseAddress = new Uri("https://localhost:5001/api/"); _httpClient.Timeout = new TimeSpan(0, 0, 30); _httpClient.DefaultRequestHeaders.Clear(); _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; _cancellationTokenSource = new CancellationTokenSource(); }
這里,我們創建了一個CancellationTokenSource只讀變量,並在構造函數中實例化它。然后,我們要修改Execute方法:
public async Task Execute() { _cancellationTokenSource.CancelAfter(2000); await GetCompaniesAndCancel(_cancellationTokenSource.Token); }
在這個方法中,我們調用CancelAfter方法來指定要取消請求的周期,並將令牌傳遞給GetCompaniesAndCancel方法。當然,我們還必須修改GetCompaniesAndCancel方法:
private async Task GetCompaniesAndCancel(CancellationToken token) { using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead, token)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync>(stream, _options); } }
此時,我們的方法接受令牌並使用它來偵聽取消通知。現在,可以重新啟動API和客戶端應用。
可以繼續看看如何在我們的應用程序中處理這個異常。
處理TaskCanceledException
如果我們想處理應用程序在取消請求后拋出的異常,只需將請求封裝在try-catch塊中:
private async Task GetCompaniesAndCancel(CancellationToken token) { try { using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead, token)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync>(stream, _options); } } catch (OperationCanceledException ocex) { Console.WriteLine(ocex.Message); } }
我們看到應用程序拋出了TaskCanceledException,但是因為它繼承了OperationCanceledException類,所以我們可以使用這個類來捕獲異常。當然,在catch塊中,我們可以執行許多操作,但對於本例來說,只記錄消息就足夠了。現在,讓我們啟動這兩個應用程序並檢查結果:
檢查響應的狀態代碼
使用我們現在的實現,如果響應不成功,我們將拋出異常。為了達到100%的准確性,EnsureSuccessStatusCode()方法將執行此操作。但在許多情況下,我們希望根據響應失敗的真正原因提示更用戶友好的消息。我們可以檢查響應的狀態碼。也就是說,這里我們將使用一種狀態代碼,並展示如何用更有意義的消息提供更好的用戶體驗。
對於本例,我們將使用HttpClientStreamService類。讓我們在這個類中創建一個新方法:
private async Task GetNonExistentCompany() { var uri = Path.Combine("companies", "F8088E81-7EFA-4E49-F824-08D8C38D155C"); using (var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync>(stream, _options); } }
現在我們對整個代碼已經很熟悉了,這里提供的Id 不存在於我們的數據庫中。因此,我們的API應該返回404。在測試它之前,我們必須修改Execute方法:
public async Task Execute() { //await GetCompaniesWithStream(); //await CreateCompanyWithStream(); await GetNonExistentCompany(); }
同時,我們必須在Program類中啟用這個服務:
private static void ConfigureServices(IServiceCollection services) { //services.AddScoped HttpClientCrudService>(); //services.AddScoped HttpClientPatchService>(); services.AddScoped HttpClientStreamService>(); //services.AddScoped HttpClientCancellationService>(); }
讓我們啟動兩個應用程序並檢查結果:
我們確實得到了404響應,但仍然拋出異常。我們可以改變這一點。
使用狀態碼
對我們的方法做一個小小的修改:
private async Task GetNonExistentCompany() { var uri = Path.Combine("companies", "F8088E81-7EFA-4E49-F824-08D8C38D155C"); using (var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)) { if(!response.IsSuccessStatusCode) { if (response.StatusCode.Equals(HttpStatusCode.NotFound)) { Console.WriteLine("The company you are searching for couldn't be found."); return; } response.EnsureSuccessStatusCode(); } var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync>(stream, _options); } }
首先檢查響應是否包含帶有IsSuccessStatusCode屬性的成功狀態代碼。
如果沒有,則顯式檢查我們想要處理的狀態代碼,在本例中是NotFound狀態代碼。在這種情況下,只需向控制台窗口寫入一條信息消息。對於所有其他不成功的狀態代碼,用EnsureSuccessStatusCode方法拋出一個異常。當然,也可以使用其他狀態代碼來擴展這個條件,但在這種情況下,最好將該邏輯提取到另一個方法中,以使該方法更具可讀性。現在,如果我們啟動應用程序:
結論
現在,我們知道了如何使用CancellationToken和CancellationTokenSource取消請求,以及如何使用CancellationTokenSource在不同的請求之間共享令牌。此外,我們還知道如何使用響應中的不同狀態代碼來防止為每個不成功的響應拋出異常。
在下一篇文章中,我們將學習更多關於HttpClientFactory的內容,並看看這種方法的優點是什么。
原文鏈接:https://code-maze.com/canceling-http-requests-in-asp-net-core-with-cancellationtoken/