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. 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. However, when I do add async before user, I get yet another error that says "Async lambda expressions cannot be converted to expression trees"
My guess is that the GetRolesAsync()
method is returning a Task and assigning it to Roles instead of the actual results of that task. 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/
Calling async method in IEnumerable.Select
How to await a list of tasks asynchronously using LINQ?
how to use async/await inside a lambda
How to use async within a lambda which returns a collection
Admittedly, I do not fully understand how async / await work, so that's probably part of the issue. My foreach code works, but I'd like to be able to understand how to make it work the way I'm trying to. 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:
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."
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.
Edit 2 - with Answer Thanks to Tseng's comments (and some other research) I was able to make things work using the following code:
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).
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.
My guess is that the GetRolesAsync() method is returning a Task and assigning it to Roles instead of the actual results of that task.
- probably not, becauseawait
will grab the result of the task. – CannibalAsEnumerable
before theSelect
so it will be run in LInq to Objects instead of trying to convert it to an expression tree for EF or whatever provider you use. – VanhookGetRolesAsync
apparently also runs something on the database context, you cannot have multiple parallelGetRolesAsync
calls. You would need to run them in sequence. As per Matt Johnson’s comment, you should consider a way to query multiple (all) user roles at once if you really need them. – btw. your original question completely left out the relevant parts of what you have tried with those linked questions, and now that you have edited that in, it’s not clear to me, why you didn’t search for the error message. – Anabal