在 .Select lambda 中使用 async / await

Posted

技术标签:

【中文标题】在 .Select lambda 中使用 async / await【英文标题】:Using async / await inside .Select lambda 【发布时间】:2017-01-23 21:30:39 【问题描述】:

我正在使用 Asp.Net Core Identity 并尝试简化一些将用户列表及其角色投影到 ViewModel 的代码。这段代码有效,但为了简化它,我陷入了错误和好奇的疯狂漩涡。

这是我的工作代码:

        var allUsers = _userManager.Users.OrderBy(x => x.FirstName);
        var usersViewModel = new List<UsersViewModel>();

        foreach (var user in allUsers)
        
            var tempVm = new UsersViewModel()
            
                Id = user.Id,
                UserName = user.UserName,
                FirstName = user.FirstName,
                LastName = user.LastName,
                DisplayName = user.DisplayName,
                Email = user.Email,
                Enabled = user.Enabled,
                Roles = String.Join(", ", await _userManager.GetRolesAsync(user))
            ;

            usersViewModel.Add(tempVm);
        

在尝试简化代码时,我想我可以做这样的事情 (损坏的代码)

        var usersViewModel = allUsers.Select(user => new UsersViewModel
        
            Id = user.Id,
            UserName = user.UserName,
            FirstName = user.FirstName,
            LastName = user.LastName,
            DisplayName = user.DisplayName,
            Email = user.Email,
            Enabled = user.Enabled,
            Roles = string.Join(", ", await _userManager.GetRolesAsync(user))
        ).ToList();

这会中断,因为我没有在 user 之前的 lambda 表达式中使用 async 关键字。但是,当我在 user 之前添加异步时,我收到另一个错误,提示 “异步 lambda 表达式无法转换为表达式树”

我的猜测是 GetRolesAsync() 方法返回一个任务并将其分配给角色,而不是该任务的实际结果。我一生似乎无法弄清楚如何让它发挥作用。

过去一天我研究并尝试了很多方法,但没有运气。以下是我看过的一些:

Is it possible to call an awaitable method in a non async method?

https://blogs.msdn.microsoft.com/pfxteam/2012/04/12/asyncawait-faq/

Calling async method in IEnumerable.Select

How to await a list of tasks asynchronously using LINQ?

how to user async/await inside a lambda

How to use async within a lambda which returns a collection

诚然,我并不完全理解 async / await 是如何工作的,所以这可能是问题的一部分。我的 foreach 代码有效,但我希望能够了解如何使其以我尝试的方式工作。因为我已经花了很多时间在上面,所以我认为这是一个很好的第一个问题。

谢谢!

编辑

我想我必须解释我在我研究的每篇文章中所做的事情,以免被标记为重复问题 - 我非常努力地避免这种情况:-/。虽然这个问题听起来很相似,但结果却并非如此。对于被标记为答案的文章,我尝试了以下代码:

    public async Task<ActionResult> Users()
    
        var allUsers = _userManager.Users.OrderBy(x => x.FirstName);
        var tasks = allUsers.Select(GetUserViewModelAsync).ToList();
        return View(await Task.WhenAll(tasks));
    

    public async Task<UsersViewModel> GetUserViewModelAsync(ApplicationUser user)
    
        return new UsersViewModel
        
            Id = user.Id,
            UserName = user.UserName,
            FirstName = user.FirstName,
            LastName = user.LastName,
            DisplayName = user.DisplayName,
            Email = user.Email,
            Enabled = user.Enabled,
            Roles = String.Join(", ", await _userManager.GetRolesAsync(user))
        ;
    

我也尝试过像这样使用 AsEnumerable:

    var usersViewModel = allUsers.AsEnumerable().Select(async user => new UsersViewModel
        
            Id = user.Id,
            UserName = user.UserName,
            FirstName = user.FirstName,
            LastName = user.LastName,
            DisplayName = user.DisplayName,
            Email = user.Email,
            Enabled = user.Enabled,
            Roles = string.Join(", ", await _userManager.GetRolesAsync(user))
        ).ToList();

这两个都会产生错误消息:“InvalidOperationException:在前一个操作完成之前在此上下文上启动了第二个操作。不保证任何实例成员都是线程安全的。”

