負責管理 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>
把示例執行起來。
點選頁面上的連結,如果你有足夠的耐心,等其完成抽獎,會看到幸運數字。
如果你覺得沒意思,在點選連結後,點選瀏覽器上的“X”,取消操作,會看到日誌輸出,表示連線斷了/請求取消了。
動不動就去訪問 HttpContext.RequestAborted.IsCancellationRequested 也不怎麼方便,至少沒有方便麵方便。所以,我們們要做一進升級——使用模型繫結。
要求是:
- 繫結的物件型別是 CancellationToken
- 繫結目標可以是 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 方式觸發,就不用設定。