1

I'm working on a WCF service/client trying to figure out how to replace a ManualResetEvent with something that won't block the caller thread.
Most important is that await client.CloseAsync() won't be called until the FinishedEventReceived event has been fired.

I've looked at using a TaskCompletionSource but I'm somewhat unsure how that would work in this case.

I know the code is a bit ugly and totally defeats the purpose of using asynchrounously programming, my apoligies.
Any ideas?

private async Task CallServiceMethodAndWaitForEvent()
{
    var mre = new ManualResetEvent(true);

    var client = new AwesomeClient();
    client.FinishedEventReceived += (s, e) =>
    {
        // Do something with the result, this event is only fired once.
        mre.Set();
    };
    client.UpdateEventReceived += (s, e) =>
    {
        // This even can fire several times before the finished event.
    };

    try
    {
        var parameters = new Parameters()
        {
            SomeParameter = "Test123",
            TestAmount = 10000,
        };

        var errors = await client.DoWorkAsync(parameters);
        Debug.WriteLine(errors);

        mre.WaitOne(TimeSpan.FromSeconds(20));
        await client.CloseAsync();
    }
    catch (FaultException ex)
    {
    }
    catch (Exception)
    {
        client.Abort();
    }
}
  • "I know the code is a bit ugly and totally defeats the purpose of using asynchrounously programming, my apoligies." - actually, I don't think it does *at all* (well, apart from the blocking `WaitOne`, but that is presumably what you're trying to fix here) – Marc Gravell Jul 19 '18 at 09:40

2 Answers2

3

Probably the simplest way to do what you want would be to replace the ManualResetEvent with - as you mentioned - a TaskCompletionSource. For example:

var tcs = new TaskCompletionSource<int>();

var client = new AwesomeClient();
client.FinishedEventReceived += (s, e) =>
{
    // Do something with the result, this event is only fired once.
    tcs.SetResult(42); // number here is a dummy, since you only want Task
};

...

await tcs.Task;
await client.CloseAsync();

Note that the timeout aspect is harder; a common approach there is to use Task.Delay as a fallback, and Task.WhenAny, i.e.

var timeout = Task.Delay(timeoutInterval);
if (timeout == await Task.WhenAny(timeout, tcs.Task))
    throw new TimeoutException();
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
1

It looks like you are working with some class that implements the Event-based Asynchronous Pattern. What you really want, if you're doing async, is to work with APIs that implement the Task-based Asynchronous Pattern.

Thankfully, Microsoft offer specific guidance on adapting EAP to look like TAP:

Wrapping an Event-based Asynchronous Pattern (EAP) implementation is more involved than wrapping an APM pattern, because the EAP pattern has more variation and less structure than the APM pattern. To demonstrate, the following code wraps the DownloadStringAsync method. DownloadStringAsync accepts a URI, raises the DownloadProgressChanged event while downloading in order to report multiple statistics on progress, and raises the DownloadStringCompleted event when it's done. The final result is a string that contains the contents of the page at the specified URI.

public static Task<string> DownloadStringAsync(Uri url)
 {
     var tcs = new TaskCompletionSource<string>();
     var wc = new WebClient();
     wc.DownloadStringCompleted += (s,e) =>
         {
             if (e.Error != null) 
                tcs.TrySetException(e.Error);
             else if (e.Cancelled) 
                tcs.TrySetCanceled();
             else 
                tcs.TrySetResult(e.Result);
         };
     wc.DownloadStringAsync(url);
     return tcs.Task;
}

Which hopefully you can adapt to the specific API you're working with.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448