2009-03-02 40 views
3

閱讀LINQ的書後,我正考慮重寫一個我在c#中編寫的使用LINQ的映射器類。我想知道有沒有人可以幫我一把。注意:它有點混亂,但User對象是本地用戶,而用戶(小寫)是從Facebook XSD生成的對象。使用Linq將facebook個人資料與我的用戶信息進行映射

原始映射器

public class FacebookMapper : IMapper 
{ 
    public IEnumerable<User> MapFrom(IEnumerable<User> users) 
    { 
     var facebookUsers = GetFacebookUsers(users); 
     return MergeUsers(users, facebookUsers); 
    } 

    public Facebook.user[] GetFacebookUsers(IEnumerable<User> users) 
    { 
     var uids = (from u in users 
     where u.FacebookUid != null 
     select u.FacebookUid.Value).ToList(); 

     // return facebook users for uids using WCF 
    } 

    public IEnumerable<User> MergeUsers(IEnumerable<User> users, Facebook.user[] facebookUsers) 
    { 
     foreach(var u in users) 
     { 
     var fbUser = facebookUsers.FirstOrDefault(f => f.uid == u.FacebookUid); 
     if (fbUser != null) 
      u.FacebookAvatar = fbUser.pic_sqare; 
     } 
     return users; 
    } 
} 

我的第二企圖撞壁

嘗試1

public IEnumerable<User> MapFrom(IEnumerable<User> users) 
{ 
    // didn't have a way to check if u.FacebookUid == null 
    return from u in users 
    join f in GetFacebookUsers(users) on u.FacebookUid equals f.uid 
    select AppendAvatar(u, f); 
} 

public void AppendAvatar(User u, Facebook.user f) 
{ 
    if (f == null) 
    return u; 
    u.FacebookAvatar = f.pic_square; 
    return u; 
} 

嘗試2

public IEnumerable<User> MapFrom(IEnumerable<User> users) 
{ 
    // had to get the user from the facebook service for each single user, 
    // would rather use a single http request. 
    return from u in users 
    let f = GetFacebookUser(user.FacebookUid) 
    select AppendAvatar(u, f); 
} 

回答

9

好吧,現在還不清楚IMapper究竟有什麼,但我會建議一些事情,其中​​一些可能由於其他限制而不可行。我已經寫了很多,因爲我已經考慮過了 - 我認爲這有助於看到行動中的思路,因爲這會讓您下次更容易做同樣的事情。 (假設你喜歡我的解決方案,當然:)

LINQ本質上是風格的功能。這意味着理想情況下,查詢不應該有副作用。舉例來說,我期望的方法用的簽名:

public IEnumerable<User> MapFrom(IEnumerable<User> users) 

返回用戶對象的一個​​新的序列有額外的信息,而不是突變的現有用戶。您目前追加的唯一信息是動漫形象,所以我會在User沿線的添加方法:

public User WithAvatar(Image avatar) 
{ 
    // Whatever you need to create a clone of this user 
    User clone = new User(this.Name, this.Age, etc); 
    clone.FacebookAvatar = avatar; 
    return clone; 
} 

你甚至可能想使User完全不可改變的 - 有周圍的各種策略,如生成器模式。詢問我是否需要更多詳細信息。無論如何,最主要的是我們已經創建了一個新用戶,它是舊的用戶的副本,但具有指定的頭像。

第一次嘗試:內部連接

現在回到你映射......你現在有三個公共方法,但我猜只有第一個必須是公開,其餘的API實際上並不需要公開Facebook用戶。它看起來像你的GetFacebookUsers方法基本上沒問題,雖然我可能根據空白排列查詢。

因此,給定一系列本地用戶和一系列Facebook用戶,我們只剩下實際的映射位。直接的「加入」條款是有問題的,因爲它不會產生沒有匹配Facebook用戶的本地用戶。相反,我們需要一種將非Facebook用戶視爲沒有虛擬形象的Facebook用戶的方式。基本上這是空對象模式。

我們可以做到這一點想出誰擁有空UID Facebook的用戶(假設對象模型允許):

// Adjust for however the user should actually be constructed. 
private static readonly FacebookUser NullFacebookUser = new FacebookUser(null); 

