2009-03-02 61 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

相关问题