ASP.NET Core知多少系列:总体介绍及目录

本文所讲方式仅适用于托管在Kestrel Server中的应用。如果托管在IIS和IIS Express上时,ASP.NET Core Module(ANCM)并不会告诉ASP.NET Core在客户端断开连接时中止请求。但可喜的是,ANCM预计在.NET Core 2.2中会完善这一机制。

1. 引言

假设有一个耗时的Action,在浏览器发出请求返回响应之前,如果刷新了页面,对于浏览器(客户端)来说前一个请求就会被终止。而对于服务端来说,又是怎样呢?前一个请求也会自动终止,还是会继续运行呢?

下面我们通过实例寻求答案。

2. 实例演示

创建一个SlowRequestController,再定义一个Get请求,并通过Task.Delay(10_000)模拟耗时行为。代码如下:

public class SlowRequestController : Controller
{
private readonly ILogger _logger; public SlowRequestController(ILogger<SlowRequestController> logger)
{
_logger = logger;
} [HttpGet("/slowtest")]
public async Task<string> Get()
{
_logger.LogInformation("Starting to do slow work"); // slow async action, e.g. call external api
await Task.Delay(10_000); var message = "Finished slow delay of 10 seconds."; _logger.LogInformation(message); return message;
}
}

如果我们发起请求,那么该页面将耗时10s才能完成显示。



如果我们检查运行日志,我们发现其输出符合预期:

如果在第一次请求返回之前,刷新页面,结果将是怎样呢??



从日志中我们可以看出:刷新后,第一个请求虽然在客户端被取消了,但是服务端仍旧会持续运行。

从而可以说明MVC的默认行为: 即使用户刷新了浏览器会取消原始请求,但MVC对其一无所知,已经被取消的请求还是会在服务端继续运行,而最终的运行结果将会被丢弃。

这样就会造成严重的性能浪费。如果服务端能感知用户中断了请求,并终止运行耗时的任务就好了。

幸好,ASP.NET Core开发团队体贴的考虑了这一点,允许我们通过以下两种方式来获取客户端的请求是否被终止。

  1. 通过HttpContexRequestAborted属性:
  2. 通过方法注入CancellationToken参数:
if (HttpContext.RequestAborted.IsCancellationRequested)
{
// can stop working now
}
[HttpGet]
public async Task<ActionResult> GetHardWork(CancellationToken cancellationToken)
{
// ... if (cancellationToken.IsCancellationRequested)
{
// stop!
} // ...
}

而这两种方式其实是一样的,因为HttpContext.RequestAbortedcancellationToken对应的是同一个对象:

if(cancellationToken == HttpContext.RequestAborted)
{
// this is true!
}

下面我们就来以cancellationToken为例,看看如何感知客户端请求终止并终止服务端服务。

3. 在Action中使用CancellationToken

CancellationToken是由CancellationTokenSource创建的轻量级对象。当某个CancellationTokenSource被取消时,它会通知所有的消费者CancellationToken

取消时,CancellationTokenIsCancellationRequested属性将设置为True,表示CancellationTokenSource已取消。

再回到前面的实例,我们有一个长期运行的操作方法(例如,通过调用许多其他API生成只读报告)。由于它是一种昂贵的方法,我们希望在用户取消请求时尽快停止执行操作。

下面的代码显示了通过在action方法中注入一个CancellationToken,并将其传递给Task.Delay,来达到同步终止服务端请求的目的:

public class SlowRequestController : Controller
{
private readonly ILogger _logger; public SlowRequestController(ILogger<SlowRequestController> logger)
{
_logger = logger;
} [HttpGet("/slowtest")]
public async Task<string> Get(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting to do slow work"); // slow async action, e.g. call external api
await Task.Delay(10_000, cancellationToken); var message = "Finished slow delay of 10 seconds."; _logger.LogInformation(message); return message;
}
}

MVC将使用CancellationTokenModelBinder自动将Action中的任何CancellationToken参数绑定到HttpContext.RequestAborted。当我们在Startup.ConfigureServices()中调用services.AddMvc() services.AddMvcCore()时,CancellationTokenModelBinder模型绑定器就会被自动注册。

通过这个小改动,我们再尝试在第一个请求返回之前刷新页面,从日志中我们发现,第一个请求将不会继续完成。而是当Task.Delay检测到CancellationToken.IsCancellationRequested属性为true时立即停止执行时并抛出TaskCancelledException

