Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert asynchronous action to asynchronous function delegate, preserving synchronous exception delivery

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
like image 729
Douglas Avatar asked Jul 14 '26 06:07

Douglas


1 Answers

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;
}
like image 148
Jon Skeet Avatar answered Jul 17 '26 17:07

Jon Skeet



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!