Как добавить асинхронный "ожидание" в оператор выбора добавления?

У меня есть такая функция:

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 не работает, поскольку его можно использовать только в методе или лямбда, отмеченном модификатором async. Где я могу разместить async с помощью этой функции?

Ответы

Ответ 1

Вы можете использовать только await внутри метода/делегата async. В этом случае вы должны отметить это лямбда-выражение как async.

Но подождите, там еще...

Select относится к периоду до async, поэтому он не обрабатывает async lambdas (в вашем случае он возвратит IEnumerable<Task<SomeViewModel>> вместо IEnumerable<SomeViewModel>, что вам действительно нужно).

Однако вы можете добавить эту функциональность самостоятельно (желательно как метод расширения), но вам нужно рассмотреть, хотите ли вы await каждого элемента, прежде чем переходить к следующему (последовательному) или await всем элементам вместе end (одновременно).

Последовательный 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();
}

Параллельный async

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

Использование

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

Вы используете await внутри лямбда, и эта лямбда будет преобразована в свой отдельный именованный метод компилятором. Чтобы использовать await, он должен быть async, а не просто определен в методе async. Когда вы создаете лямбда async, у вас теперь есть последовательность задач, которые вы хотите перевести в последовательность своих результатов асинхронно. Task.WhenAll делает именно это, поэтому мы можем передать наш новый запрос в WhenAll, чтобы получить задачу, представляющую наши результаты, что именно этот метод хочет вернуть:

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

Хотя, возможно, слишком тяжелый для вашего случая использования, используя TPL Dataflow, вы получите более тонкий контроль над обработкой async.

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;
}

Это может быть значительно менее длинным, если service.GetData() возвратил IObservable<X>, и этот метод возвратил IObservable<SomeViewModel>.