简而言之,用户刷新浏览器,在服务端通过抛出TaskCancelledException异常终止了第一个请求,而该异常通过请求管道再传播回来。

在这个场景中,Task.Delay()会监视CancellationToken,因此无需我们手动检查CancellationToken是否被取消。

4. 手动检查CancellationToken状态

如果你正在调用支持CancellationToken的内置方法,比如Task.Delay()HttpClient.SendAsync(),那么你可以直接传入CancellationToken,并让内部方法负责实际取消。

在其他情况下,您可能正在进行一些同步工作,您希望能够取消这些工作。例如,假设正在构建一份报告来计算公司员工的所有佣金。你循环每个员工,然后遍历他们的每一笔销售。

能够在中途取消此报告生成的简单解决方案是检查for循环内的CancellationToken,如果用户取消请求则跳出循环。

以下示例通过循环10次并执行某些同步(不可取消)工作来表示此类情况,该工作由对Thread.Sleep()来模拟。在每个循环开始时,我们检查CancellationToken,如果取消则抛出异常。这使得我们可以终止一个长时间运行的同步任务。

public class SlowRequestController : Controller
{
private readonly ILogger _logger; public SlowRequestController(ILogger<SlowRequestController> logger)
{
_logger = logger;
} [HttpGet("/slowtest")]
public async Task<string> Get(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting to do slow work"); for(var i=0; i<10; i++)
{
cancellationToken.ThrowIfCancellationRequested();
// slow non-cancellable work
Thread.Sleep(1000);
}
var message = "Finished slow delay of 10 seconds."; _logger.LogInformation(message); return message;
}
}

现在,如果你取消请求,则对ThrowIfCancelletionRequested()的调用将抛出一个OperationCanceledException,它将再次传播回过滤器管道和中间件管道。

5. 使用ExceptionFilter捕捉取消异常

ExceptionFilters是一个MVC概念,可用于处理在您的操作方法或操作过滤器中发生的异常。可以参考官方文档

可以将过滤器应用到控制器级别和操作级别,也可以应用于全局级别。为了简单起见,我们创建一个过滤器并添加到全局过滤器。

public class OperationCancelledExceptionFilter : ExceptionFilterAttribute
{
private readonly ILogger _logger; public OperationCancelledExceptionFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<OperationCancelledExceptionFilter>();
}
public override void OnException(ExceptionContext context)
{
if(context.Exception is OperationCanceledException)
{
_logger.LogInformation("Request was cancelled");
context.ExceptionHandled = true;
context.Result = new StatusCodeResult(499);
}
}
}

我们通过重载OnException方法并特殊处理OperationCanceledException异常即可成功捕获取消异常。

Task.Delay()抛出的异常是TaskCancelledException 类型,其为OperationCanceledException的基类,所以,以上过滤器也可正确捕捉。

然后注册过滤器:

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add<OperationCancelledExceptionFilter>();
});
}
}

现在再测试,我们发现运行日志将不会包含异常信息,取而代之的是我们自定义的信息。

6. 最后

通过本文,我们知道用户可以通过点击浏览器上的停止或重新加载按钮随时取消Web应用的请求。而实际上仅仅是终止了客户端的请求,服务端的请求还在继续运行。对于简单耗时短的请求来说,我们可以不予理睬。但是,对于耗时任务来说,我们却不可以置若罔闻,因为其有很高的性能损耗。

而如何解决呢?其关键是通过CancellationToken来捕捉用户请求的状态,从而根据需要进行相应的处理。

参考资料:

CancellationTokens and Aborted ASP.NET Core Requests

Using CancellationTokens in ASP.NET Core MVC controllers

