7

這裏的想法支持很簡單,但實現有一些有趣的細微差別。這是擴展方法,我想在.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一起使用時,此實現是否會按預期行事?

回答

6

將這種實現的實際行爲與TPL使用時如預期?

  1. 不會的標誌Task<T>結果作爲取消,因此如預期的行爲不會完全相同。
  2. 如果發生超時,則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); 
     } 
    } 
} 
+0

@ 280Z28謝謝 - 我寫這個沒有VS,所以無法實際測試這一切;) –

+0

@ 280Z28呀 - 正如我couldn'te測試了一下,我並沒有意識到,'Abort'仍然會觸發回調(有道理)。這隻會導致行爲有點偏離,但仍然有效。 (你會得到一個WebException而不是正確的取消)。 –

+0

我編輯您的文章:1)正確描述了我原來的問題(包括1個新的)的2個主要錯誤,2)包含最新的工作代碼,和3)包含顯示下一個成功案例和3個不同的正確行爲測試類失敗案例(取消,超時和404錯誤)。 –