[ASP.NET Web API 2] 通過 CancellationToken 取消非同步請求

使用 Web API 專案實作取消很簡單,只要再 Action 多一個 CancellationToken 參數就可以了,試驗一下發現並沒有那麼簡單...

這次我要觀察取消的效果,所以需要用 Console App,新增 Console App 專案,然後安裝以下套件

Install-Package Microsoft.AspNet.WebApi.OwinSelfHost
Install-Package Microsoft.Owin.Diagnostics
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package NLog.Config
Install-Package Swagger-Net
Install-Package EntityFramework

設定 Route,Swagger

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var httpConfig = new HttpConfiguration();
 
        Route.Configure(httpConfig);
        Swagger.Configure(httpConfig);
 
        // Use the extension method provided by the WebApi.Owin library:
        app.UseWebApi(httpConfig);
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.Cancel/Server1/Configurations/Startup.cs

 

預設頁面導到 swagger

public class Route
{
    public static void Configure(HttpConfiguration httpConfig)
    {
        httpConfig.Routes.MapHttpRoute(
                                       "swagger_root",
                                       "",
                                       null,
                                       null,
                                       new RedirectHandler(message => message.RequestUri.ToString(), "swagger"));
 
        httpConfig.Routes.MapHttpRoute(
                                       "DefaultApi",
                                       "api/{controller}/{id}",
                                       new {id = RouteParameter.Optional});
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.Cancel/Server1/Configurations/Route.cs

 

安裝 swagger-net 後,就會有一個 SwaggerConfig 檔案,我修改 class name、method name,主要是套用 CancellationTokenOperationFilter,它是讓 swagger ui 不要產生 CancellationToken 的參數

public class Swagger
{
        public static void Configure(HttpConfiguration config)
        {
            var thisAssembly = typeof(Swagger).Assembly;

            config.EnableSwagger(c =>
                                 {
                                     c.OperationFilter<CancellationTokenOperationFilter>();
                                   
	     .....
        }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.Cancel/Server1/Configurations/Startup.cs

 

CancellationToken 用法很簡單,讓 Action 參數帶有 CancellationToken 即可,cancel.ThrowIfCancellationRequested() 用來檢查是否有取消有的話就會拋出例外,也可以搭配 cancel.IsCancellationRequested 判斷,經我測試 cancel.IsCancellationRequested 也是會進入到 catch 區段

public class DefaultController : ApiController
{
    private static readonly ILogger s_logger;
 
    static DefaultController()
    {
        if (s_logger == null)
        {
            s_logger = LogManager.GetCurrentClassLogger();
        }
    }
 
    // GET api/default
    public async Task<IHttpActionResult> Get(CancellationToken cancel)
    {
        var index = 0;
        try
        {
            for (var i = 0; i < 10; i++)
            {
                cancel.ThrowIfCancellationRequested();
 
                await Task.Delay(1000, cancel);
                index = i + 1;
            }
 
            s_logger.Trace("Process Done");
 
            return this.Ok($"{index}");
        }
        catch (Exception ex) when (ex is TaskCanceledException || ex is OperationCanceledException)
        {
            s_logger.Trace("Process Cancel");
        }
 
        return new ResponseMessageResult(new HttpResponseMessage(HttpStatusCode.NoContent));
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.Cancel/Server1/Controllers/DefaultController.cs

 

預設,swagger-net 為了讓 CancellationToken 變成參數,還需要一個 Swagger Filter 把 CancellationToken 參數拿掉

public class CancellationTokenOperationFilter : IOperationFilter
{
    public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
    {
        var excludedParameters = apiDescription.ParameterDescriptions
                                               .Where(p => p.ParameterDescriptor.ParameterType == typeof(CancellationToken))
                                               .Select(p => operation.parameters.FirstOrDefault(operationParam => operationParam.name == p.Name))
                                               .ToArray();
 
        foreach (var parameter in excludedParameters)
            operation.parameters.Remove(parameter);
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.Cancel/Server1/Filters/CancellationTokenOperationFilter.cs

 

接著 Ctrl+F5,把 Console App 叫起來,開啟瀏覽器訪問 https://localhost:44378

執行 /api/default,它應該會轉圈圈等待,這時候,把分頁關掉,Console App 就會收到 Cancel 的訊號囉


 

HttpClient 的取消

HttpClient 提供了許多非同步的的方法,傳入 CancellationToken 。

request.ContinueWith 用來觀察 Task 取消狀態是否如我所預期,當調用 cancelToken.Cancel() 後,task.IsCanceled 為 true

[TestMethod]
public void Cancel()
{
    var cancelToken = new CancellationTokenSource();
 
    var url     = "api/Default";
    var request = s_client.GetAsync(url, cancelToken.Token);
 
    //Task.Delay(2000).Wait();
    Task.Delay(1000).Wait();
 
    var response =
        request.ContinueWith(task =>
                             {
                                 var status =
                                     $"Status      = {task.Status}\r\n"      +
                                     $"IsCanceled  = {task.IsCanceled}\r\n"  +
                                     $"IsCompleted = {task.IsCompleted}\r\n" +
                                     $"IsFaulted   = {task.IsFaulted}";
 
                                 s_logger.Trace(status);
                                 return task;
                             },
                             TaskContinuationOptions.None
                            );
 
    cancelToken.Cancel();
    //s_client.CancelPendingRequests();
    Task.Delay(1000).Wait();
    Task.WaitAll(response);
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.Cancel/Client/DefaultControllerUnitTest.cs
 

HttpClient 的 CancelPendingRequests,也可以取消請求

不過,它們都無法取消 Server 的命令,看來 HttpClient 要實作取消得靠其他方式

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo