简体   繁体   中英

Async Task management for HttpClient

I'm creating a generic loader and I want to kick off an HttpClient SendAsync request. However, some of these requests may take time, so I want to add the option to cancel, and notify upon completion.

This seems like a standard scenario imho.

I'm not sure if this is the correct way to go about this, but based on some examples I've looked at, here is where I'm at. If you look at the bottom of the code, my question is - at that point, do I check the response and raise a success or error event ?

    public bool StartFetch()
    {
        if (IsFetching) return false;
        IsFetching = true;

        mCancellationTokenSource = new CancellationTokenSource();

        // this is not awaited, so execution should continue
        StartTask(request, mCancellationTokenSource.Token);
        return true;
    }

    public bool CancelFetch()
    {
        // send cancellation
        if (mCancellationTokenSource != null)
            mCancellationTokenSource.Cancel();

        Cleanup();
        return true;
    }


    private async Task StartTask(LFHttpRequest request, CancellationToken cancellationToken)
    {
        var message = new HttpRequestMessage(request.Method, request.Uri);
        var response = await HttpClient.SendAsync(message, cancellationToken); 

        // at this point, do I take a look at response and raise a custom OnSuccess or OnError event???   

       // or do I want to grab the task from `SendAsync`, check for completed or faulted?
    }

When you're looking at exposing task-related state like IsFetching , it's often cleaner and easier to just expose the Task itself.

Something like this:

public Task<T> FetchTask { get; private set; }

public bool StartFetch()
{
    if (FetchTask != null) return false;

    mCancellationTokenSource = new CancellationTokenSource();
    FetchTask = FetchAsync(request, mCancellationTokenSource.Token);
    return true;
}

public bool CancelFetch()
{
    // send cancellation
    if (mCancellationTokenSource != null)
        mCancellationTokenSource.Cancel();

    FetchTask = null;
    return true;
}


private async Task<T> FetchAsync(LFHttpRequest request, CancellationToken cancellationToken)
{
    var message = new HttpRequestMessage(request.Method, request.Uri);
    var response = await HttpClient.SendAsync(message, cancellationToken); 
    response.EnsureSuccessStatusCode();
    var ret = // Convert response.Content into T.
    return ret;
}

I would recommend throwing InvalidOperationException s for the StartFetch and CancelFetch operations if the IsFetching state is invalid. This may seem annoying but it lets you catch programmer error and threading issues before they get to be a bigger, hidden problem.

As for your asynchronous approach, your method should return a result. So maybe something like private async Task<MyHttpResult> StartTask(...) . Your result should contain a way determine success, failure, and cancellation.

For example:

public sealed class MyHttpResult
{
  public HttpResponse Result { get; private set; }
  public Exception Error { get; private set; }
  public bool WasCancelled { get; private set; }

  public MyHttpResult(HttpResponse result, Exception error, bool wasCancelled)
  {
    this.Result = result;
    this.Error = error;
    this.WasCancelled = wasCancelled;
  }
}

Many async methods will throw a TaskCanceledException if they are cancelled, so you can catch that to signify, like so:

async Task<MyHttpResult> StartTask(LFHttpRequest request, CancellationToken cancellationToken)
{
  var message = new HttpRequestMessage(new HttpMethod(request.Method), request.Uri);

  HttpResponse response = null;
  Exception lastError = null;
  bool wasCancelled = false;

  try
  {
    response = await MessageInvoker.SendAsync(message, cancellationToken);
  }
  catch(TaskCanceledException)
  {
    wasCancelled = true;
  }
  catch(Exception ex)
  {
    lastError = ex;
  }

  var result = new MyHttpResult(response, lastError, wasCancelled);
  return result;
}

This is all assuming that your observers are also the callers, so they can await this method. If this is not the case, your idea of an EventHandler makes sense. Instead of returning the result, you could create a custom EventArgs class like so:

public delegate void TaskResultEventHandler<T>(object sender, TaskResultEventArgs<T> e);

public sealed class TaskResultEventArgs<T> : EventArgs
{
      public T Result { get; private set; }
      public Exception Error { get; private set; }
      public bool WasCancelled { get; private set; }

      public TaskResultEventArgs(T result, Exception error, bool wasCancelled)
      {
        this.Result = result;
        this.Error = error;
        this.WasCancelled = wasCancelled;
      }
}

Then it's simply a matter of exposing a TaskResultEventHandler<HttpResponse> and your observers subscribing to it. You could invoke it like so:

var handler = this.HttpTaskCompleted;

if(handler != null)
  handler(this, new TaskResultEventArgs<HttpResponse>(response, lastError, wasCancelled));

After you awaited the http call

var response = await HttpClient.SendAsync(message, cancellationToken); 

You should test for cancellation:

if(cancellationToken.IsCancellationRequested)
   //... do what you want, throw or return false or null, depending on how you want to handle this cancellation.

Or you can check and throw the Microsoft exception in one call :

cancel.ThrowIfCancellationRequested();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM