好的,不清楚
IMapper
里面具体有什么内容,但是我建议一些事情,其中一些可能由于其他限制而无法实现。我已经按照自己的想法写出了这些内容 - 我认为看到思路的过程很有帮助,因为这会让你下次做同样的事情更容易。(当然,假设您喜欢我的解决方案 :) )
LINQ在本质上采用函数式风格。这意味着理想情况下,查询不应该有副作用。例如,我期望一个签名为:
public IEnumerable<User> MapFrom(IEnumerable<User> users)
为了返回一个包含额外信息的新用户对象序列,而不是改变现有用户。你目前只添加了头像信息,所以我建议在
User
类中添加以下方法:
public User WithAvatar(Image avatar)
{
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();
}
}
以下几点需要注意:
- 如前所述,如果无法获取用户的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);
}
以下是带有注释的查询表达式:
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);
非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();
}
假设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();
}
}
再次强调,这不是特别“LINQ风格”的解决方案(在主要操作中)——但这很合理,因为你并没有真正的“查询”;你正在“更新”。