2016-09-15 40 views
5

我正在使用Asp.Net核心標識,並試圖簡化一些代碼,將用戶列表和他們的角色投影到ViewModel。這段代碼很有用,但爲了簡化它,我陷入了一個瘋狂的錯誤和好奇之中。內部使用異步/等待。選擇lambda

這是我工作的代碼:

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

這因爲我在用戶之前沒有在lambda表達式中使用async關鍵字。然而,當我這樣做之前用戶增加異步,我得到另一個錯誤,即「異步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 - 使用答案 由於曾國藩的意見(和其他一些研究),我是能夠使事情的工作使用下面的代碼:

 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,看看數據庫實際上有多少命中 - 就像Matt Johnson所說的那樣,它很多(N + 1)。

所以,儘管這並回答我的問題,我現在重新考慮如何運行查詢,可能只是下降的角色,在主視圖中,只爲每個用戶選擇拉他們。儘管我通過這個問題學到了很多東西(並且學到了更多我不知道的東西),所以謝謝大家。

+0

'我的猜測是,GetRolesAsync()方法返回一個任務,並將其分配給角色而不是任務的實際結果。' - 可能不會,因爲'await'會抓住任務的結果。 – mason

+2

嘗試在「選擇」之前放置一個'AsEnumerable',以便它將在LInq中運行到對象,而不是嘗試將它轉換爲EF或您使用的任何提供程序的表達式樹。 – juharr

+0

VAR usersViewModels =(AWAIT Task.WhenAll(allUsers.AsEnumerable(),選擇(異步用戶=>新UsersViewModel { 編號= user.Id, 用戶名= user.UserName, 姓= user.FirstName, 名字= user.LastName, DisplayName的= user.DisplayName, 電子郵件= user.Email, 啓用= user.Enabled, 角色=的string.join( 「」,等待_userManager.GetRolesAsync(用戶)) })))。 ToList(); –

回答

7

我認爲你在這裏混合兩件事。表達樹和代表。 Lambda可以用來表達它們,但它取決於方法接受的參數的類型,它將在哪個參數中被轉換。

將lambda傳遞給方法,其中Action<T>Func<T, TResult>將被轉換爲委託(基本上是匿名函數/方法)。

當傳遞lambda表達式接受Expression<T>的方法,創建從拉姆達表達式樹。表達式樹只是描述代碼的代碼,但它們本身不是代碼。

這就是說,表達式樹無法執行,因爲它被轉換爲可執行代碼。您可以在運行時編譯表達式樹,然後像委託一樣執行它。

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中獲取的數據等)。

通過結果在存儲器中的第二部分進行迭代,並且提取用於每個臨時模型的數據和最後將其映射到視圖的模型。