好吧,现在还不清楚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“解决方案(在主操作中) - 但这是合理的,因为你并不真正”查询“;你正在“更新”。
乔恩,这真棒+1,我爱你的工作。我想我理解这个分组,我可能会选择这个选项。如果我使用分组方法,是否必须调用.Concat(NullFacebookUsers)?另外,我的本地用户是一个EF对象,你知道一个好的克隆方法吗? – bendewey 2009-04-01 22:20:36