Utiliser Linq pour mapper le profil facebook avec mes informations d'utilisateur

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

  •  03-07-2019
  •  | 
  •  

Question

Après avoir lu un livre sur LINQ, je pense à réécrire une classe de mappeur que j’ai écrite en c # pour utiliser LINQ. Je me demande si quelqu'un peut me donner un coup de main. Remarque: c'est un peu déroutant, mais l'objet User est l'utilisateur local et l'utilisateur (minuscule) est l'objet généré à partir de la XSD Facebook.

Mappeur d'origine

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

Mes deux premières tentatives ont frappé les murs

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

Tentative 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);
}
Était-ce utile?

La solution

D'accord, on ne sait pas exactement ce que IMapper contient, mais je suggérerais certaines choses, dont certaines pourraient ne pas être réalisables en raison d'autres restrictions. J'ai écrit ce texte à peu près au fur et à mesure que j'y réfléchissais - je pense que cela aide de voir le train de pensées en action, car cela vous facilitera la tâche pour faire la même chose la prochaine fois. (En supposant que vous aimez mes solutions, bien sûr:)

LINQ est intrinsèquement fonctionnel dans le style. Cela signifie que, dans l'idéal, les requêtes ne devraient pas avoir d'effets secondaires. Par exemple, je m'attendrais à une méthode avec une signature de:

public IEnumerable<User> MapFrom(IEnumerable<User> users)

pour renvoyer une nouvelle séquence d'objets utilisateur avec des informations supplémentaires, plutôt que de muter les utilisateurs existants. L’avatar est la seule information que vous ajoutez actuellement. Par conséquent, j’ajouterais une méthode dans Utilisateur dans les termes suivants:

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

Vous pouvez même vouloir rendre User totalement immuable - il existe diverses stratégies autour de cela, telles que le modèle de générateur. Demandez-moi si vous voulez plus de détails. Quoi qu’il en soit, l’essentiel est que nous ayons créé un nouvel utilisateur, qui est une copie de l’ancien, mais avec l’avatar spécifié.

Première tentative: jointure interne

Revenons maintenant à votre mappeur ... vous avez actuellement trois méthodes publiques , mais mon devine est que seul le premier doit être public et que reste de l'API n'a pas réellement besoin d'exposer les utilisateurs de Facebook. Il semble que votre méthode GetFacebookUsers soit globalement correcte, bien que je voudrais probablement aligner la requête en termes d'espaces.

Donc, étant donné une séquence d'utilisateurs locaux et une collection d'utilisateurs de Facebook, nous nous retrouvons à faire le bit de mappage réel. Un lien " rejoindre " La clause est problématique, car elle ne donnera pas les utilisateurs locaux qui n'ont pas d'utilisateur Facebook correspondant. Au lieu de cela, nous avons besoin d’un moyen de traiter un utilisateur non-Facebook comme s’il s’agissait d’un utilisateur Facebook sans avatar. Il s'agit essentiellement du modèle d'objet null.

Nous pouvons le faire en proposant un utilisateur Facebook qui a un uid nul (en supposant que le modèle d'objet le permette):

// Adjust for however the user should actually be constructed.
private static readonly FacebookUser NullFacebookUser = new FacebookUser(null);

Cependant, nous voulons réellement une séquence de ces utilisateurs, car c’est ce que Enumerable.Concat utilise:

private static readonly IEnumerable<FacebookUser> NullFacebookUsers =
    Enumerable.Repeat(new FacebookUser(null), 1);

Maintenant, nous pouvons simplement "ajouter". cette entrée factice à notre vrai, et faire une jointure interne normale. Notez que cela suppose que la recherche des utilisateurs de Facebook trouvera toujours un utilisateur pour tout " réel " Facebook UID. Si ce n’est pas le cas, nous devons revoir cette procédure et ne pas utiliser de jointure interne.

Nous incluons le " null " utilisateur à la fin, puis faites la jointure et projetez en utilisant 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);
}

La classe complète serait donc:

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

