async/await inside LINQ where clause not working

Viewed 1074

I'm tring to make a database query inside a LINQ statement asynchronous, but I'm running into an error. The code below runs fine with out async/await

    var newEntities = _repositoryMapping.Mapper.Map<List<Entry>>(entries);

    newEntities = newEntities.Where(async e => await !_context.Entries.AnyAsync(c => c.Id == e.Id)).ToList();

Severity Code Description Project File Line Suppression State Error CS4010 Cannot convert async lambda expression to delegate type 'Func<Entry, bool>'. An async lambda expression may return void, Task or Task, none of which are convertible to 'Func<Entry, bool>'

Other than breaking this up into a foreach loop, how can I make this work with async/await?

2 Answers

If you care about performance, code should be smarter. You just need to send one query and check what is already present in database.

Prepared extension which can do that in generic way:

newEntities = (await newEntities.FilterExistentAsync(_context.Entries, e => e.Id)).ToList();

Implementation is not so complex

public static class QueryableExtensions
{
    public static async Task<IEnumerable<T>> FilterExistentAsync<T, TProp>(this ICollection<T> items,
        IQueryable<T> dbQuery, Expression<Func<T, TProp>> prop, CancellationToken cancellationToken = default)
    {
        var propGetter = prop.Compile();
        var ids = items.Select(propGetter).ToList();
        var parameter = prop.Parameters[0];

        var predicate = Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(TProp) }, Expression.Constant(ids), prop.Body);
        var predicateLambda = Expression.Lambda(predicate, parameter);

        var filtered = Expression.Call(typeof(Queryable), "Where", new[] {typeof(T)}, dbQuery.Expression,
            predicateLambda);

        var selectExpr = Expression.Call(typeof(Queryable), "Select", new[] {typeof(T), typeof(TProp)}, filtered, prop);
        var selectQuery = dbQuery.Provider.CreateQuery<TProp>(selectExpr);

        var existingIds = await selectQuery.ToListAsync(cancellationToken);

        return items.Where(i => !existingIds.Contains(propGetter(i)));
    }
}

For the Exception, you can add a extension for IEnumerable to support async

    public static class MyExtensions
    {
        public static async Task<IEnumerable<T>> Where<T>(this IEnumerable<T> source, 
                                                          Func<T, Task<bool>> func)
        {
            var tasks = new List<Task<bool>>();

            foreach (var element in source)
            {
                tasks.Add(func(element));
            }

            var results = await Task.WhenAll<bool>(tasks.ToArray());

            var trueIndex = results.Select((x, index) => new { x, index })
                                   .Where(x => x.x)
                                   .Select(x => x.index).ToList();

            var filterSource = source.Where((x, index) => trueIndex.Contains(index));

            return filterSource;
        }
    }

Then you can use someting like below

 var result = await users.Where(async x => await TestAsync(x));

Full code here https://dotnetfiddle.net/lE2swz

Related