After reading a book on LINQ I’m thinking about re-writing a mapper class that I wrote in c# to use LINQ. I’m wondering if anyone can give me a hand. Note: its a bit confusing, but the User object is the local user and user (lowercase) is the object generated from the Facebook XSD.
Original Mapper
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; }
Attempt 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); }
Okay, it’s not clear exactly what
IMapperhas in it, but I’d suggest a few things, some of which may not be feasible due to other restrictions. I’ve written this out pretty much as I’ve thought about it – I think it helps to see the train of thought in action, as that’ll make it easier for you to do the same thing next time. (Assuming you like my solutions, of course 🙂LINQ is inherently functional in style. That means that ideally, queries shouldn’t have side-effects. For instance, I’d expect a method with a signature of:
to return a new sequence of user objects with extra information, rather than mutating the existing users. The only information you’re currently appending is the avatar, so I’d add a method in
Useralong the lines of:You might even want to make
Userfully immutable – there are various strategies around that, such as the builder pattern. Ask me if you want more details. Anyway, the main thing is that we’ve created a new user which is a copy of the old one, but with the specified avatar.First attempt: inner join
Now back to your mapper… you’ve currently got three public methods but my guess is that only the first one needs to be public, and that the rest of the API doesn’t actually need to expose the Facebook users. It looks like your
GetFacebookUsersmethod is basically okay, although I’d probably line up the query in terms of whitespace.So, given a sequence of local users and a collection of Facebook users, we’re left doing the actual mapping bit. A straight ‘join’ clause is problematic, because it won’t yield the local users which don’t have a matching Facebook user. Instead, we need some way of treating a non-Facebook user as if they were a Facebook user without an avatar. Essentially this is the null object pattern.
We can do that by coming up with a Facebook user who has a null uid (assuming the object model allows that):
However, we actually want a sequence of these users, because that’s what
Enumerable.Concatuses:Now we can simply ‘add’ this dummy entry to our real one, and do a normal inner join. Note that this assumes that the lookup of Facebook users will always find a user for any ‘real’ Facebook UID. If that’s not the case, we’d need to revisit this and not use an inner join.
We include the ‘null’ user at the end, then do the join and project using
WithAvatar:So the full class would be:
A few points here:
A second approach: group join
Let’s see if we can address these points. I’ll assume that if we’ve fetched multiple Facebook users for a single Facebook UID, then it doesn’t matter which of them we grab the avatar from – they should be the same.
What we need is a group join, so that for each local user we get a sequence of matching Facebook users. We’ll then use
DefaultIfEmptyto make life easier.We can keep
WithAvataras it was before – but this time we’re only going to call it if we’ve got a Facebook user to grab the avatar from. A group join in C# query expressions is represented byjoin ... into. This query is reasonably long, but it’s not too scary, honest!Here’s the query expression again, but with comments:
The non-LINQ solution
Another option is to only use LINQ very slightly. For example:
This uses an iterator block instead of a LINQ query expression.
ToDictionarywill throw an exception if it receives the same key twice – one option to work around this is to changeGetFacebookUsersto make sure it only looks for distinct IDs:That assumes the web service works appropriately, of course – but if it doesn’t, you probably want to throw an exception anyway 🙂
Conclusion
Take your pick out of the three. The group join is probably hardest to understand, but behaves best. The iterator block solution is possibly the simplest, and should behave okay with the
GetFacebookUsersmodification.Making
Userimmutable would almost certainly be a positive step though.One nice by-product of all of these solutions is that the users come out in the same order they went in. That may well not be important to you, but it can be a nice property.
Hope this helps – it’s been an interesting question 🙂
EDIT: Is mutation the way to go?
Having seen in your comments that the local User type is actually an entity type from the entity framework, it may not be appropriate to take this course of action. Making it immutable is pretty much out of the question, and I suspect that most uses of the type will expect mutation.
If that’s the case, it may be worth changing your interface to make that clearer. Instead of returning an
IEnumerable<User>(which implies – to some extent – projection) you might want to change both the signature and the name, leaving you with something like this:Again, this isn’t a particularly ‘LINQ-y’ solution (in the main operation) any more – but that’s reasonable, as you’re not really ‘querying’; you’re ‘updating’.