实现扩展方法WebRequest.GetResponseAsync与的CancellationToken
问题描述:
这里的想法支持很简单,但实现有一些有趣的细微差别。这是扩展方法,我想在.NET 4实现的签名。实现扩展方法WebRequest.GetResponseAsync与的CancellationToken
public static Task<WebResponse> GetResponseAsync(this WebRequest request, CancellationToken token);
这是我最初的实现。从我读过的网页请求可能需要cancelled due to a timeout。除此之外页描述的支持,我想正确地调用request.Abort()
如果取消通过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();
}
}
我的问题很简单但具有挑战性。在与TPL一起使用时,此实现是否会按预期行事?
答
将这种实现的实际行为与TPL使用时如预期?
号
- 不会的标志
Task<T>
结果作为取消,因此如预期的行为不会完全相同。 - 如果发生超时,则
Task.Exception
报告的AggregateException
中包含的WebException
将具有状态WebExceptionStatus.RequestCanceled
。它应该是WebExceptionStatus.Timeout
。
我实际上会推荐使用TaskCompletionSource<T>
来实现这个。这允许你写的代码,而无需使自己的APM风格的方法:
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;
}
这里的好处是,按照预期的Task<T>
结果将完全工作(将被标记为取消,或提高与超时信息相同的异常作为同步版本等)。这也避免了使用Task.Factory.FromAsync
的开销,因为您已经处理了您自己所涉及的大部分困难工作。
补遗通过280Z28
这里是示出了用于上述方法正常工作的单元测试。
[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);
}
}
}
@ 280Z28谢谢 - 我写这个没有VS,所以无法实际测试这一切;) –
@ 280Z28呀 - 正如我couldn'te测试了一下,我并没有意识到,'Abort'仍然会触发回调(有道理)。这只会导致行为有点偏离,但仍然有效。 (你会得到一个WebException而不是正确的取消)。 –
我编辑您的文章:1)正确描述了我原来的问题(包括1个新的)的2个主要错误,2)包含最新的工作代码,和3)包含显示下一个成功案例和3个不同的正确行为测试类失败案例(取消,超时和404错误)。 –