使用者向伺服器傳送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/