使用Linq将Facebook个人资料与我的用户信息进行映射

3

在阅读了一本关于 LINQ 的书籍之后,我考虑重新编写我用 C# 编写的映射器类,以使用 LINQ。我想知道是否有人可以帮我一把。注意:有点混淆,但 User 对象是本地用户,而 user(小写)是从 Facebook XSD 生成的对象。

原始映射器

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;
    }
}

My first two attempts hit walls

Attempt 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);
}
2个回答

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 用户集合,我们需要做的就是实际映射部分。直接使用“join”子句有问题,因为它不会产生没有匹配 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找到一个用户。如果不是这种情况,我们需要重新审视此问题并不使用内部连接。
我们在最后包含“null”用户,然后使用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成为不可变类型几乎肯定是一个积极的步骤。

所有这些解决方案的一个好处是用户的输出顺序与输入顺序相同。这对您可能并不重要,但可以是一个好的属性。

希望这有所帮助 - 这是一个有趣的问题:)

编辑:改变是否是正确的方法?

根据评论中提到的本地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风格”的解决方案(在主要操作中)——但这很合理,因为你并没有真正的“查询”;你正在“更新”。


Jon,这太棒了+1,我喜欢你的工作。我想我理解了分组,我可能会选择那个选项。如果我使用分组方法,我是否必须调用.Concat(NullFacebookUsers)?此外,我的本地用户是一个EF对象,你知道一个好的克隆方法吗? - bendewey
不,分组位处理“非Facebook用户”,因为它不匹配其中任何一个,这就是为什么我们使用DefaultIfEmpty().First()的原因。对不起,我不知道如何克隆EF实体 :( - Jon Skeet
@bendewey:考虑到 EF 的特性,我在我的答案中添加了一个额外的部分(在底部)。 - Jon Skeet
糟糕 - 我刚刚明白你为什么问关于分组操作的 .Concat(...) 部分了。不好意思,我无意中包含了那个!现在已经修复了... - Jon Skeet

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用户集合非常大,否则您可能会遇到明显的性能差异)。
我建议不要像在重构尝试中那样使用Select来执行集合元素的实际变异操作,就像使用foreach循环一样。您可以这样做,但人们会对您的代码感到惊讶,并且您必须一直牢记延迟评估。

+1 谢谢你的回答,很有帮助。不过,Jon在LINQ中的分组技术更符合我所寻找的。 - bendewey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接