在这一点上,我原来的 ForEach 似乎是最好的选择,但我仍然想知道如果我使用异步方法来做这件事的正确方法是什么。

编辑 2 - 带答案 感谢 Tseng 的 cmets(和其他一些研究),我能够使用以下代码使事情正常进行:

        var userViewModels = allUsers.Result.Select(async user => new UsersViewModel
        
            Id = user.Id,
            UserName = user.UserName,
            FirstName = user.FirstName,
            LastName = user.LastName,
            DisplayName = user.DisplayName,
            Email = user.Email,
            Enabled = user.Enabled,
            Roles = string.Join(", ", await _userManager.GetRolesAsync(user))
        );
        var vms = await Task.WhenAll(userViewModels);
        return View(vms.ToList());

虽然现在我已将每个人的 cmets 都考虑在内,但我开始更仔细地查看 SQL Profiler,以了解 DB 实际上获得了多少命中 - 正如 Matt Johnson 所提到的,它很多 (N+1)。

因此,虽然这确实回答了我的问题,但我现在正在重新考虑如何运行查询,并且可能只是将角色放在主视图中,并且仅在选择每个用户时才拉出它们。不过,我确实通过这个问题学到了很多东西(并且学到了更多我不知道的东西),所以谢谢大家。

【问题讨论】:

My guess is that the GetRolesAsync() method is returning a Task and assigning it to Roles instead of the actual results of that task. - 可能不会,因为await 会获取任务的结果。 尝试在Select 之前放置一个AsEnumerable,这样它将在LInq to Objects 中运行,而不是尝试将其转换为EF 或您使用的任何提供程序的表达式树。 var usersViewModels = (await Task.WhenAll(allUsers.AsEnumerable().Select(async user => new UsersViewModel Id = user.Id, UserName = user.UserName, FirstName = user.FirstName, LastName = user.LastName, DisplayName = user.DisplayName, Email = user.Email, Enabled = user.Enabled, Roles = string.Join(", ", await _userManager.GetRolesAsync(user)) ))).ToList() ; 因此,您拨打一个电话以获取所有用户,然后为每个用户单独拨打电话以获取他们的角色。这是著名的反模式,称为“Select N+1”(Google/Bing it)。你真的不应该这样做。当你有 10,000 个用户时会发生什么?您真的要进行 10,001 次数据库调用吗? 可能重复:Multi-async in Entity Framework 6? 【参考方案1】:

我认为你在这里混合了两件事。表达式树和代表。 Lambda 可以用来表达这两种情况,但这取决于方法接受的参数类型,它将被转换到哪个参数中。

传递给作为Action&lt;T&gt;Func&lt;T, TResult&gt; 的方法的lambda 将被转换为委托(基本上是匿名函数/方法)。

当您将 lambda 表达式传递给接受 Expression&lt;T&gt; 的方法时,您会从 lambda 中创建一个表达式树。表达式树只是描述代码的代码,而不是代码本身。

话虽如此,表达式树无法执行,因为它已转换为可执行代码。您可以在运行时编译表达式树,然后像委托一样执行它。

ORM 框架使用表达式树来允许您编写“代码”,这些“代码”可以翻译成不同的东西(例如数据库查询)或在运行时动态生成代码。

因此,您不能在接受Expression&lt;T&gt; 的方法中使用async。当您将其转换为AsEnumerable() 时它可能会起作用的原因是因为它返回一个IEnumerable&lt;T&gt; 并且其上的LINQ 方法接受Func&lt;T, TResult&gt;。但它本质上是获取整个查询并在内存中完成所有工作,因此您不能使用投影(或者您必须在仅使用表达式和投影之前获取数据),将过滤后的结果转换为列表然后过滤它。

你可以试试这样的:

// Filter, sort, project it into the view model type and get the data as a list
var users = await allUsers.OrderBy(user => user.FirstName)
                             .Select(user => new UsersViewModel
    
        Id = user.Id,
        UserName = user.UserName,
        FirstName = user.FirstName,
        LastName = user.LastName,
        DisplayName = user.DisplayName,
        Email = user.Email,
        Enabled = user.Enabled
    ).ToListAsync();

// now that we have the data, we iterate though it and 
// fetch the roles
var userViewModels = users.Select(async user => 

    user.Roles = string.Join(", ", await _userManager.GetRolesAsync(user))
);