然而,我們實際上需要一個序列這些用戶的,因爲這就是Enumerable.Concat用途:

private static readonly IEnumerable<FacebookUser> NullFacebookUsers = 
    Enumerable.Repeat(new FacebookUser(null), 1); 

現在,我們可以簡單地「增加」這個虛擬記錄到我們真正的一個,做一個正常的內連接。請注意,這個假設,Facebook用戶的查找將總是找到任何「真正的」Facebook UID的用戶。如果情況並非如此,我們需要重新考慮這一點,而不是使用內部連接。

我們在末尾加「空」的用戶,然後再做連接和使用WithAvatar項目:

public IEnumerable<User> MapFrom(IEnumerable<User> users) 
{ 
    var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers); 
    return from user in users 
      join facebookUser in facebookUsers on 
       user.FacebookUid equals facebookUser.uid 
      select user.WithAvatar(facebookUser.Avatar); 
} 

所以滿級是:

public sealed class FacebookMapper : IMapper 
{ 
    private static readonly IEnumerable<FacebookUser> NullFacebookUsers = 
     Enumerable.Repeat(new FacebookUser(null), 1); 

    public IEnumerable<User> MapFrom(IEnumerable<User> users) 
    { 
     var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers); 
     return from user in users 
       join facebookUser in facebookUsers on 
        user.FacebookUid equals facebookUser.uid 
       select user.WithAvatar(facebookUser.pic_square); 
    } 

    private Facebook.user[] GetFacebookUsers(IEnumerable<User> users) 
    { 
     var uids = (from u in users 
        where u.FacebookUid != null 
        select u.FacebookUid.Value).ToList(); 

     // return facebook users for uids using WCF 
    } 
} 

幾個點這裏:

  • 如前所述,如果用戶的Facebook UID可能不會被提取爲有效用戶。
  • 同樣,如果我們有重複的Facebook用戶,每個本地用戶最終會出現兩次!
  • 這將替換(刪除)非Facebook用戶的頭像。

第二種方法:組加入

讓我們看看,如果我們可以解決這些問題。我假設,如果我們已經爲單個Facebook UID獲取了多個 Facebook用戶,那麼我們從中獲取哪個頭像並不重要 - 它們應該是相同的。

我們需要的是一個羣組連接,因此對於每個本地用戶,我們都會得到一系列匹配的Facebook用戶。然後我們將使用DefaultIfEmpty使生活更輕鬆。

我們可以像以前一樣保留WithAvatar--但是這一次我們只會打電話給我們,如果我們有Facebook用戶從中獲取頭像。在C#查詢表達式中的組加入由join ... into表示。這個查詢是相當長的,但它不是太可怕,誠實!

public IEnumerable<User> MapFrom(IEnumerable<User> users) 
{ 
    var facebookUsers = GetFacebookUsers(users); 
    return from user in users 
      join facebookUser in facebookUsers on 
       user.FacebookUid equals facebookUser.uid 
       into matchingUsers 
      let firstMatch = matchingUsers.DefaultIfEmpty().First() 
      select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square); 
} 

這裏的查詢表達式一遍,但評論:

// "Source" sequence is just our local users 
from user in users 
// Perform a group join - the "matchingUsers" range variable will 
// now be a sequence of FacebookUsers with the right UID. This could be empty. 
join facebookUser in facebookUsers on 
    user.FacebookUid equals facebookUser.uid 
    into matchingUsers 
// Convert an empty sequence into a single null entry, and then take the first 
// element - i.e. the first matching FacebookUser or null 
let firstMatch = matchingUsers.DefaultIfEmpty().First() 
// If we've not got a match, return the original user. 
// Otherwise return a new copy with the appropriate avatar 
select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square); 

非LINQ的解決方案

另一種選擇是隻使用LINQ非常輕微。例如:

public IEnumerable<User> MapFrom(IEnumerable<User> users) 
{ 
    var facebookUsers = GetFacebookUsers(users); 
    var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid); 

    foreach (var user in users) 
    { 
     FacebookUser fb; 
     if (uidDictionary.TryGetValue(user.FacebookUid, out fb) 
     { 
      yield return user.WithAvatar(fb.pic_square); 
     } 
     else 
     { 
      yield return user; 
     } 
    } 
} 