Quelques points ici:

  • Comme indiqué précédemment, la jointure interne devient problématique si l'UID Facebook d'un utilisateur peut ne pas être récupéré en tant qu'utilisateur valide.
  • De même, nous avons des problèmes si nous avons des utilisateurs Facebook en double - chaque utilisateur local finirait par sortir deux fois!
  • Ceci remplace (supprime) l'avatar des utilisateurs autres que Facebook.

Une deuxième approche: rejoindre le groupe

Voyons si nous pouvons aborder ces points. Je suppose que si nous avons recherché plusieurs utilisateurs Facebook pour un seul UID Facebook, le type d'utilisateur parmi lequel nous récupérons l'avatar importe peu: ils doivent être identiques.

Ce qu'il nous faut, c'est une jointure de groupe. Ainsi, pour chaque utilisateur local, nous obtenons une séquence d'utilisateurs Facebook correspondants. Nous utiliserons ensuite DefaultIfEmpty pour vous simplifier la vie.

Nous pouvons conserver WithAvatar comme auparavant - mais cette fois, nous ne l'appellerons que si nous avons un utilisateur de Facebook sur lequel extraire l'avatar. Une jointure de groupe dans les expressions de requête C # est représentée par join ... dans . Cette requête est assez longue, mais ce n'est pas trop effrayant, honnête!

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

Voici à nouveau l'expression de requête, mais avec des commentaires:

// "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);

La solution non LINQ

Une autre option consiste à n’utiliser LINQ que très légèrement. Par exemple:

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

Ceci utilise un bloc itérateur au lieu d’une expression de requête LINQ. ToDictionary lève une exception s'il reçoit la même clé deux fois - une option pour y remédier est de changer GetFacebookUsers pour s'assurer qu'il ne recherche que des ID distincts:

    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
    }

Cela suppose que le service Web fonctionne correctement, bien entendu, mais si ce n'est pas le cas, vous souhaiterez probablement émettre une exception de toute façon:)

Conclusion

Faites votre choix parmi les trois. La jointure de groupe est probablement la plus difficile à comprendre, mais elle se comporte le mieux. La solution de bloc d'itérateur est peut-être la plus simple et devrait se comporter correctement avec la modification GetFacebookUsers .

Rendre utilisateur immuable serait certainement certainement une étape positive.

Un des sous-produits de toutes ces solutions est que les utilisateurs s'affichent dans le même ordre. Cela peut ne pas être important pour vous, mais cela peut être une belle propriété.

J'espère que cela aide - c'est une question intéressante:)

EDIT: la mutation est-elle la voie à suivre?

Après avoir constaté dans vos commentaires que le type d'utilisateur local est en réalité un type d'entité du cadre de l'entité, il se peut que ne soit pas approprié pour prendre cette mesure. Le rendre immuable est à peu près hors de question, et je soupçonne que la plupart des utilisations du type vont s'attendre à une mutation.

Si tel est le cas, il peut être intéressant de modifier votre interface pour que cela soit plus clair. Au lieu de renvoyer un IEnumerable < User > (ce qui implique - dans une certaine mesure - une projection), vous souhaiterez peut-être modifier la signature et le nom, ce qui vous laissera quelque chose comme ceci:

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

Encore une fois, ce n’est pas un "LINQ-y" particulier. solution (dans l'opération principale) plus - mais c'est raisonnable, car vous n'êtes pas vraiment "interroger"; vous "mettez à jour".

Autres conseils

Je serais enclin à écrire quelque chose comme ceci à la place:

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

Cependant, je ne dirais pas que cela représente une nette amélioration par rapport à ce que vous avez (sauf si les collections d'utilisateurs ou d'utilisateurs de Facebook sont très volumineuses, auquel cas vous pourriez vous retrouver avec une différence de performances notable.)

(Je vous déconseille d'utiliser . Sélectionnez comme une boucle foreach pour effectuer une action de mutation réelle sur un élément d'un ensemble, comme vous l'avez fait lors de vos tentatives de refactoring. Vous pouvez le faire, mais votre code vous surprendra et vous devrez garder l’évaluation paresseuse à l’esprit tout le temps.)

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top