Ладно, не совсем понятно, что именно есть в 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 без аватара. По сути, это шаблон нулевого объекта.
Мы можем сделать это, придумав пользователя 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 всегда найдут пользователя для любого «реального» UID Facebook. Если это не так, мы должны вернуться к этому и не использовать внутреннее соединение.
В конце мы добавляем «нулевого» пользователя, затем выполняем объединение и проектирование, используя 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
}
}
Несколько моментов здесь:
- Как отмечалось ранее, внутреннее объединение становится проблематичным, если пользовательский UID Facebook не может быть выбран как действительный пользователь.
- Точно так же у нас возникают проблемы, если у нас есть дубликаты пользователей 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
, чтобы убедиться, что он ищет только отдельные идентификаторы:
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
}
Это предполагает, что веб-сервис работает, конечно, - но если это не так, вы, вероятно, все равно захотите вызвать исключение:)
Заключение
Возьми свой выбор из трех. Групповое объединение, вероятно, труднее понять, но оно ведет себя лучше. Решение блока итераторов, возможно, самое простое, и оно должно вести себя хорошо с модификацией 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" решение (в основной операции) - но это разумно, поскольку вы на самом деле не "запрашиваете"; вы "обновляете".