Использование Linq для сопоставления профиля Facebook с моей информацией о пользователе

StackOverflow https://stackoverflow.com/questions/603650

  •  03-07-2019
  •  | 
  •  

Вопрос

Прочитав книгу о LINQ, я подумываю о том, чтобы переписать класс картографа, который я написал на C#, для использования LINQ.Мне интересно, может ли кто-нибудь мне помочь.Примечание:это немного сбивает с толку, но объект User — это локальный пользователь, а пользователь (строчные буквы) — это объект, сгенерированный из XSD Facebook.

Оригинальный картограф

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

Мои первые две попытки уперлись в стену

Попытка 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);
}
Это было полезно?

Решение

Ладно, не совсем понятно, что именно 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 без аватара.По сути, это шаблон нулевого объекта.

Мы можем сделать это, найдя пользователя Facebook с нулевым идентификатором uid (при условии, что объектная модель позволяет это):

// 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 Однако immutable почти наверняка будет положительным шагом.

Приятным побочным продуктом всех этих решений является то, что пользователи выходят из системы в том же порядке, в котором они вошли.Возможно, для вас это не важно, но это может быть хорошая собственность.

Надеюсь, это поможет - это был интересный вопрос :)

РЕДАКТИРОВАТЬ:Является ли мутация выходом из ситуации?

Увидев в ваших комментариях, что локальный тип пользователя на самом деле является типом сущности из структуры сущностей, он может нецелесообразно предпринимать такие действия.О том, чтобы сделать его неизменяемым, практически не может быть и речи, и я подозреваю, что большинство случаев использования этого типа будут ожидать мутация.

В этом случае, возможно, стоит изменить интерфейс, чтобы сделать его более понятным.Вместо возврата 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» решение (в основной операции), но это разумно, поскольку на самом деле вы не «запрашиваете»;вы «обновляетесь».

Другие советы

Вместо этого я был бы склонен написать что-то вроде этого:

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 цикл для выполнения фактического действия по изменению элемента набора, как вы это делали при попытках рефакторинга.Вы можете это сделать, но люди будут удивлены вашим кодом, и вам придется все время помнить о ленивых вычислениях.)

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top