【ASP.NET Core】繫結到 CancellationToken 物件

東邪獨孤發表於2022-03-06

負責管理 HTTP 請求上下文的 HttpContext 物件有一個名為 RequestAborted 的屬性。據其名思其義,就是可用來表示客戶端請求是否已取消。

果然,它的型別是 CancellationToken,這傢伙是結構型別,為啥強調是結構呢——因為是值型別啊。在訪問 HTTP 的整個上下文傳遞過程,直接賦值會複製多個例項,弄不好就會搞得一個請求通訊期間狀態資料不一致。所以,類庫內部在傳遞此屬性值時會用 object 型別的變數來引用它的值,嗯,對的,就是“裝箱”。以引用型別的方式操作它,可以避免物件的複製而造成資料不統一。

具體可以看看 CancellationTokenModelBinder 類的原始碼(名稱空間:Microsoft.AspNetCore.Mvc.ModelBinding.Binders)。

public class CancellationTokenModelBinder : IModelBinder
{
    /// <inheritdoc />
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        // We need to force boxing now, so we can insert the same reference to the boxed CancellationToken
        // in both the ValidationState and ModelBindingResult.
        //
        // DO NOT simplify this code by removing the cast.
        var model = (object)bindingContext.HttpContext.RequestAborted;
        bindingContext.ValidationState.Add(model, new ValidationStateEntry() { SuppressValidation = true });
        bindingContext.Result = ModelBindingResult.Success(model);

        return Task.CompletedTask;
    }
}

以上內容,大夥伴們應該能看懂的。看不懂咋辦?沒事的,不要自卑,不必跳河,看不懂但知道怎麼用就行。

重點看這一句:

var model = (object)bindingContext.HttpContext.RequestAborted;

把它強制轉換為 object 型別再賦值,確保賦值後 CancellationToken 例項沒有被複制。

如果在提交 HTTP 後,以及在伺服器處理完畢返回訊息給客戶端之前,如果客戶端關閉(取消)了連線(比如,關掉瀏覽器,單擊“取消”請求,網路斷了,路由器著火了等情況),那麼,透過 HttpContext.RequestAborted 屬性我們在伺服器程式碼中就獲得相關資訊。說直接一點,就是 IsCancellationRequested 會返回 true。

老周暫不講模型繫結的事,先看看這個 RequestAborted 屬性如何使用。

來個示例。從前,有個 controller 名叫 Happy,它有兩個兒子(action),老大叫 Index,老二叫 ChouJiang(抽獎)。Happy 家裡開了個彩票店,老大 Index 負責門面,喜迎南北客;老二負責業務,包括把開獎結果告訴客人。

    public class HappyController : Controller
    {
        // 用來隨機生成幸運數字
        private static readonly Random rand = new((int)DateTime.Now.ToBinary());

        // 記錄日誌,可通過依賴注入解決例項化問題
        private readonly ILogger logger;
        /// <summary>
        /// 建構函式
        /// </summary>
        /// <param name="logfac">從依賴注入獲取</param>
        public HappyController(ILoggerFactory logfac)
        {
            logger = logfac.CreateLogger("Demo Log");
        }

        public IActionResult Index()
        {
            // 門面功夫,開門迎客
            return View("~/views/TestView1.cshtml");
        }

        public async Task<IActionResult> ChouJiang()
        {
            // 抽獎模擬中
            int x = 5;
            int result = 0;
            // 抽五次,選最後一個幸運數字
            while(x > 0)
            {
                // 如果連線掛了,直接拜拜
                if(HttpContext.RequestAborted.IsCancellationRequested)
                {
                    logger.LogInformation("請求已取消");
                    return NoContent();
                }
                await Task.Delay(500);  //模擬延時
                x--;
                result = rand.Next(0, 1000);//生成隨機數
            }
            // 開大獎了
            return Content($"<script>alert('幸運數字:{result}')</script>", "text/html", Encoding.UTF8);
        }
    }

此例中的核心是判斷 HttpContext.RequestAborted.IsCancellationRequested 是否為 true。如果是,那麼這一輪抽獎活動結束。

下面 Razor 程式碼是 Happy 彩票店的門面裝修效果,請隔壁老王設計的。

@{
    ViewBag.Title = "演示-1";
}

<p>點選下面連結,開啟虎年幸運大獎</p>
<a target="_blank" asp-action="ChouJiang" asp-controller="Happy">抽獎</a>

把示例執行起來。

img

點選頁面上的連結,如果你有足夠的耐心,等其完成抽獎,會看到幸運數字。

img

如果你覺得沒意思,在點選連結後,點選瀏覽器上的“X”,取消操作,會看到日誌輸出,表示連線斷了/請求取消了。

img


動不動就去訪問 HttpContext.RequestAborted.IsCancellationRequested 也不怎麼方便,至少沒有方便麵方便。所以,我們們要做一進升級——使用模型繫結。

要求是:

  1. 繫結的物件型別是 CancellationToken
  2. 繫結目標可以是 action 方法引數,也可以是 Controller 的屬性(MVC),或 Model Page 的屬性(Razor Pages)。

於是,上面的抽獎程式碼可以這樣改:

        public async Task<IActionResult> ChouJiang(CancellationToken ct)
        {
            // ……
            while(x > 0)
            {
                // 如果連線掛了,直接拜拜
                if(ct.IsCancellationRequested)
                {
                    logger.LogInformation("請求已取消");
                    return NoContent();
                }
                await Task.Delay(500);  //模擬延時
                x--;
                result = rand.Next(0, 1000);//生成隨機數
            }
            // ……
        }

也可以在 Controller 中定義屬性來繫結。把本例進行修改。

        // 這是屬性
        [BindProperty(SupportsGet = true)]
        public CancellationToken CancelTK { get; set; }

        public async Task<IActionResult> ChouJiang()
        {
            // ……
            while(x > 0)
            {
                // 如果連線掛了,直接拜拜
                if(CancelTK.IsCancellationRequested)
                {
                    //……
                }
                await Task.Delay(500);  //模擬延時
                x--;
                result = rand.Next(0, 1000);//生成隨機數
            }
            // ……
        }

如果用屬性來繫結,那麼在屬性上應用 BindProperty 特性是必須的。這裡要把 SupportsGet 設定為 true,因為老周這個例子中,檢視是點選連結後呼叫抽獎程式碼的,是以 HTTP-GET 方式請求的,而預設情況是 BindProperty 在 GET 方式時不進行繫結。所以,為了能順利繫結,就得把 SupportsGet 改為 true;如果你用的是 POST 方式觸發,就不用設定。

相關文章