7

Ý tưởng ở đây rất đơn giản, nhưng việc triển khai có một số sắc thái thú vị. Đây là chữ ký của phương pháp mở rộng mà tôi muốn triển khai trong .NET 4.Phương thức mở rộng triển khai WebRequest.GetResponseAsync với sự hỗ trợ cho CancellationToken

public static Task<WebResponse> GetResponseAsync(this WebRequest request, CancellationToken token); 

Đây là triển khai ban đầu của tôi. Từ những gì tôi đã đọc, yêu cầu web có thể cần phải là cancelled due to a timeout. Ngoài hỗ trợ được mô tả trên trang đó, tôi muốn gọi đúng số request.Abort() nếu yêu cầu hủy qua 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(); 
    } 
} 

Câu hỏi của tôi đơn giản nhưng đầy thử thách. Việc triển khai này có thực sự hoạt động như mong đợi khi được sử dụng với TPL không?

Trả lời

6

Việc triển khai này có thực sự hoạt động như mong đợi khi được sử dụng với TPL không?

số

  1. Nó sẽ không cờ Task<T> kết quả như hủy bỏ, vì vậy hành vi sẽ không được chính xác như mong đợi.
  2. Trong trường hợp hết thời gian chờ, WebException có trong AggregateException được báo cáo bởi Task.Exception sẽ có trạng thái WebExceptionStatus.RequestCanceled. Thay vào đó, nó sẽ là WebExceptionStatus.Timeout.

Tôi thực sự khuyên bạn nên sử dụng TaskCompletionSource<T> để thực hiện việc này. Điều này cho phép bạn viết mã mà không làm phương pháp phong cách APM của riêng bạn:

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; 
} 

Ưu điểm ở đây là Task<T> kết quả của bạn sẽ làm việc hoàn toàn như mong đợi (sẽ được gắn cờ là bị hủy, hoặc nâng cao ngoại lệ cùng với thông tin thời gian chờ dưới dạng phiên bản đồng bộ, v.v.) Điều này cũng tránh được phí tổn sử dụng Task.Factory.FromAsync, vì bạn đã xử lý hầu hết các công việc khó khăn liên quan đến chính bạn.


Phụ Lục bởi 280Z28

Dưới đây là một thử nghiệm đơn vị cho thấy hoạt động thích hợp cho các phương pháp trên.

[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 Thank - Tôi viết này mà không có VS, như vậy có thể không thực sự kiểm tra nó tất cả;) –

+0

@ 280Z28 Yeah - Như tôi đã couldn'te kiểm tra nó, tôi đã không nhận ra rằng 'Abort' sẽ vẫn kích hoạt gọi lại (có nghĩa là nó làm). Điều đó sẽ chỉ khiến cho hành vi trở nên hơi lệch, nhưng vẫn hoạt động. (Bạn sẽ nhận được một WebException thay vì hủy bỏ thích hợp). –

+0

Tôi đã chỉnh sửa bài đăng của bạn thành 1) mô tả đúng 2 lỗi chính trong câu hỏi ban đầu của tôi (bao gồm 1 lỗi mới), 2) chứa mã làm việc mới nhất và 3) chứa lớp kiểm tra cho thấy hành vi phù hợp trong trường hợp thành công và 3 trường hợp lỗi (hủy, hết thời gian chờ và lỗi 404). –