Entity Framework Entities - Einige Daten aus dem Webdienst - Beste Architektur?

10

Derzeit verwenden wir Entity Framework als ORM für einige Webanwendungen. Bis jetzt hat es uns gut gepasst, da alle unsere Daten in einer einzigen Datenbank gespeichert sind. Wir verwenden das Repository-Muster und verfügen über Dienste (die Domänenschicht), die diese verwenden, und geben die EF-Entitäten direkt an die ASP.NET MVC-Controller zurück.

Es besteht jedoch die Anforderung, eine Drittanbieter-API (über einen Webdienst) zu verwenden, die uns zusätzliche Informationen liefert, die sich auf den Benutzer in unserer Datenbank beziehen. In unserer lokalen Benutzerdatenbank speichern wir eine externe ID, die wir der to-API zur Verfügung stellen können, um zusätzliche Informationen zu erhalten. Es sind eine ganze Reihe von Informationen verfügbar, aber der Einfachheit halber bezieht sich eine davon auf das Unternehmen des Benutzers (Name, Manager, Raum, Berufsbezeichnung, Standort usw.). Diese Informationen werden an verschiedenen Stellen in unseren Web-Apps verwendet - im Gegensatz zur Verwendung an einem einzigen Ort.

Meine Frage ist also, wo ist der beste Ort, um diese Informationen zu füllen und darauf zuzugreifen? Da es an verschiedenen Orten verwendet wird, ist es nicht wirklich sinnvoll, es auf Ad-hoc-Basis abzurufen, wo immer wir es in der Webanwendung verwenden. Daher ist es sinnvoll, diese zusätzlichen Daten von der Domänenschicht zurückzugeben.

Mein erster Gedanke war nur, eine Wrapper-Modellklasse zu erstellen, die die EF-Entität (EFUser) und eine neue 'ApiUser'-Klasse mit den neuen Informationen enthält - und wenn wir einen Benutzer erhalten, erhalten wir den EFUser und dann den zusätzlichen Informationen aus der API und füllen Sie das ApiUser-Objekt. Dies ist zwar in Ordnung, um einzelne Benutzer zu erhalten, fällt jedoch um, wenn mehrere Benutzer abgerufen werden. Wir können die API nicht aufrufen, wenn wir eine Liste von Benutzern erhalten.

Mein zweiter Gedanke war nur, der EFUser-Entität, die den ApiUser zurückgibt, eine Singleton-Methode hinzuzufügen und sie bei Bedarf einfach zu füllen. Dies löst das oben genannte Problem, da wir nur dann darauf zugreifen, wenn wir es benötigen.

Oder der letzte Gedanke war, eine lokale Kopie der Daten in unserer Datenbank zu behalten und sie mit der API zu synchronisieren, wenn sich der Benutzer anmeldet. Dies ist nur ein minimaler Aufwand, da es sich nur um einen Synchronisierungsprozess handelt - und wir haben nicht den Aufwand, sie zu treffen die DB und API jedes Mal, wenn wir Benutzerinformationen erhalten möchten. Dies bedeutet jedoch, dass die Daten an zwei Orten gespeichert werden und dass die Daten für jeden Benutzer, der sich eine Weile nicht angemeldet hat, veraltet sind.

Hat jemand Ratschläge oder Vorschläge, wie man mit solchen Szenarien am besten umgeht?

stevehayter
quelle
it's not really sensible to fetch it on an ad-hoc basis-- Warum? Aus Leistungsgründen?
Robert Harvey
Ich meine nicht, die API auf Ad-hoc-Basis zu treffen - ich meine nur, die vorhandene Entitätsstruktur beizubehalten und dann die API bei Bedarf in der Webanwendung ad-hoc aufzurufen - ich meinte nur, dass dies nicht der Fall ist sinnvoll, da es an vielen Orten getan werden müsste.
Stevehayter

Antworten:

3

Dein Fall

