2

Is it possible to return a task from a method which first calls multiple Task<T> returning methods and then returns some type that includes the results from previous calls without using await?

For example, the below is straight forward:

public Task<SomeType> GetAsync() => FirstOrDefaultAsync();

However, I would like to do something like this:

public Task<SomeType> GetAsync()
{
    var list = GetListAsync();   // <-- Task<List<T>>
    var count = GetCountAsync(); // <-- Task<int>

    return new SomeType // <-- Obviously compiler error
    {
        List  /* <-- List<T> */ = list,  // <-- Also compiler error
        Count /* <-- int     */ = count, // <-- Also compiler error
    };
}

Is it possible to do this without having to write:

public async Task<SomeType> GetAsync()
{
    return new Type2
    {
        List = await GetListAsync(),
        Count = await GetCountAsync(),
    };
}
Tom Vast
  • 39
  • 3
  • Calling `GetListAsync` starts a task, the return value will be generated sometime in the future, when you say `await GetListAsync()`, you are telling wait till that return value is received, so How do you expect to assign a value that will be generated in sometime in the future right now to a variable? – Mat J Sep 15 '19 at 11:42
  • 1
    Why? `async/await` was invented to make this more readable and manageable. – H H Sep 15 '19 at 12:00
  • @HenkHolterman Because I read that having multiple await is bad for performance. I try to await on last call – Tom Vast Sep 15 '19 at 12:23
  • 1
    @TomVast I thought you were trying to avoid refactoring existing codebase, but that is a [wrong reason](https://stackoverflow.com/a/21407306/11683) to do it. – GSerg Sep 15 '19 at 12:34
  • @TomVast - you heard wrong. But maybe you want to overlap both tasks with `await WhenAll()` , depending on your app type. – H H Sep 15 '19 at 13:32

3 Answers3

8

Frankly, the version already in the question is correct:

public async Task<SomeType> GetAsync()
{
    return new Type2
    {
        List = await GetListAsync(),
        Count = await GetCountAsync(),
    };
}

I realize you asked "without using await", but: the hacks to avoid the await are suboptimal; in particular, you should almost never use ContinueWith - that is the legacy API, and the Task implementation is now optimized for await, not ContinueWith.

As for:

Because I read that having multiple await is bad for performance. I try to await on last call

No; once you have one incomplete await, it pretty much doesn't matter how many more you have - they're effectively free. The issue of having one vs zero incomplete await is comparable to the ContinueWith, so : you're not gaining anything by avoiding the await.

Conclusion: just use the await. It is simpler and more direct, and the internals are optimized for it.

As a minor optimization, you might want to add ConfigureAwait(false), i.e.

public async Task<SomeType> GetAsync()
{
    return new Type2
    {
        List = await GetListAsync().ConfigureAwait(false),
        Count = await GetCountAsync().ConfigureAwait(false),
    };
}

Or if they should run concurrently, and the implementation supports it:

public Task<SomeType> GetAsync()
{
    var list = GetListAsync(); 
    var count = GetCountAsync();

    return new SomeType
    {
        List = await list.ConfigureAwait(false),
        Count = await count.ConfigureAwait(false),
    };
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
0

You can use Task.WhenAll together with Task.ContinueWith.

public Task<SomeType> GetAsync()
{
    var list = GetListAsync();
    var count = GetCountAsync();

    return Task.WhenAll(list, count).ContinueWith(_ => new Type2
    {
        List = list.Result,
        Count = count.Result,
    });
}

Edit

As suggested in comments, you're better off just using await. I also advice to read the post linked by GSerg - Performance of Task.ContinueWith in non-async method vs. using async/await

Alaa Masoud
  • 7,085
  • 3
  • 39
  • 57
  • 1
    Note that it will be throwing `AggregateException`s in case of errors. You will need to [unwrap](https://stackoverflow.com/q/21520869/11683) it. – GSerg Sep 15 '19 at 11:55
  • 3
    This works, but is it any better? In what way? The sane thing to do here is just use `await`. – H H Sep 15 '19 at 12:01
  • @HenkHolterman It could be depending on implementation. I am assuming he wants to `await` on `GetAsync` and have both `GetListAsync` and `GetCountAsync` run in parallel. Depends on what these 2 methods do though.. It probably would be easier to just do `await Task.WhenAll(...)` – Alaa Masoud Sep 15 '19 at 12:06
  • @HenkHolterman That requires declaring `GetAsync` as `async`, which in turn will make all its callers async etc. Apparently the OP is trying to use async while not refactoring the existing methods to be async. – GSerg Sep 15 '19 at 12:09
  • @GSerg: No, `async` only affects the method itself, not the callers. Either way it just returns a `Task`. – H H Sep 15 '19 at 12:10
  • @HenkHolterman Then it only moves the point of ugliness from this method one step up the chain, because the caller, not being `async`, will also have to cope without `await`. – GSerg Sep 15 '19 at 12:12
  • It moves the discussion one step up. It changes nothing about how to write _this_ method. – H H Sep 15 '19 at 12:13
  • This is a bad way of doing things, frankly; the original code in the question *that uses await* would be hugely preferable – Marc Gravell Sep 15 '19 at 12:52
0

The problem is that the Task.WhenAll method does not accept tasks with different result types. All tasks must be of the same type. Fortunately this is easy to fix. The WhenAll variant bellow waits for two tasks with different types, and returns a task with the combined results.

public static Task<TResult> WhenAll<T1, T2, TResult>(
    Task<T1> task1, Task<T2> task2, Func<T1, T2, TResult> factory)
{
    return Task.WhenAll(task1, task2).ContinueWith(t =>
    {
        var tcs = new TaskCompletionSource<TResult>();
        if (t.IsFaulted)
        {
            tcs.SetException(t.Exception.InnerExceptions);
        }
        else if (t.IsCanceled)
        {
            tcs.SetCanceled();
        }
        else
        {
            tcs.SetResult(factory(task1.Result, task2.Result));
        }
        return tcs.Task;
    }, default, TaskContinuationOptions.ExecuteSynchronously,
        TaskScheduler.Default).Unwrap();
}

It can be used like this:

public Task<SomeType> GetAsync()
{
    return WhenAll(GetListAsync(), GetCountAsync(),
        (list, count) => new SomeType { List = list, Count = count });
}

The advantage over the other solutions is at the handling of exceptions. If both GetListAsync and GetCountAsync fail, the task returned from GetAsync will preserve both exceptions in a shallow AggregateException (not nested in another AggregateException).

Btw this answer is inspired by a Stephen Cleary's answer here.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104