ASP.NET Core 中断请求了解一下(翻译)的更多相关文章

  1. ASP.NET Core 中文文档

    ASP.NET Core 中文文档 翻译计划 五月中旬 .NET Core RC2 如期发布,我们遂决定翻译 ASP.NET Core 文档.我们在 何镇汐先生. 悲梦先生. 张仁建先生和 雷欧纳德先 ...

  2. 新的ASP.NET Core 迁移指南

    最近在微信里做了一个调查: Web Forms应用程序升级到.NET 6, 收到550份调查,调查还在继续,欢迎参与调查.可以访问链接:https://wj.qq.com/s2/9822949/ac3 ...

  3. ASP.NET Core文档中Work with Data章节的翻译目录

    作为初学者看了相关的教程,遇到的问题有: 1. 教程不是针对初学者,往往在某一方面教的较深,但并不系统,不适合初学者: 2. 虽然翻译的很顺畅,但是谈了自己较多的开发体会,初学者看着困难,尤其是TOM ...

  4. 【翻译】Asp.net Core介绍

    ASP.NET Core is a significant redesign of ASP.NET. This topic introduces the new concepts in ASP.NET ...

  5. [翻译] ASP.NET Core 2.2 正式版发布

    本文为翻译,原文地址:https://blogs.msdn.microsoft.com/webdev/2018/12/04/asp-net-core-2-2-available-today/ 我(文章 ...

  6. 翻译 Asp.Net Core 2.2.0-preview1已经发布

    Asp.Net Core 2.2.0-preview1已经发布 原文地址 ASP.NET Core 2.2.0-preview1 now available 今天我们很高兴地宣布,现在可以试用ASP. ...

  7. 【翻译】asp.net core中使用MediatR

    这篇文章来自:https://ardalis.com/using-mediatr-in-aspnet-core-apps 本文作为翻译,有一些单词翻译成中文可能会有一些误解(对于读者)或者错误(对于作 ...

  8. [翻译] 初看 ASP.NET Core 3.0 即将到来的变化

    [翻译] 初看 ASP.NET Core 3.0 即将到来的变化 原文: A first look at changes coming in ASP.NET Core 3.0 在我们努力完成下一个 m ...

  9. [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置

    [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置 原文: USING CONSUL FOR STORING THE CONFIGURATION IN ASP.NET COR ...

随机推荐

  1. SSM-Spring-19:Spring中JdbcTemplate

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- Spring自带一个ORM持久化框架JdbcTemplate,他可以说是jdbc的加强版,但是对最细微的控制肯 ...

  2. 在 Docker 容器中运行应用程序

    案例说明 运行 3 个容器,实现对网站的监控. 三个容器的说明: 容器 web: 创建自 nginx 映像,使用 80 端口,运行于后台,实现 web 服务. 容器 mailer: 该容器中运行一个 ...

  3. CSS3 :nth-child() 选择器---挖坑

    E:nth-child(n) 语法: E:nth-child(n) { sRules } 说明: 匹配父元素的第n个子元素E,假设该子元素不是E,则选择符无效.(也就是说,会检查从body开始的每个元 ...

  4. PHP写的爬虫,爬指定网站页面上的各种图片

    打算用php实现一个爬虫,这是爬指定页面的图片的一段程序,其他的部分还没调试好,先把这个放上来 <?php $string=file_get_contents("http://www. ...

  5. 最近最久未使用页面淘汰算法———LRU算法(java实现)

    请珍惜小编劳动成果,该文章为小编原创,转载请注明出处. LRU算法,即Last Recently Used ---选择最后一次访问时间距离当前时间最长的一页并淘汰之--即淘汰最长时间没有使用的页 按照 ...

  6. 再谈.net的堆和栈---.NET Memory Management Basics

    .NET Memory Management Basics .NET memory management is designed so that the programmer is freed fro ...

  7. java Socket多线程聊天程序

    参考JAVA 通过 Socket 实现 TCP 编程 参考java Socket多线程聊天程序(适合初学者) 以J2SDK-1.3为例,Socket和ServerSocket类库位于java.net包 ...

  8. 删除外部dwg中指定的块定义

    本例实现删除外部图纸中指定的块定义,在外部图纸当前模型空间中是没有该块定义的块参照存在. 代码如下: void CBlockUtil::DeleteBlockDefFormOtherDwg(const ...

  9. bzoj 4916: 神犇和蒟蒻 (杜教筛+莫比乌斯反演)

    题目大意: 读入n. 第一行输出“1”(不带引号). 第二行输出$\sum_{i=1}^n i\phi(i)$. 题解: 所以说那个$\sum\mu$是在开玩笑么=.= 设$f(n)=n\phi(n) ...

  10. bzoj 3759 Hungergame 博弈论+线性基

    和nim游戏类似 易证必败状态为:当前打开的箱子中石子异或和为0,没打开的箱子中不存在一个子集满足异或和为0 因为先手无论是取石子还是开箱子,后手都可以通过取石子来使状态变回原状态 所以只需判定是否有 ...