How to add an async “await” to an addrange select

2019-03-29 13:29发布

问题:

I have a function like this:

public async Task<SomeViewModel> SampleFunction()
{
    var data = service.GetData();
    var myList = new List<SomeViewModel>();

    myList.AddRange(data.select(x => new SomeViewModel
    {
        Id = x.Id,
        DateCreated = x.DateCreated,
        Data = await service.GetSomeDataById(x.Id)
    }

    return myList;
}

My await isn't working as it can only be used in a method or lambda marked with the async modifier. Where do I place the async with this function?

回答1:

You can only use await inside an async method/delegate. In this case you must mark that lambda expression as async.

But wait, there's more...

Select is from the pre-async era and so it doesn't handle async lambdas (in your case it would return IEnumerable<Task<SomeViewModel>> instead of IEnumerable<SomeViewModel> which is what you actually need).

You can however add that functionality yourself (preferably as an extension method), but you need to consider whether you wish to await each item before moving on to the next (sequentialy) or await all items together at the end (concurrently).

Sequential async

static async Task<TResult[]> SelectAsync<TItem, TResult>(this IEnumerable<TItem> enumerable, Func<TItem, Task<TResult>> selector)
{
    var results = new List<TResult>();
    foreach (var item in enumerable)
    {
        results.Add(await selector(item));
    }
    return results.ToArray();
}

Concurrent async

static Task<TResult[]> SelectAsync<TItem, TResult>(this IEnumerable<TItem> enumerable, Func<TItem, Task<TResult>> selector)
{
    return Task.WhenAll(enumerable.Select(selector));
}

Usage

public Task<SomeViewModel[]> SampleFunction()
{
    return service.GetData().SelectAsync(async x => new SomeViewModel
    {
        Id = x.Id,
        DateCreated = x.DateCreated,
        Data = await service.GetSomeDataById(x.Id)
    }
}


回答2:

You're using await inside of a lambda, and that lambda is going to be transformed into its own separate named method by the compiler. To use await it must itself be async, and not just be defined in an async method. When you make the lambda async you now have a sequence of tasks that you want to translate into a sequence of their results, asynchronously. Task.WhenAll does exactly this, so we can pass our new query to WhenAll to get a task representing our results, which is exactly what this method wants to return:

public Task<SomeViewModel[]> SampleFunction()
{
    return Task.WhenAll(service.GetData().Select(
        async x => new SomeViewModel
    {
        Id = x.Id,
        DateCreated = x.DateCreated,
        Data = await service.GetSomeDataById(x.Id)
    }));
}


回答3:

Though maybe too heavyweight for your use case, using TPL Dataflow will give you finer control over your async processing.

public async Task<List<SomeViewModel>> SampleFunction()
{
    var data = service.GetData();

    var transformBlock = new TransformBlock<X, SomeViewModel>(
        async x => new SomeViewModel
        {
            Id = x.Id,
            DateCreated = x.DateCreated,
            Data = await service.GetSomeDataById(x.Id)
        },
        new ExecutionDataflowBlockOptions
        {
            // Let 8 "service.GetSomeDataById" calls run at once.
            MaxDegreeOfParallelism = 8
        });

    var result = new List<SomeViewModel>();

    var actionBlock = new ActionBlock<SomeViewModel>(
        vm => result.Add(vm));

    transformBlock.LinkTo(actionBlock,
        new DataflowLinkOptions { PropagateCompletion = true });

    foreach (var x in data)
    {
        transformBlock.Post(x);
    }
    transformBlock.Complete();

    await actionBlock.Completion;

    return result;
}

This could be substantially less long-winded if service.GetData() returned an IObservable<X> and this method returned an IObservable<SomeViewModel>.