简体   繁体   English

在 .Select lambda 中使用 async / await

[英]Using async / await inside .Select lambda

I am using Asp.Net Core Identity and trying to simplify some code that projects a list of users and their roles to a ViewModel.我正在使用 Asp.Net Core Identity 并试图简化一些将用户及其角色列表投影到 ViewModel 的代码。 This code works, but in trying to simplify it I have gone into a crazy spiral of errors and curiosity.这段代码有效,但在试图简化它时,我陷入了错误和好奇心的疯狂漩涡。

Here is my working code:这是我的工作代码:

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

In trying to simplify the code, I figured I could do something like this (broken code) :在尝试简化代码时,我想我可以做这样的事情(损坏的代码)

        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();

This breaks because I'm not using the async keyword in the lambda expression before user .这会中断,因为我没有在user之前的 lambda 表达式中使用 async 关键字。 However, when I do add async before user , I get yet another error that says "Async lambda expressions cannot be converted to expression trees"但是,当我在user之前添加 async 时,我又收到另一个错误,提示“Async lambda 表达式无法转换为表达式树”

My guess is that the GetRolesAsync() method is returning a Task and assigning it to Roles instead of the actual results of that task.我的猜测是 GetRolesAsync() 方法正在返回一个任务并将其分配给角色而不是该任务的实际结果。 What I can't seem to figure out for the life of me is how to make it work.对于我的生活,我似乎无法弄清楚如何让它发挥作用。

I have researched and tried many methods over the past day with no luck.在过去的一天里,我研究并尝试了很多方法,但都没有运气。 Here are a few that I looked at:以下是我看过的一些:

Is it possible to call an awaitable method in a non async method? 是否可以在非异步方法中调用可等待方法?

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

Calling async method in IEnumerable.Select 在 IEnumerable.Select 中调用异步方法

How to await a list of tasks asynchronously using LINQ? 如何使用 LINQ 异步等待任务列表?

how to user async/await inside a lambda 如何在 lambda 中使用 async/await

How to use async within a lambda which returns a collection 如何在返回集合的 lambda 中使用异步

Admittedly, I do not fully understand how async / await work, so that's probably part of the issue.诚然,我并不完全理解 async/await 是如何工作的,所以这可能是问题的一部分。 My foreach code works, but I'd like to be able to understand how to make it work the way I'm trying to.我的 foreach 代码有效,但我希望能够了解如何使其按照我尝试的方式工作。 Since I've spent so much time on it already I figured this would be a good first question.由于我已经花了很多时间,我认为这将是一个很好的第一个问题。

Thanks!谢谢!

Edit编辑

I guess I have to explain what I did in each case of the articles I researched in order for this to not be flagged as a duplicate question - and I tried really hard to avoid that :-/.我想我必须解释我在我研究的文章的每个案例中所做的事情,以免将其标记为重复的问题-我非常努力地避免这种情况:-/。 While the question sounds similar, the results are not.虽然这个问题听起来很相似,但结果却并非如此。 In the case of the article that was marked as an answer I tried the following code:对于标记为答案的文章,我尝试了以下代码:

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

I also tried using AsEnumerable like so:我也尝试使用 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();

Both of these produce the error message: "InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe."这两个都会产生错误消息: “InvalidOperationException:在上一个操作完成之前在此上下文上启动了第二个操作。不保证任何实例成员都是线程安全的。”

At this point it seems like my original ForEach may be the best bet, but I'm still left wondering what would be the right way to do this if I were to do it using the async methods.在这一点上,似乎我最初的 ForEach 可能是最好的选择,但我仍然想知道如果我使用异步方法来做这件事,什么是正确的方法。

Edit 2 - with Answer Thanks to Tseng's comments (and some other research) I was able to make things work using the following code:编辑 2 - 有答案感谢 Tseng 的评论(以及其他一些研究),我能够使用以下代码使事情正常进行:

        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());