In Ihrem Fall sind alle drei Optionen realisierbar. Ich denke, dass die beste Option wahrscheinlich darin besteht, Ihre Datenquellen an einem Ort zu synchronisieren, den die asp.net-Anwendung nicht einmal kennt. Vermeiden Sie also jedes Mal die beiden Abrufe im Vordergrund und synchronisieren Sie die API stillschweigend mit der Datenbank. Wenn dies in Ihrem Fall eine praktikable Option ist, sage ich, tun Sie es.

Eine Lösung, bei der Sie den Abruf "einmal" durchführen, wie es die andere Antwort vorschlägt, scheint nicht sehr praktikabel zu sein, da die Antwort nirgendwo beibehalten wird und ASP.NET MVC den Abruf nur für jede Anforderung immer wieder vornimmt.

Ich würde den Singleton meiden, ich denke nicht, dass es aus vielen der üblichen Gründe überhaupt eine gute Idee ist.

Wenn die dritte Option nicht realisierbar ist, besteht eine Option darin, sie verzögert zu laden. Das heißt, eine Klasse soll die Entität erweitern und die API nach Bedarf treffen . Das ist jedoch eine sehr gefährliche Abstraktion, da es sich um einen noch magischeren und nicht offensichtlichen Zustand handelt.

Ich denke, es läuft wirklich auf mehrere Fragen hinaus:

  • Wie oft ändern sich die API-Aufrufdaten? Nicht oft? Dritte Option. Häufig? Plötzlich ist die dritte Option nicht mehr realisierbar. Ich bin mir nicht sicher, ob ich gegen Ad-hoc-Anrufe so bin wie Sie.
  • Wie teuer ist ein API-Aufruf? Zahlen Sie pro Anruf? Sind sie schnell? Frei? Wenn sie schnell sind, funktioniert es möglicherweise, jedes Mal einen Anruf zu tätigen. Wenn sie langsam sind, müssen Sie eine Vorhersage treffen und die Anrufe tätigen. Wenn sie Geld kosten, ist das ein großer Anreiz für das Caching.
  • Wie schnell muss die Reaktionszeit sein? Natürlich ist schneller besser, aber in einigen Fällen kann es sich lohnen, die Geschwindigkeit zu opfern, insbesondere wenn sie nicht direkt einem Benutzer zugewandt ist.
  • Wie unterschiedlich sind die API-Daten von Ihren Daten? Sind sie zwei konzeptionell unterschiedliche Dinge? In diesem Fall ist es möglicherweise sogar noch besser, die API nur außerhalb verfügbar zu machen, als das API-Ergebnis mit dem Ergebnis direkt zurückzugeben und die andere Seite den zweiten Aufruf ausführen zu lassen und die Verwaltung zu übernehmen.

Ein oder zwei Worte zur Trennung von Bedenken

Gestatten Sie mir, gegen das zu argumentieren, was Bobson hier über die Trennung von Bedenken sagt. Letztendlich verstößt das Einfügen dieser Logik in solche Entitäten ebenso gegen die Trennung von Bedenken.

Ein solches Repository verletzt die Trennung von Bedenken ebenso schlimm, indem es die präsentationszentrierte Logik in die Geschäftslogikschicht einfügt. Ihr Repository ist sich jetzt plötzlich der präsentationsbezogenen Dinge bewusst, z. B. wie Sie den Benutzer in Ihren asp.net mvc-Controllern anzeigen.

In dieser verwandten Frage habe ich nach dem direkten Zugriff auf Entitäten von einem Controller aus gefragt. Gestatten Sie mir, dort eine der Antworten zu zitieren:

"Willkommen bei BigPizza, dem benutzerdefinierten Pizzaladen. Darf ich Ihre Bestellung annehmen?" "Nun, ich hätte gerne eine Pizza mit Oliven, aber Tomatensauce oben und Käse unten und backe sie 90 Minuten lang im Ofen, bis sie schwarz und hart ist wie ein flacher Granitstein." "OK, Sir, benutzerdefinierte Pizzas sind unser Beruf, wir werden es schaffen."

Die Kassiererin geht in die Küche. "Da ist ein Psycho an der Theke, er möchte eine Pizza mit ... es ist ein Granitfelsen mit ... warte ... wir müssen zuerst einen Namen haben", sagt er dem Koch.