第一部分将完全在数据库上完成,您将保留所有优势(即订单发生在数据库上,因此您不必在获取结果后进行内存排序,并且限制调用会限制数据从数据库等中获取)。

第二部分遍历内存中的结果并为每个临时模型获取数据,最后将其映射到视图模型中。

【讨论】:

【参考方案2】:

这是一个解决方案,您可以使用List 作为回报。

var userViewModels = (await allUsers).Select(async user => new UsersViewModel
        
            Id = user.Id,
            UserName = user.UserName,
            FirstName = user.FirstName,
            LastName = user.LastName,
            DisplayName = user.DisplayName,
            Email = user.Email,
            Enabled = user.Enabled,
            Roles = string.Join(", ", await _userManager.GetRolesAsync(user))
        ).Select(q => q.Result);

更新 1:

感谢@TheodorZoulias,我意识到.Select(q =&gt; q.Result) 会导致线程阻塞。 所以我认为在某人找到更好的方法之前使用这个解决方案会更好。 它也可以随机播放项目。

List<UsersViewModel> userViewModels = new();
(await allUsers)
.Select(async user => new UsersViewModel()

    //(...)
    Roles = string.Join(", ", await _userManager.GetRolesAsync(user))
)
.ToList()
.ForEach(async q => userViewModels.Add(await q));

【讨论】:

allUsers.Result?这不会阻塞当前线程吗? @TheodorZoulias:是的,你是对的。非常感谢您的评论。我以前用这种方式,但现在不是一个好主意。你知道任何返回List&lt;T&gt;而不是Task&lt;List&lt;T&gt;&gt;的正确解决方案吗? 不,我不知道。 AFAIK 唯一正确的解决方案是async all the way。 @TheodorZoulias Async All the Way 在 API 控制器中使用时没有用,最好不要返回 Tasks。我将我的答案更新为一些更好的解决方案。让我们讨论一下。 你又是对的,我的重点是返回类型。我再次更新了答案,现在看起来更紧凑了。【参考方案3】:

扩展方法:

public static async Task<IEnumerable<TDest>> SelectSerialAsync<TSource, TDest>(this IEnumerable<TSource> sourceElements, Func<TSource, Task<TDest>> func)

    List<TDest> destElements = new List<TDest>();

    foreach (TSource sourceElement in sourceElements)
    
        TDest destElement = await func(sourceElement);
        destElements.Add(destElement);
    

    return destElements;

用法:

DestType[] array = (await sourceElements.SelectSerialAsync<SourceType, DestType>(
    async (sourceElement) =>  return await SomeAsyncMethodCall(sourceElement); 
)).ToArray();

【讨论】:

“携带延迟执行语义”是什么意思?我返回 IEnumerable 的原因是因为 Linq 库的其余部分也这样做。我只想保持一致。我对调用者执行 ToList() 或 ToArray(),无论我需要什么。由于此方法适用于源类型和目标类型,因此我更喜欢 TSource 和 TDest。不过,Tnx 用于 Linq.Async 提示。 当您说“延期”时,您可能指的是“收益”。我从不使用它。在我的用例中,我不需要它,afaik。因此,它也不会出现在我的扩展方法中。但你知道吗,我会研究“产量”。在我了解更多信息后,我可能会发现它的用途。 这就是我将它添加到我自己的扩展方法集合中的原因。它完全符合我的需要。以及 OP 需要它做什么。所以我想我会分享。 一开始是关于我的类型的命名。 没有任何误导。 yield 的使用不需要与 IEnumerable 一起使用。我看不出重命名类型会如何使 3 行代码 sn-p 更好。但可以肯定的是,我会做一个小的编辑,因为你提供。

以上是关于在 .Select lambda 中使用 async / await的主要内容,如果未能解决你的问题,请参考以下文章

AWS Lambda 函数总是返回 null (Node/Javascript)?

Lambda表达式中 select 怎么动态添加查询字段

lambda 表达式使用 select 和 where 子句连接多个表

使用 LINQ Select 和 Lambda 避免 2100 rpc 限制

TypeError:无法提取-Javascript Asyn问题

lambda表达式中的Select和Where的区别?