在. select lambda中使用async / await



我使用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关键字。然而,当我在用户之前添加async时,我又得到了另一个错误,说"async lambda表达式不能转换为表达式树"

我的猜测是GetRolesAsync()方法正在返回一个Task并将其分配给Roles,而不是该任务的实际结果。我似乎一辈子也弄不明白的是如何使它起作用。

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

是否可以在非异步方法中调用可等待方法?

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

调用IEnumerable中的async方法。选择

如何使用LINQ异步等待任务列表?

如何在lambda中使用async/await

如何在lambda中使用async返回一个集合

无可否认,我不完全理解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可能是最好的选择,但我仍然想知道如果我使用异步方法来做这件事,正确的方法是什么。

Edit 2 - with Answer感谢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());

虽然现在我已经考虑了每个人的评论,但我开始更仔细地查看SQL Profiler,只是为了看看DB实际上得到了多少点击率——正如Matt Johnson提到的,它很多(N+1)。

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

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

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

当你将lambda表达式传递给一个接受Expression<T>的方法时,你从lambda创建了一个表达式树。表达式树只是描述代码的代码,而不是代码本身。

也就是说,表达式树不能被执行,因为它被转换为可执行代码。可以在运行时编译表达式树,然后像执行委托一样执行它。

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

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

你可以试试这样做:

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

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

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

这里有一个解决方案,您可以获得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 => 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));

扩展方法:

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

相关内容

  • 没有找到相关文章

最新更新