问题描述
这里的想法很简单,但实现有一些有趣的细微之处。这是扩展方法的签名,我想在执行 .NET 4
公共静态任务< WebResponse的> GetResponseAsync(这WebRequest的要求的CancellationToken令牌);
下面是我的初步实施。从我读过,web请求可能需要为cancelled由于超时。除此之外页描述的支持,我想正确地调用 request.Abort()
如果取消通过请求的CancellationToken
。
公共静态任务< WebResponse的> GetResponseAsync(这WebRequest的要求的CancellationToken令牌)
{
如果(要求== NULL)
抛出新ArgumentNullException(要求);
返回Task.Factory.FromAsync< WebRequest的,的CancellationToken,WebResponse类>(BeginGetResponse,request.EndGetResponse,请求令牌,NULL);
}
私有静态的IAsyncResult BeginGetResponse(WebRequest的要求的CancellationToken令牌,AsyncCallback的回调,对象的状态)
{
IAsyncResult的asyncResult = request.BeginGetResponse(回调,状态);
如果(!asyncResult.IsCompleted)
{
如果(request.Timeout!= Timeout.Infinite)
ThreadPool.RegisterWaitForSingleObject(asyncResult.AsyncWaitHandle,WebRequestTimeoutCallback,要求request.Timeout,真正的);
如果(令牌!= CancellationToken.None)
ThreadPool.RegisterWaitForSingleObject(token.WaitHandle,WebRequestCancelledCallback,Tuple.Create(请求令牌),Timeout.Infinite,真正的);
}
返回asyncResult;
}
私有静态无效WebRequestTimeoutCallback(对象状态,布尔TIMEDOUT)
{
如果(TIMEDOUT)
{
WebRequest的请求=状态的WebRequest;
如果(请求!= NULL)
request.Abort();
}
}
私有静态无效WebRequestCancelledCallback(对象状态,布尔TIMEDOUT)
{
元组LT; WebRequest的,的CancellationToken>数据=状态元组LT; WebRequest的,的CancellationToken取代;
如果(数据= NULL和放大器;!&安培; data.Item2.IsCancellationRequested)
{
data.Item1.Abort();
}
}
我的问题很简单,但具有挑战性。这是否会实施的行为事实上与第三方物流使用时如预期?
没有。
- 在不会的标志
任务< T>
结果为已取消,所以行为不会完全如预期 - 在超时的情况下,
WebException
包含在AggregateException
报道Task.Exception
将有状态WebExceptionStatus.RequestCanceled
。它应该改为为WebExceptionStatus.Timeout
。
我真的建议使用 TaskCompletionSource< T>
来实现这一点。这允许你写的code,而不使自己的APM风格的方法:
公共静态任务< WebResponse的> GetResponseAsync(这WebRequest的要求的CancellationToken令牌)
{
如果(要求== NULL)
抛出新ArgumentNullException(要求);
布尔超时= FALSE;
TaskCompletionSource< WebResponse的> completionSource =新TaskCompletionSource< WebResponse的>();
的AsyncCallback completedCallback =
结果=>
{
尝试
{
completionSource.TrySetResult(request.EndGetResponse(结果));
}
赶上(WebException前)
{
如果(超时)
completionSource.TrySetException(新WebException(期间超时期限的请求未收到任何答复。WebExceptionStatus.Timeout));
否则,如果(token.IsCancellationRequested)
completionSource.TrySetCanceled();
其他
completionSource.TrySetException(前);
}
赶上(例外前)
{
completionSource.TrySetException(前);
}
};
IAsyncResult的asyncResult = request.BeginGetResponse(completedCallback,NULL);
如果(!asyncResult.IsCompleted)
{
如果(request.Timeout!= Timeout.Infinite)
{
WaitOrTimerCallback timedOutCallback =
(对象状态,布尔TIMEDOUT)=>
{
如果(TIMEDOUT)
{
超时= TRUE;
request.Abort();
}
};
ThreadPool.RegisterWaitForSingleObject(asyncResult.AsyncWaitHandle,timedOutCallback,空,request.Timeout,真正的);
}
如果(令牌!= CancellationToken.None)
{
WaitOrTimerCallback cancelledCallback =
(对象状态,布尔TIMEDOUT)=>
{
如果(token.IsCancellationRequested)
request.Abort();
};
ThreadPool.RegisterWaitForSingleObject(token.WaitHandle,cancelledCallback,空,Timeout.Infinite,真正的);
}
}
返回completionSource.Task;
}
这里的好处是,你的任务< T>
结果将工作完全按预期(将被标记为取消,或作为同步版本超时信息提出同样的异常, 等等)。这也避免了使用的开销 Task.Factory.FromAsync
,因为你已经处理大部分涉及有自己的困难的工作。
补遗由280Z28
下面是一个单元测试,显示为上面的方法正确操作。
[TestClass的]
公共类AsyncWebRequestTests
{
[测试方法]
公共无效TestAsyncWebRequest()
{
开放的我们的uri =新的URI(http://google.com);
WebRequest的请求= HttpWebRequest.Create(URI);
任务< WebResponse的>响应= request.GetResponseAsync();
response.Wait();
}
[测试方法]
公共无效TestAsyncWebRequestTimeout()
{
开放的我们的uri =新的URI(http://google.com);
WebRequest的请求= HttpWebRequest.Create(URI);
request.Timeout = 0;
任务< WebResponse的>响应= request.GetResponseAsync();
尝试
{
response.Wait();
Assert.Fail(预期例外);
}
赶上(AggregateException除外)
{
Assert.AreEqual(TaskStatus.Faulted,response.Status);
ReadOnlyCollection还<异常>异常= exception.InnerExceptions;
Assert.AreEqual(1,exceptions.Count);
Assert.IsInstanceOfType(例外[0]的typeof(WebException));
WebException webException =(WebException)异常[0];
Assert.AreEqual(WebExceptionStatus.Timeout,webException.Status);
}
}
[测试方法]
公共无效TestAsyncWebRequestCancellation()
{
开放的我们的uri =新的URI(http://google.com);
WebRequest的请求= HttpWebRequest.Create(URI);
CancellationTokenSource cancellationTokenSource =新CancellationTokenSource();
任务< WebResponse的>响应= request.GetResponseAsync(cancellationTokenSource.Token);
cancellationTokenSource.Cancel();
尝试
{
response.Wait();
Assert.Fail(预期例外);
}
赶上(AggregateException除外)
{
Assert.AreEqual(TaskStatus.Canceled,response.Status);
ReadOnlyCollection还<异常>异常= exception.InnerExceptions;
Assert.AreEqual(1,exceptions.Count);
Assert.IsInstanceOfType(例外[0]的typeof(OperationCanceledException));
}
}
[测试方法]
公共无效TestAsyncWebRequestError()
{
开放的我们的uri =新的URI(http://google.com/fail);
WebRequest的请求= HttpWebRequest.Create(URI);
任务< WebResponse的>响应= request.GetResponseAsync();
尝试
{
response.Wait();
Assert.Fail(预期例外);
}
赶上(AggregateException除外)
{
Assert.AreEqual(TaskStatus.Faulted,response.Status);
ReadOnlyCollection还<异常>异常= exception.InnerExceptions;
Assert.AreEqual(1,exceptions.Count);
Assert.IsInstanceOfType(例外[0]的typeof(WebException));
WebException webException =(WebException)异常[0];
Assert.AreEqual(的HTTPStatus code.NotFound,((HttpWebResponse)webException.Response).STATUS code);
}
}
}
The idea here is simple, but the implementation has some interesting nuances. This is the signature of the extension method I would like to implement in .NET 4.
public static Task<WebResponse> GetResponseAsync(this WebRequest request, CancellationToken token);
Here is my initial implementation. From what I've read, the web request might need to be cancelled due to a timeout. In addition to the support described on that page, I want to properly call request.Abort()
if cancellation is requested via the CancellationToken
.
public static Task<WebResponse> GetResponseAsync(this WebRequest request, CancellationToken token)
{
if (request == null)
throw new ArgumentNullException("request");
return Task.Factory.FromAsync<WebRequest, CancellationToken, WebResponse>(BeginGetResponse, request.EndGetResponse, request, token, null);
}
private static IAsyncResult BeginGetResponse(WebRequest request, CancellationToken token, AsyncCallback callback, object state)
{
IAsyncResult asyncResult = request.BeginGetResponse(callback, state);
if (!asyncResult.IsCompleted)
{
if (request.Timeout != Timeout.Infinite)
ThreadPool.RegisterWaitForSingleObject(asyncResult.AsyncWaitHandle, WebRequestTimeoutCallback, request, request.Timeout, true);
if (token != CancellationToken.None)
ThreadPool.RegisterWaitForSingleObject(token.WaitHandle, WebRequestCancelledCallback, Tuple.Create(request, token), Timeout.Infinite, true);
}
return asyncResult;
}
private static void WebRequestTimeoutCallback(object state, bool timedOut)
{
if (timedOut)
{
WebRequest request = state as WebRequest;
if (request != null)
request.Abort();
}
}
private static void WebRequestCancelledCallback(object state, bool timedOut)
{
Tuple<WebRequest, CancellationToken> data = state as Tuple<WebRequest, CancellationToken>;
if (data != null && data.Item2.IsCancellationRequested)
{
data.Item1.Abort();
}
}
My question is simple yet challenging. Will this implementation actually behave as expected when used with the TPL?
No.
- It will not flag the
Task<T>
result as cancelled, so the behavior will not be exactly as expected. - In the event of a timeout, the
WebException
contained in theAggregateException
reported byTask.Exception
will have the statusWebExceptionStatus.RequestCanceled
. It should instead beWebExceptionStatus.Timeout
.
I would actually recommend using TaskCompletionSource<T>
to implement this. This allows you to write the code without making your own APM style methods:
public static Task<WebResponse> GetResponseAsync(this WebRequest request, CancellationToken token)
{
if (request == null)
throw new ArgumentNullException("request");
bool timeout = false;
TaskCompletionSource<WebResponse> completionSource = new TaskCompletionSource<WebResponse>();
AsyncCallback completedCallback =
result =>
{
try
{
completionSource.TrySetResult(request.EndGetResponse(result));
}
catch (WebException ex)
{
if (timeout)
completionSource.TrySetException(new WebException("No response was received during the time-out period for a request.", WebExceptionStatus.Timeout));
else if (token.IsCancellationRequested)
completionSource.TrySetCanceled();
else
completionSource.TrySetException(ex);
}
catch (Exception ex)
{
completionSource.TrySetException(ex);
}
};
IAsyncResult asyncResult = request.BeginGetResponse(completedCallback, null);
if (!asyncResult.IsCompleted)
{
if (request.Timeout != Timeout.Infinite)
{
WaitOrTimerCallback timedOutCallback =
(object state, bool timedOut) =>
{
if (timedOut)
{
timeout = true;
request.Abort();
}
};
ThreadPool.RegisterWaitForSingleObject(asyncResult.AsyncWaitHandle, timedOutCallback, null, request.Timeout, true);
}
if (token != CancellationToken.None)
{
WaitOrTimerCallback cancelledCallback =
(object state, bool timedOut) =>
{
if (token.IsCancellationRequested)
request.Abort();
};
ThreadPool.RegisterWaitForSingleObject(token.WaitHandle, cancelledCallback, null, Timeout.Infinite, true);
}
}
return completionSource.Task;
}
The advantage here is that your Task<T>
result will work fully as expected (will be flagged as canceled, or raise the same exception with timeout info as synchronous version, etc). This also avoids the overhead of using Task.Factory.FromAsync
, since you're already handling most of the difficult work involved there yourself.
Addendum by 280Z28
Here is a unit test showing proper operation for the method above.
[TestClass]
public class AsyncWebRequestTests
{
[TestMethod]
public void TestAsyncWebRequest()
{
Uri uri = new Uri("http://google.com");
WebRequest request = HttpWebRequest.Create(uri);
Task<WebResponse> response = request.GetResponseAsync();
response.Wait();
}
[TestMethod]
public void TestAsyncWebRequestTimeout()
{
Uri uri = new Uri("http://google.com");
WebRequest request = HttpWebRequest.Create(uri);
request.Timeout = 0;
Task<WebResponse> response = request.GetResponseAsync();
try
{
response.Wait();
Assert.Fail("Expected an exception");
}
catch (AggregateException exception)
{
Assert.AreEqual(TaskStatus.Faulted, response.Status);
ReadOnlyCollection<Exception> exceptions = exception.InnerExceptions;
Assert.AreEqual(1, exceptions.Count);
Assert.IsInstanceOfType(exceptions[0], typeof(WebException));
WebException webException = (WebException)exceptions[0];
Assert.AreEqual(WebExceptionStatus.Timeout, webException.Status);
}
}
[TestMethod]
public void TestAsyncWebRequestCancellation()
{
Uri uri = new Uri("http://google.com");
WebRequest request = HttpWebRequest.Create(uri);
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
Task<WebResponse> response = request.GetResponseAsync(cancellationTokenSource.Token);
cancellationTokenSource.Cancel();
try
{
response.Wait();
Assert.Fail("Expected an exception");
}
catch (AggregateException exception)
{
Assert.AreEqual(TaskStatus.Canceled, response.Status);
ReadOnlyCollection<Exception> exceptions = exception.InnerExceptions;
Assert.AreEqual(1, exceptions.Count);
Assert.IsInstanceOfType(exceptions[0], typeof(OperationCanceledException));
}
}
[TestMethod]
public void TestAsyncWebRequestError()
{
Uri uri = new Uri("http://google.com/fail");
WebRequest request = HttpWebRequest.Create(uri);
Task<WebResponse> response = request.GetResponseAsync();
try
{
response.Wait();
Assert.Fail("Expected an exception");
}
catch (AggregateException exception)
{
Assert.AreEqual(TaskStatus.Faulted, response.Status);
ReadOnlyCollection<Exception> exceptions = exception.InnerExceptions;
Assert.AreEqual(1, exceptions.Count);
Assert.IsInstanceOfType(exceptions[0], typeof(WebException));
WebException webException = (WebException)exceptions[0];
Assert.AreEqual(HttpStatusCode.NotFound, ((HttpWebResponse)webException.Response).StatusCode);
}
}
}
这篇关于实现扩展方法WebRequest.GetResponseAsync与支持的CancellationToken的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!