【发布时间】:2014-08-15 17:32:31
【问题描述】:
像 MVC WebApi 一样运行在异步 ASP.NET 管道上,意思是execution timeout is unsupported。
在 MVC 中我使用 [AsyncTimeout] 过滤器,WebApi 没有这个。那么如何让 WebApi 中的请求超时呢?
【问题讨论】:
标签: c# asp.net asp.net-web-api asp.net-web-api2
像 MVC WebApi 一样运行在异步 ASP.NET 管道上,意思是execution timeout is unsupported。
在 MVC 中我使用 [AsyncTimeout] 过滤器,WebApi 没有这个。那么如何让 WebApi 中的请求超时呢?
【问题讨论】:
标签: c# asp.net asp.net-web-api asp.net-web-api2
对于您想要超时的每个端点,通过管道传递CancellationToken,例如:
[HttpGet]
public Task<Response> GetAsync()
{
var tokenSource = new CancellationTokenSource(_timeoutInSec * 1000);
return GetResponseAsync(tokenSource.Token);
}
【讨论】:
让您的生活更轻松,在您的基本控制器中添加以下方法:
protected async Task<T> RunTask<T>(Task<T> action, int timeout) {
var timeoutTask = Task.Delay(timeout);
var firstTaskFinished = await Task.WhenAny(timeoutTask, action);
if (firstTaskFinished == timeoutTask) {
throw new Exception("Timeout");
}
return action.Result;
}
现在每个从您的基本控制器继承的控制器都可以访问方法 RunTask。现在在您的 API 中调用 RunTask 方法,就像这样:
[HttpPost]
public async Task<ResponseModel> MyAPI(RequestModel request) {
try {
return await RunTask(Action(), Timeout);
} catch (Exception e) {
return null;
}
}
private async Task<ResponseModel> Action() {
return new ResponseModel();
}
【讨论】:
根据 Mendhak 的建议,您可以做您想做的事,但不完全是您想要做的方式,而无需跳过很多圈。 没有过滤器可能看起来像这样:
public class ValuesController : ApiController
{
public async Task<HttpResponseMessage> Get( )
{
var work = this.ActualWork( 5000 );
var timeout = this.Timeout( 2000 );
var finishedTask = await Task.WhenAny( timeout, work );
if( finishedTask == timeout )
{
return this.Request.CreateResponse( HttpStatusCode.RequestTimeout );
}
else
{
return this.Request.CreateResponse( HttpStatusCode.OK, work.Result );
}
}
private async Task<string> ActualWork( int sleepTime )
{
await Task.Delay( sleepTime );
return "work results";
}
private async Task Timeout( int timeoutValue )
{
await Task.Delay( timeoutValue );
}
}
在这里您将收到一个超时,因为我们正在做的实际“工作”将比超时时间更长。
用属性做你想做的事是可能的,但并不理想。这与之前的基本思想相同,但过滤器实际上可以用于通过反射执行操作。我不认为我会推荐这条路线,但在这个人为的例子中,你可以看到它是如何完成的:
public class TimeoutFilter : ActionFilterAttribute
{
public int Timeout { get; set; }
public TimeoutFilter( )
{
this.Timeout = int.MaxValue;
}
public TimeoutFilter( int timeout )
{
this.Timeout = timeout;
}
public override async Task OnActionExecutingAsync( HttpActionContext actionContext, CancellationToken cancellationToken )
{
var controller = actionContext.ControllerContext.Controller;
var controllerType = controller.GetType( );
var action = controllerType.GetMethod( actionContext.ActionDescriptor.ActionName );
var tokenSource = new CancellationTokenSource( );
var timeout = this.TimeoutTask( this.Timeout );
object result = null;
var work = Task.Run( ( ) =>
{
result = action.Invoke( controller, actionContext.ActionArguments.Values.ToArray( ) );
}, tokenSource.Token );
var finishedTask = await Task.WhenAny( timeout, work );
if( finishedTask == timeout )
{
tokenSource.Cancel( );
actionContext.Response = actionContext.Request.CreateResponse( HttpStatusCode.RequestTimeout );
}
else
{
actionContext.Response = actionContext.Request.CreateResponse( HttpStatusCode.OK, result );
}
}
private async Task TimeoutTask( int timeoutValue )
{
await Task.Delay( timeoutValue );
}
}
然后可以这样使用:
[TimeoutFilter( 10000 )]
public string Get( )
{
Thread.Sleep( 5000 );
return "Results";
}
这适用于简单类型(例如字符串),在 Firefox 中为我们提供:<z:anyType i:type="d1p1:string">Results</z:anyType>,尽管如您所见,序列化并不理想。就序列化而言,使用具有此确切代码的自定义类型会有点问题,但通过一些工作,这可能在某些特定场景中很有用。动作参数以字典而不是数组的形式出现也可能在参数排序方面造成一些问题。显然,对此有真正的支持会更好。
就 vNext 而言,他们很可能正计划为 Web API 添加服务器端超时功能,因为 MVC 和 API 控制器正在统一。如果他们这样做了,它可能不会通过System.Web.Mvc.AsyncTimeoutAttribute 类,因为他们明确删除了对System.Web 的依赖。
截至今天,向project.json 文件添加System.Web.Mvc 条目似乎不起作用,但这很可能会改变。如果是这样,虽然您将无法将新的云优化框架与此类代码一起使用,但您可能可以在仅用于与完整 .NET 框架一起运行的代码上使用 AsyncTimeout 属性。
对于它的价值,这是我尝试添加到project.json 的内容。也许一个特定的版本会让它更快乐?
"frameworks": {
"net451": {
"dependencies": {
"System.Web.Mvc": ""
}
}
}
对它的引用确实显示在解决方案资源管理器的引用列表中,但它带有一个黄色感叹号,表示存在问题。应用程序本身返回 500 个错误,而此引用仍然存在。
【讨论】:
使用 WebAPI,您通常会在 客户端 端而不是服务器端处理超时。这是因为,和I quote:
取消HTTP请求的方式是直接在HttpClient上取消。原因是多个请求可以在单个 HttpClient 中重用 TCP 连接,因此您不能安全地取消单个请求而不会影响其他请求。
您可以控制请求的超时时间——如果我没记错的话,我认为它在 HttpClientHandler 上。
如果您确实需要在 API 端本身实现超时,我建议您创建一个线程来完成您的工作,然后在一段时间后取消它。例如,您可以将其放在Task 中,使用Task.Wait 创建您的“超时”任务,并使用Task.WaitAny 作为第一个返回的任务。这可以模拟超时。
同样,如果您正在执行特定操作,请检查它是否已经支持超时。很多时候,我会从我的 WebAPI 执行一个HttpWebRequest,并指定它的Timeout 属性。
【讨论】:
Task.WaitAny 来完成,您的主要任务在一个线程中,而“等待”超时任务在另一个线程中。如果超时任务提前返回,则可以立即返回。