"Nein!", Schreit der Koch, "nicht schon wieder! Sie wissen, dass wir das schon versucht haben." Er nimmt einen Stapel Papier mit 400 Seiten. "Hier haben wir Granitfelsen aus dem Jahr 2005, aber ... es gab keine Oliven, sondern Paprika ... oder hier ist Top-Tomate ... aber der Kunde wollte es nur eine halbe Minute gebacken. " "Vielleicht sollten wir es TopTomatoGraniteRockSpecial nennen?" "Aber der Käse unten wird nicht berücksichtigt ..." Die Kassiererin: "Das soll Special ausdrücken." "Aber es wäre auch etwas Besonderes, wenn der Pizza-Stein wie eine Pyramide geformt würde", antwortet der Koch. "Hmmm ... es ist schwierig ...", sagt die verzweifelte Kassiererin.

"IST MEINE PIZZA BEREITS IM OFEN?", Schreit sie plötzlich durch die Küchentür. "Lassen Sie uns diese Diskussion beenden, sagen Sie mir einfach, wie man diese Pizza macht, wir werden keine solche Pizza ein zweites Mal haben", entscheidet der Koch. "OK, es ist eine Pizza mit Oliven, aber Tomatensauce oben und Käse unten und backe sie 90 Minuten lang im Ofen, bis sie schwarz und hart ist wie ein flacher Granitstein."

(Lesen Sie den Rest der Antwort, es ist wirklich schön imo).

Es ist naiv, die Tatsache zu ignorieren, dass es eine Datenbank gibt - es gibt eine Datenbank, und egal wie schwer Sie das abstrahieren möchten, es geht nirgendwo hin. Ihre Anwendung wird sich bewusst sein die Datenquelle. Sie können es nicht im laufenden Betrieb austauschen. ORMs sind nützlich, aber sie lecken aufgrund der Kompliziertheit des von ihnen gelösten Problems und aus zahlreichen Leistungsgründen (z. B. Select n + 1).

Benjamin Gruenbaum
quelle
Vielen Dank für Ihre sehr gründliche Antwort @Benjamin. Ich habe zunächst damit begonnen, einen Prototyp mit der oben genannten Bobson-Lösung zu erstellen (noch bevor er seine Antwort veröffentlicht hat), aber Sie sprechen einige wichtige Punkte an. Um Ihre Fragen zu beantworten: - Der API-Aufruf ist nicht sehr teuer (sie sind kostenlos und auch schnell). - Einige Teile der Daten ändern sich ziemlich regelmäßig (einige sogar alle paar Stunden). - Geschwindigkeit ist ziemlich wichtig, aber das Publikum der Anwendung ist so, dass blitzschnelles Laden keine absolute Voraussetzung ist.
Stevehayter
@stevehayter In diesem Fall würde ich höchstwahrscheinlich die Aufrufe der API von der Clientseite aus ausführen. Es ist billiger und schneller und bietet Ihnen eine feinkörnigere Kontrolle.
Benjamin Gruenbaum
1
Aufgrund dieser Antworten neige ich weniger dazu, eine lokale Kopie der Daten zu behalten. Eigentlich neige ich dazu, die API separat verfügbar zu machen und die zusätzlichen Daten auf diese Weise zu behandeln. Ich denke, dies mag ein guter Kompromiss zwischen der Einfachheit der @ Bobson-Lösung sein, fügt aber auch einen Grad an Trennung hinzu, bei dem ich mich etwas wohler fühle. Ich werde diese Strategie in meinem Prototyp betrachten und mit meinen Ergebnissen berichten (und das Kopfgeld vergeben!).
Stevehayter
@BenjaminGruenbaum - Ich bin nicht sicher, ob ich Ihrem Argument folge. Wie macht mein Vorschlag das Repository auf die Präsentation aufmerksam? Sicher, es ist bekannt, dass auf ein API-gestütztes Feld zugegriffen wurde, aber das hat nichts damit zu tun, was die Ansicht mit diesen Informationen macht.
Bobson
1
Ich habe mich dafür entschieden, alles auf die Clientseite zu verschieben - aber als Erweiterungsmethode auf dem EFUser (der in der Präsentationsschicht in einer separaten Assembly vorhanden ist). Die Methode gibt einfach die Daten von der API zurück und legt einen Singleton fest, damit er nicht wiederholt getroffen wird. Die Lebensdauer der Objekte ist so kurz, dass ich hier kein Problem mit einem Singleton habe. Auf diese Weise gibt es einen gewissen Grad an Trennung, aber ich habe immer noch die Möglichkeit, mit der EFUser-Entität zu arbeiten. Vielen Dank an alle Befragten für ihre Hilfe. Auf jeden Fall eine interessante Diskussion :).
Stevehayter
2