這使用迭代器塊而不是LINQ查詢表達式。如果收到兩次相同的密鑰ToDictionary將拋出一個異常 - 一個選項來解決,這是改變GetFacebookUsers,以確保它只會尋找不同的ID:

private Facebook.user[] GetFacebookUsers(IEnumerable<User> users) 
    { 
     var uids = (from u in users 
        where u.FacebookUid != null 
        select u.FacebookUid.Value).Distinct().ToList(); 

     // return facebook users for uids using WCF 
    } 

這假定Web服務的工作適當,當然 - 但如果沒有,你可能想反正拋出一個異常:)

結論

任你選出來的三個。團隊加入可能最難理解,但表現最好。迭代器塊解決方案可能是最簡單的,並且應該在GetFacebookUsers修改中表現良好。

製作User不可變將幾乎肯定會是一個積極的一步,雖然。

所有這些解決方案的一個很好的副產品是用戶以與他們相同的順序出來。這對您來說可能並不重要,但它可能是一個很好的屬性。

希望這有助於 - 這是一個有趣的問題:)

編輯:是否突變的路要走?

在您的評論中看到,本地用戶類型實際上是實體框架中的實體類型,它可能不適合採取此操作。使它不可變是非常不可能的,我懷疑這種類型的大多數用途將會在期望突變。

如果是這種情況,可能需要更改界面以使其更清晰。不是返回IEnumerable<User>( - 在一定程度上 - 這意味着投影),你可能想改變這兩個簽名和名字,讓你有這樣的事情:

public sealed class FacebookMerger : IUserMerger 
{ 
    public void MergeInformation(IEnumerable<User> users) 
    { 
     var facebookUsers = GetFacebookUsers(users); 
     var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid); 

     foreach (var user in users) 
     { 
      FacebookUser fb; 
      if (uidDictionary.TryGetValue(user.FacebookUid, out fb) 
      { 
       user.Avatar = fb.pic_square; 
      } 
     } 
    } 

    private Facebook.user[] GetFacebookUsers(IEnumerable<User> users) 
    { 
     var uids = (from u in users 
        where u.FacebookUid != null 
        select u.FacebookUid.Value).Distinct().ToList(); 

     // return facebook users for uids using WCF 
    } 
} 

再次,這是不是一個特別「 LINQ-y「解決方案(在主操作中) - 但這是合理的,因爲你並不真正」查詢「;你正在「更新」。

+0

喬恩,這真棒+1,我愛你的工作。我想我理解這個分組,我可能會選擇這個選項。如果我使用分組方法,是否必須調用.Concat(NullFacebookUsers)?另外,我的本地用戶是一個EF對象,你知道一個好的克隆方法嗎? – bendewey 2009-04-01 22:20:36

5

我會傾向於寫這樣的事情,而不是:

public class FacebookMapper : IMapper 
{ 
    public IEnumerable<User> MapFacebookAvatars(IEnumerable<User> users) 
    { 
     var usersByID = 
      users.Where(u => u.FacebookUid.HasValue) 
       .ToDictionary(u => u.FacebookUid.Value); 

     var facebookUsersByID = 
      GetFacebookUsers(usersByID.Keys).ToDictionary(f => f.uid); 

     foreach(var id in usersByID.Keys.Intersect(facebookUsersByID.Keys)) 
      usersByID[id].FacebookAvatar = facebookUsersByID[id].pic_sqare; 

     return users; 
    } 

    public Facebook.user[] GetFacebookUsers(IEnumerable<int> uids) 
    { 
     // return facebook users for uids using WCF 
    } 
} 

不過,我不會聲稱是在你有什麼大的起色(除非用戶或Facebook的用戶集合是非常大,在這種情況下,你可能會發現一個明顯的性能差異。)

(我建議不要使用Selectforeach循環來執行對一個元素的實際變異操作,你的方式在你的重構嘗試中。你可以做到這一點,但人們會對你的代碼感到驚訝,並且你必須在整個時間內保持懶惰的評估。)

+0

+1感謝您的回答這很有幫助,Jon在LINQ中的分組技術更符合我期待的內容。 – bendewey 2009-04-02 14:56:24

相關問題