【发布时间】: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 次数据库调用吗?
标签: c# asynchronous lambda asp.net-core asp.net-identity