Although now that I have taken everyone's comments into consideration, I started looking closer at the SQL Profiler just to see how many hits the DB is actually getting - as Matt Johnson mentioned, it's a lot (N+1).虽然现在我已经考虑了每个人的评论,但我开始仔细研究 SQL Profiler,只是为了了解数据库实际获得了多少点击 - 正如 Matt Johnson 提到的,很多(N+1)。

So while this does answer my question, I am now reconsidering how to run the query and may just drop the roles in the main view and only pull them as each user is selected.因此,虽然这确实回答了我的问题,但我现在正在重新考虑如何运行查询,并且可能只是在主视图中删除角色,并且仅在选择每个用户时才提取它们。 I definitely learned a lot through this question though (and learned more of what I don't know), so thank you everyone.我确实通过这个问题学到了很多(并且学到了更多我不知道的东西),所以谢谢大家。

I think you are mixing two things here.我认为你在这里混合了两件事。 Expression trees and delegates.表达式树和委托。 Lambda can be used to express both of them, but it depends on the type of parameter the method accepts in which one it will be turned. Lambda 可以用来表达它们,但这取决于方法接受的参数类型,它将被转换为哪个参数。

A lambda passed to a method which as Action<T> or Func<T, TResult> will be converted into a delegate (basically an anonymous function/method).传递给作为Action<T>Func<T, TResult>的方法的 lambda 将被转换为委托(基本上是匿名函数/方法)。

When you pass lambda expression to a method accepting Expression<T> , you create an expression tree from the lambda.当您将 lambda 表达式传递给接受Expression<T>的方法时,您将从 lambda 创建一个表达式树。 Expression trees are just code which describe code, but are not code themselves.表达式树只是描述代码的代码,而不是代码本身。

That being said, an expression tree can't be executed because it's converted to executable code.话虽如此,表达式树无法执行,因为它已转换为可执行代码。 You can compile a expression tree at runtime and then execute it like a delegate.您可以在运行时编译表达式树,然后像委托一样执行它。

ORM Frameworks use expression trees to allow you writing "code" which can be translated into something different (a database query for example) or to dynamically generate code at runtime. ORM 框架使用表达式树来允许您编写“代码”,这些“代码”可以转换为不同的内容(例如数据库查询)或在运行时动态生成代码。

For that reason you can't use async in methods that accept Expression<T> .因此,您不能在接受Expression<T>方法中使用async The reason why it may work when you convert it to AsEnumerable() is because it returns an IEnumerable<T> and the LINQ methods on it accept Func<T, TResult> .当您将其转换为AsEnumerable()时它可能起作用的原因是因为它返回一个IEnumerable<T>并且其上的 LINQ 方法接受Func<T, TResult> But it essentially fetches the whole query and does the whole stuff in memory, so you can't use projections (or you have to fetch the data before using just expressions and projections), turn the filtered result into list and then filter it.但它本质上是获取整个查询并在内存中执行所有操作,因此您不能使用投影(或者您必须在仅使用表达式和投影之前获取数据),将过滤后的结果转换为列表,然后对其进行过滤。

You could try something like this:你可以尝试这样的事情:

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

The first part will be done fully on the database and you keep all your advantages (ie the order happens on database, so you don't have to do in-memory sorting after fetching the results and the limit calls limits the data fetched from the DB etc.).第一部分将完全在数据库上完成,您将保留所有优势(即订单发生在数据库上,因此您不必在获取结果后进行内存排序,并且限制调用限制了从数据库中获取的数据)数据库等)。

The second part iterates through the result in memory and fetches the data for each temporary model and finally maps it into the view model.第二部分遍历内存中的结果并获取每个临时模型的数据,最后将其映射到视图模型中。

here is a solution which you can have List in return.这是一个解决方案,您可以使用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);

Update 1:更新 1:

Thanks to @TheodorZoulias, I realized that .Select(q => q.Result) causes thread block.感谢@TheodorZoulias,我意识到.Select(q => q.Result)会导致线程阻塞。 So I think it would be better to use this solution until sb finds some better way.所以我认为最好使用这个解决方案,直到 sb 找到更好的方法。 It may also shuffle the items.它也可以洗牌项目。

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

Extension method:扩展方法:

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

Usage:用法:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM