Frage

Nachdem ein Buch über LINQ lesen Ich denke über Umschreiben eine Mapper-Klasse, die ich in c # geschrieben LINQ zu verwenden. Ich frage mich, wenn jemand mir eine Hand geben kann. . Hinweis: es ist ein wenig verwirrend, aber das User-Objekt ist die lokalen Benutzer und Benutzer (klein geschrieben) ist das Objekt aus dem Facebook XSD erzeugt

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

Meine ersten beiden Versuche schlagen Wände

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

Versuch 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);
}
War es hilfreich?

Lösung

Okay, es ist nicht klar, genau das, was IMapper in ihm hat, aber ich würde ein paar Dinge vorschlagen, von denen einige aufgrund anderer Einschränkungen nicht durchführbar sein. Ich habe das ziemlich viel geschrieben, wie ich darüber nachgedacht habe - ich glaube, es hilft, den Gedankengang in Aktion zu sehen, wie es einfacher zu machen, wird für Sie die gleiche Sache beim nächsten Mal zu tun. (Vorausgesetzt, dass Sie wie meine Lösungen, natürlich:)

LINQ ist von Natur aus funktionalem Stil. Das bedeutet, dass im Idealfall Abfrage sollte keine Nebenwirkungen hat. Zum Beispiel würde ich eine Methode mit einer Signatur erwarte von:

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

eine neue Folge von Benutzerobjekten mit zusätzlichen Informationen zurückzukehren, anstatt die bestehenden Benutzer mutiert. Die einzige Information, Sie gerade anhängen ist der Avatar, also würde ich eine Methode in User entlang der Linien von:

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

Sie möchten vielleicht sogar User völlig unveränderlich machen - es gibt verschiedene Strategien, um die, wie die Erbauer. Fragen Sie mich, wenn Sie weitere Informationen wünschen. Wie auch immer, die Hauptsache ist, dass wir einen neuen Benutzer erstellt haben, die eine Kopie der alten, aber mit dem angegebenen Avatar.

Erster Versuch: INNER JOIN

Nun zurück zu Ihrem Mapper ... Sie haben zur Zeit drei öffentliche Methoden bekommen aber mein erraten ist, dass nur die erste öffentlich sein muss, und dass die Rest der API muss nicht tatsächlich die Facebook-Nutzer verfügbar machen. Es sieht aus wie Ihre GetFacebookUsers Methode grundsätzlich in Ordnung, obwohl ich wahrscheinlich die Abfrage in Bezug auf die Leerzeichen in einer Reihe aufstellen würde.

So, da eine Folge von lokalen Benutzern und eine Sammlung von Facebook-Nutzer, sind wir die tatsächliche Zuordnung Bit tun gelassen. Eine gerade „Join“ Klausel ist problematisch, weil es nicht die lokalen Benutzer ergeben, die keine passenden Facebook-Nutzer haben. Stattdessen brauchen wir eine Möglichkeit, einen Nicht-Facebook-Nutzer zu behandeln, als ob sie ein Facebook-Nutzer ohne einen Avatar waren. Im Wesentlichen ist dies das Null-Objekt-Muster.

Wir können tun, dass mit einem Facebook-Nutzer, die eine Null-uid hat durch kommen (das Objektmodell unter der Annahme erlaubt, dass):

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

Aber wir eigentlich wollen ein sequence diese Nutzer, denn das ist, was Enumerable.Concat verwendet:

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

Jetzt können wir einfach „add“ diese Dummy-Eintrag in unseren realen und tun eine normale innere Verknüpfung. Beachten Sie, dass diese übernimmt , dass die Suche von Facebook-Nutzer werden immer einen Benutzer für jede „echte“ Facebook UID finden. Wenn das nicht der Fall ist, würden wir müssen diese zu überdenken und nicht verwendet eine innere Verknüpfung.

Wir sind die „Null“ Benutzer am Ende, hat dann die Verbindung und Projekt mit 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);
}

So ist die volle Klasse wäre:

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

Ein paar Punkte hier:

  • Wie bereits erwähnt, ist die innere Verknüpfung problematisch wird, wenn ein Facebook-UID des Benutzers möglicherweise nicht als gültiger Benutzer abgerufen werden.
  • Ebenso bekommen wir Probleme, wenn wir doppelte Facebook-Nutzer haben - jeder lokale Benutzer würde zweimal am Ende herauskommt
  • !
  • Dies ersetzt (entfernt), um den Avatar für Nicht-Facebook-Nutzer.

Ein zweiter Ansatz: Gruppe beitreten

Lassen Sie uns sehen, ob wir diese Punkte ansprechen können. Ich werde davon ausgehen, dass, wenn wir haben geholt mehrere Facebook-Nutzer für eine einzelne Facebook UID, dann spielt es keine Rolle, welche von ihnen wir den Avatar packen aus -. Sie sollten gleich sein

Was wir brauchen, ist eine Gruppe beitreten, so dass für jeden lokalen Benutzer wir eine Sequenz von passenden Facebook-Nutzer erhalten. Wir werden dann DefaultIfEmpty verwenden, um das Leben leichter zu machen.

Wir können WithAvatar halten, wie es vorher war - aber diesmal wir es nur geht zu nennen, wenn wir eine Facebook-Nutzer haben von den Avatar zu greifen. Eine Gruppe beitreten in C # Abfrage-Ausdrücken wird durch join ... into dargestellt. Diese Abfrage ist ziemlich lang, aber es ist nicht zu gruselig, ehrlich!

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

Hier ist der Abfrage-Ausdruck wieder, aber mit Kommentaren:

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

Die nicht-LINQ-Lösung

Eine weitere Option ist nur LINQ zu verwenden, sehr leicht. Zum Beispiel:

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

Dieses verwendet einen Iteratorblock anstelle einer LINQ-Abfrage-Ausdruck. ToDictionary wird eine Ausnahme werfen, wenn sie den gleichen Schlüssel erhält das Doppelte - eine Option, dies zu umgehen ist GetFacebookUsers zu ändern, um sicherzustellen, es sieht nur für eindeutige IDs:

    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
    }

Das übernimmt der Web-Service angemessen, natürlich funktioniert - aber wenn es nicht der Fall ist, möchten Sie wahrscheinlich eine Ausnahme werfen trotzdem:)

Fazit

Nehmen Sie Ihre Auswahl aus den dreien. Die Gruppe beizutreten, ist wahrscheinlich am schwersten zu verstehen, sondern verhält sich am besten. Die Iteratorblock Lösung ist vielleicht die einfachste und soll mit der GetFacebookUsers Änderung in Ordnung verhalten.

macht User unveränderlich wäre allerdings mit ziemlicher Sicherheit einen positiven Schritt sein.

Ein schönes Nebenprodukt all dieser Lösungen ist, dass die Benutzer in der gleichen Reihenfolge kommen sie in ging. Das mag für Sie nicht wichtig sein, aber es kann ein schönes Hotel sein.

Hope, das hilft - es war eine interessante Frage:)

EDIT: Ist Mutation der Weg zu gehen

?

in Ihren Kommentaren gesehen zu haben, dass der lokale Benutzertyp ist eigentlich ein Entitätstyp aus dem Entity Framework, es kann nicht geeignet, diese Vorgehensweise zu nehmen. es unveränderlich zu machen ist ziemlich außer Frage, und ich vermute, dass die meisten Anwendungen des Typs erwarten Mutation.

Wenn das der Fall ist, kann es sich lohnen, Ihre Schnittstelle zu ändern, dass klarer zu machen. Anstatt eine IEnumerable<User> Rückkehr (was bedeutet - in gewissem Maße - Projektion) Sie sowohl die Unterschrift und den Namen ändern möchten, können Sie mit so etwas wie dieses verlassen:

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

Auch dies ist keine besonders „LINQ-y“ Lösung (im Hauptbetrieb) mehr - aber das ist sinnvoll, da man nicht wirklich „anfragende“ sind; du bist "Aktualisierung".

Andere Tipps

Ich würde geneigt sein, so etwas zu schreiben statt:

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

Allerdings würde ich nicht behaupten, dass eine große Verbesserung gegenüber war, was du hast (es sei denn, die Benutzer oder Facebook-Benutzer Sammlungen sind sehr groß, in dem Fall, dass Sie mit einem spürbaren Unterschied in der Leistung aufzuwickeln könnten.)

(würde ich empfehlen, gegen Select wie ein foreach Schleife unter Verwendung eines tatsächlichen Mutieren Aktion auf ein Element eines Satzes auszuführen, wie Sie in Ihrem Refactoring versucht haben. Sie können es tun, aber die Menschen werden von Ihrem Code überraschen und Sie werden die ganze Zeit faul Auswertung im Auge behalten.)

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top