7

L'idea è semplice, ma l'implementazione presenta alcune interessanti sfumature. Questa è la firma del metodo di estensione che vorrei implementare in .NET 4.Implementazione del metodo di estensione WebRequest.GetResponseAsync con supporto per CancellationToken

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

Ecco la mia implementazione iniziale. Da quello che ho letto, la richiesta web potrebbe essere cancelled due to a timeout. Oltre al supporto descritto in questa pagina, desidero chiamare correttamente lo request.Abort() se viene richiesta la cancellazione tramite lo 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(); 
    } 
} 

La mia domanda è semplice ma impegnativa. Questa implementazione si comporterà effettivamente come previsto se utilizzata con TPL?

risposta

6

Questa implementazione si comporterà effettivamente come previsto se utilizzata con TPL?

No.

  1. Non sarà bandiera Task<T> risultato come cancellata, in modo che il comportamento non sarà esattamente come previsto.
  2. In caso di timeout, lo contenuto nello segnalato da Task.Exception avrà lo stato WebExceptionStatus.RequestCanceled. Dovrebbe invece essere WebExceptionStatus.Timeout.

In realtà mi piacerebbe utilizzare TaskCompletionSource<T> per implementarlo. Questo permette di scrivere il codice senza fare i propri metodi di stile 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; 
} 

Il vantaggio è che il risultato Task<T> funzionerà pienamente come previsto (verrà contrassegnato come cancellato, o alzare la stessa eccezione con informazioni timeout come versione sincrona, ecc.). Ciò evita anche il sovraccarico dell'utilizzo di Task.Factory.FromAsync, poiché già gestisci la maggior parte del lavoro difficile da te coinvolto.


addendum da 280Z28

Ecco un test di unità mostrante il funzionamento corretto per il metodo di cui sopra.

[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 Grazie - ho scritto questo senza VS, quindi non potuto effettivamente provare tutto;) @ –

+0

280Z28 Sì - Come ho couldn'te provarlo, non mi rendevo conto che 'Abort' sarebbe ancora attivare la richiamata (ha senso che lo faccia). Ciò farà sì che il comportamento sia un po 'spento, ma comunque funzionale. (Riceverai una WebException invece della corretta cancellazione). –

+0

Ho modificato il post in 1) descrivo correttamente i 2 errori principali nella mia domanda originale (incluso 1 nuovo), 2) contiene l'ultimo codice di lavoro e 3) contengono una classe di test che mostra un comportamento corretto in un caso di successo e 3 diversi casi di fallimento (cancellazione, timeout e errore 404). –

Problemi correlati