3

I want to convert asynchronous action delegates to asynchronous function delegates that return a specified value. I have come up with an extension method for this:

public static Func<Task<TResult>> Return<TResult>(this Func<Task> asyncAction, TResult result)
{
    ArgumentValidate.NotNull(asyncAction, nameof(asyncAction));

    return async () =>
    {
        await asyncAction();
        return result;
    };
}

However, my extension method is buggy in that exceptions that would have been delivered synchronously from the action delegate now get delivered asynchronously from the function delegate. Concretely:

Func<Task> asyncAction = () => { throw new InvalidOperationException(); };
var asyncFunc = asyncAction.Return(42);
var task = asyncFunc();   // exception should be thrown here
await task;               // but instead gets thrown here

Is there a way of creating this wrapper in such a way that synchronous exceptions continue to delivered synchronously? Is ContinueWith the way to go?

Update: A concrete example of an asynchronous operation that throws exceptions synchronously:

public static Task WriteAllBytesAsync(string filePath, byte[] bytes)
{
    if (filePath == null)
        throw new ArgumentNullException(filePath, nameof(filePath));
    if (bytes == null)
        throw new ArgumentNullException(filePath, nameof(bytes));

    return WriteAllBytesAsyncInner(filePath, bytes);
}

private static async Task WriteAllBytesAsyncInner(string filePath, byte[] bytes)
{
    using (var fileStream = File.OpenWrite(filePath))
        await fileStream.WriteAsync(bytes, 0, bytes.Length);
}

Test:

Func<Task> asyncAction = () => WriteAllBytesAsync(null, null);
var asyncFunc = asyncAction.Return(42);
var task = asyncFunc();   // ArgumentNullException should be thrown here
await task;               // but instead gets thrown here
Douglas
  • 53,759
  • 13
  • 140
  • 188
  • can you provide a more concrete example? – Daniel A. White Feb 25 '16 at 17:02
  • It's not clear why you'd expect the exception to be thrown synchronously. If you call `asyncAction()` *that* won't throw an exception either - it will return a faulted task. – Jon Skeet Feb 25 '16 at 17:03
  • @JonSkeet: You're right; I had a small bug in my example (the anonymous function should not have been marked as `async`). Does the question make sense now? – Douglas Feb 25 '16 at 17:07
  • @Douglas: Hmm. Only because your `Func` isn't actually doing *anything* async. Presumably normally it would be code which actually used async/await, and just happened to throw an exception? At that point, again, you'd end up with calling `asyncAction()` not throwing. – Jon Skeet Feb 25 '16 at 17:08
  • @DanielA.White: For a concrete example of where one would use this, look at the third overload in the last code snippet (under "Update") of [my answer here](http://stackoverflow.com/a/35613744/1149773). – Douglas Feb 25 '16 at 17:10
  • @JonSkeet: Yes. I have asynchronous operations that throw initial exceptions synchronously (mainly `ArgumentException`), and the remainder asynchronously through the returned `Task`. I want to preserve this behaviour in my `Return` transformation. – Douglas Feb 25 '16 at 17:11
  • @Douglas: I suspect that if you show an example of how those asynchronous operations are implemented, you'll get an example of what to do in your `Return` method... – Jon Skeet Feb 25 '16 at 17:12

1 Answers1

7

Well, you won't be able to use async in the initial call. That much is clear. But you can use a synchronous delegate which calls the function, and then captures the returned task to await it inside an async delegate:

public static Func<Task<TResult>> Return<TResult>(this Func<Task> asyncAction, TResult result)
{
    ArgumentValidate.NotNull(asyncAction, nameof(asyncAction));

    return () =>
    {
        // Call this synchronously
        var task = asyncAction();
        // Now create an async delegate for the rest
        Func<Task<TResult>> intermediate = async () => 
        {
            await task;
            return result;
        };
        return intermediate();
    };
}

Alternatively, refactor it into two methods, basically extracting the asynchronous lambda expression into an async method:

public static Func<Task<TResult>> Return<TResult>(
    this Func<Task> asyncAction, TResult result)
{
    ArgumentValidate.NotNull(asyncAction, nameof(asyncAction));

    return () =>
    {
        var task = asyncAction();
        return AwaitAndReturn(task, result);
    };
}

public static async Func<Task<TResult>> AwaitAndReturn<TResult>(
    this Task asyncAction, TResult result)
{
    await task;
    return result;
}
Douglas
  • 53,759
  • 13
  • 140
  • 188
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194