I have an existing method with return type IAsyncResult from older code, how can I call an async Task from within this method?
The answer is in the TAP interop documentation. Essentially you return a TaskCompletionSource<T>.Task
.
Here's some helpers from my AsyncEx library that make this much easier:
public static IAsyncResult ToBegin<TResult>(Task<TResult> task, AsyncCallback callback, object state)
{
var tcs = new TaskCompletionSource<TResult>(state, TaskCreationOptions.RunContinuationsAsynchronously);
var oldContext = SynchronizationContext.Current;
SynchronizationContext.Current = null;
try
{
CompleteAsync(task, callback, tcs);
}
finally
{
SynchronizationContext.Current = oldContext;
}
return tcs.Task;
}
// `async void` is on purpose, to raise `callback` exceptions directly on the thread pool.
private static async void CompleteAsync<TResult>(Task<TResult> task, AsyncCallback callback, TaskCompletionSource<TResult> tcs)
{
try
{
tcs.TrySetResult(await task.ConfigureAwait(false));
}
catch (OperationCanceledException ex)
{
tcs.TrySetCanceled(ex.CancellationToken);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
finally
{
callback?.Invoke(tcs.Task);
}
}
public static TResult ToEnd<TResult>(IAsyncResult asyncResult) =>
((Task<TResult>)asyncResult).GetAwaiter().GetResult();
Note that the TaskCompletionSource<T>
is necessary to have the proper semantics for your state
argument.
With these, your implementation can look like:
public IAsyncResult BeginXXX(...rest, AsyncCallback callback, object state) =>
XXXAsync(...rest).ToBegin(callback, state);
public EndXXX(IAsyncResult asyncResult) => TaskExtensions.ToEnd<MyResult>(asyncResult);
public async Task<MyResult> XXXAsync(...rest)
{
var value = await DoMyTaskAsync();
DoSomethingWithValue(value);
return myResult;
}
Note that this assumes you're going to translate everything at this level into async
/await
and get rid of the new SomeOtherAsyncResult
. However, if you have a lower-level IAsyncResult
-based API that you are not ready to convert to async
/await
and that you still need to call, then your XXXAsync
method can use TAP-over-APM:
public async Task<MyResult> XXXAsync(...rest)
{
var value = await DoMyTaskAsync();
DoSomethingWithValue(value);
return await TaskFactory.FromAsync(BeginOtherAsyncResult, EndOtherAsyncResult,
...restOtherAsyncResultArgs, null);
}
In this case you end up with APM-over-TAP (to keep your BeginXXX
interface) and TAP-over-APM (to continue using the OtherAsyncResult
interface), with your TAP method sandwiched between (XXXAsync
).