Bei einer ordnungsgemäßen Trennung der Bedenken sollte nichts über der Entity Framework / API-Ebene überhaupt erkennen, woher die Daten stammen. Sofern der API-Aufruf nicht teuer ist (in Bezug auf Zeit oder Verarbeitung), sollte der Zugriff auf Daten, die ihn verwenden, genauso transparent sein wie der Zugriff auf Daten aus der Datenbank.

Der beste Weg, dies zu implementieren, besteht darin, dem EFUserObjekt zusätzliche Eigenschaften hinzuzufügen, die die API-Daten nach Bedarf verzögern. Etwas wie das:

partial class EFUser
{
    private APIUser _apiUser;
    private APIUser ApiUser
    {
       get { 
          if (_apiUser == null) _apiUser = API.LoadUser(this.ExternalID);
          return _apiUser;
       }
    }
    public string CompanyName { get { return ApiUser.UserCompanyName; } }
    public string JobTitle{ get { return ApiUser.UserJobTitle; } }
}

Extern wird bei der ersten Verwendung CompanyNameoder JobTitlebei der ersten Verwendung ein einzelner API-Aufruf (und damit eine kleine Verzögerung) ausgeführt. Alle nachfolgenden Aufrufe bis zur Zerstörung des Objekts sind jedoch genauso schnell und einfach wie der Datenbankzugriff.

Bobson
quelle
Vielen Dank an @Bobson ... dies war eigentlich die erste Route, die ich eingeschlagen habe (mit einigen Erweiterungsmethoden, die hinzugefügt wurden, um die Details für Benutzerlisten in großen Mengen zu laden - zum Beispiel den Firmennamen für eine Benutzerliste anzuzeigen). Es scheint bisher gut zu meinen Bedürfnissen zu passen - aber Benjamin unten wirft einige wichtige Punkte auf, deshalb werde ich diese Woche weiter evaluieren.
Stevehayter
0

Eine Idee ist, ApiUser so zu ändern, dass nicht immer die zusätzlichen Informationen vorhanden sind. Stattdessen legen Sie eine Methode auf ApiUser, um sie abzurufen:

ApiUser apiUser = backend.load($id);
//Locally available data directly on the ApiUser like so:
String name = apiUser.getName();
//Expensive extra data available after extra call:
UserExtraData extraData = apiUser.fetchExtraData();
String managerName = extraData.getManagerName();

Sie können dies auch geringfügig ändern, um das verzögerte Laden zusätzlicher Daten zu verwenden, sodass Sie UserExtraData nicht aus dem ApiUser-Objekt extrahieren müssen:

//Extra data fetched on first get:
String managerName = apiUser.lazyGetExtraData().getManagerName();

Auf diese Weise werden bei einer Liste die zusätzlichen Daten standardmäßig nicht abgerufen. Sie können aber trotzdem darauf zugreifen, während Sie die Liste durchlaufen!

Alexander Torstling
quelle
Sie sind sich nicht sicher, was Sie hier meinen - in backend.load () laden wir bereits - also würde das sicher die API-Daten laden?
Stevehayter
Ich meine, Sie sollten warten, bis Sie explizit dazu aufgefordert werden - faul die API-Daten zu laden.
Alexander Torstling