简体   繁体   中英

Using async and await when external Input/output API provides its own callback delegates

I have a class library that (amongst other things) acts as a wrapper for an external client library for a web API.

My (simplified) code here takes a query string, generates a ReportUtilities object, uses this to download a report, then returns the report string back to the caller:

public string GetXmlReport(string queryString)
{
    ReportUtilities reportUtil = new ReportUtilities(_user,queryString,"XML");

    byte[] data = reportUtil.GetResponse().Download();
    return Encoding.UTF8.GetString(data);
}

The issue is that this method is downloading data from a webservice using a syncronous method. I would like to make this available asyncronously in my library by adding a GetXmlReportAsync() method to my class.

Now, this would be straighforward if the ReportUtilities class provided a GenerateAndDownloadAsync() method returning Task<byte[]> but unfortunately it does not and it is in the external library so I am stuck with what it provides.

The ReportUtilities class does have a GetResponseAsync() method that returns void and provides a delegate for aa OnReadyCallback method together with an OnReadyCallback OnReady{get;set;} property.

I should add that .GetResponse() returns a ReportResponse object which does have a DownloadAsync() method but, again, this returns void rather than Task<byte[]> . Again ReportResponse comes with OnDownloadSuccessCallback delegate and OnDownloadSuccessCallback OnDownloadSuccess { get; set; } OnDownloadSuccessCallback OnDownloadSuccess { get; set; } OnDownloadSuccessCallback OnDownloadSuccess { get; set; } property.

It's almost as if the external library authors are 'rolling thier own' async API rather than using the one built into C#?

My question is: How can I implement a GetXmlReportAsync() method in my class to make the most efficient use of the asyncronous functions in the client library?

Obviously I could just do:

public async Task<string> GetXmlReportAsync(string queryString)
{
    ReportUtilities reportUtil = new ReportUtilities(_user,queryString,"XML");

    byte[] data = await Task.Run(() => { return reportUtil.GetResponse().Download(); });
    return Encoding.UTF8.GetString(data);
}

but then the thread gets tied up with 2 syncronous input/output method calls to the external library: .GetResponse() and .Download() which surely isnt optimal?

Alternatively, I could imagine a situation where I just exposed a similar API to the external library in my own with clients having to provide callbacks for when thier reports are ready but I would much prefer to wrap this up into the more familiar async/await style API.

Am I trying to fit a square peg in a round hole or am I missing a neat way to wrap this into an async/await style API?

How can I implement a GetXmlReportAsync() method in my class to make the most efficient use of the asyncronous functions in the client library?

You could wrap the asynchoronous GetResponseAsync call with a TaskCompletionSource<string> . It would register the delegate once complete and set the completion of the task via SetResult . It would look something of this sort:

public Task<string> GetXmlReportAsync()
{
    var tcs = new TaskCompletionSource<string>();
    ReportUtilities reportUtil = new ReportUtilities(_user,queryString,"XML");

    reportUtil.GetResponseAsync(callBack => 
    {
        // I persume this would be the callback invoked once the download is done
        // Note, I am assuming sort sort of "Result" property provided by the callback,
        // Change this to the actual property
        byte[] data = callBack.Result;
        tcs.SetResult(Encoding.UTF8.GetString(data));
    });

    return tcs.Task;
}

It's almost as if the external library authors are 'rolling thier own' async API rather than using the one built into C#?

A not uncommon situation for older libraries, particularly ones that were directly ported from other platforms/languages.

Am I trying to fit a square peg in a round hole or am I missing a neat way to wrap this into an async/await style API?

The pattern they're using is very similar to EAP , and there's a common pattern for converting EAP to TAP . You can do something similar with a few adjustments.

I recommend creating extension methods for the third-party library types that give you nice TAP endpoints, and then building your logic on top of that. That way, the TAP method doesn't mix concerns (translating asynchronous patterns, and doing business logic - ie, converting to a string).

The ReportUtilities class does have a GetResponseAsync() method that returns void and provides a delegate for aa OnReadyCallback method together with an OnReadyCallback OnReady{get;set;} property.

Something like this, then:

public static Task<ReportResponse> GetResponseTaskAsync(this ReportUtilities @this)
{
  var tcs = new TaskCompletionSource<ReportResponse>();
  @this.OnReady = response =>
  {
    // TODO: check for errors, and call tcs.TrySetException if one is found.
    tcs.TrySetResult(response);
  };
  @this.GetResponseAsync();
  return tcs.Task;
}

Similarly for the next level:

public static Task<byte[]> DownloadTaskAsync(this ReportResponse @this)
{
  var tcs = new TaskCompletionSource<byte[]>();
  // TODO: how to get errors? Is there an OnDownloadFailed?
  @this.OnDownloadSuccess = result =>
  {
    tcs.TrySetResult(result);
  };
  @this.DownloadAsync();
  return tcs.Task;
}

Then your business logic can use the clean TAP endpoints:

public async Task<string> GetXmlReportAsync(string queryString)
{
  ReportUtilities reportUtil = new ReportUtilities(_user, queryString, "XML");

  var response = await reportUtil.GetResponseTaskAsync().ConfigureAwait(false);
  var data = await response.DownloadTaskAsync().ConfigureAwait(false);
  return Encoding.UTF8.GetString(data);
}

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