Entità Framework Entità - Alcuni dati dal servizio Web - Migliore architettura?


10

Attualmente stiamo utilizzando Entity Framework come ORM in alcune applicazioni Web e fino ad ora, ci ha adattato bene poiché tutti i nostri dati sono archiviati in un unico database. Stiamo utilizzando il modello di repository e disponiamo di servizi (livello di dominio) che li utilizzano e restituiscono le entità EF direttamente ai controller MVC ASP.NET.

Tuttavia, è emersa la necessità di utilizzare un'API di terze parti (tramite un servizio Web) che ci fornirà ulteriori informazioni relative all'utente nel nostro database. Nel nostro database utenti locale, memorizzeremo un ID esterno che possiamo fornire all'API per ottenere ulteriori informazioni. Sono disponibili molte informazioni, ma per motivi di semplicità, una di queste riguarda l'azienda dell'utente (nome, responsabile, stanza, titolo di lavoro, posizione ecc.). Queste informazioni verranno utilizzate in vari punti delle nostre app Web, anziché essere utilizzate in un unico posto.

Quindi la mia domanda è: dov'è il posto migliore per popolare e accedere a queste informazioni? Poiché viene utilizzato in vari luoghi, non è davvero ragionevole recuperarlo su una base ad-hoc ovunque utilizziamo nell'applicazione Web, quindi ha senso restituire questi dati aggiuntivi dal livello di dominio.

Il mio pensiero iniziale era solo quello di creare una classe di modello wrapper che avrebbe contenuto l'entità EF (EFUser) e una nuova classe "ApiUser" contenente le nuove informazioni - e quando otteniamo un utente, otteniamo l'EFUser e quindi riceviamo l'ulteriore informazioni dall'API e popola l'oggetto ApiUser. Tuttavia, mentre ciò andrebbe bene per ottenere singoli utenti, cade quando si ottengono più utenti. Non possiamo colpire l'API quando si ottiene un elenco di utenti.

Il mio secondo pensiero era solo quello di aggiungere un metodo singleton all'entità EFUser che restituisce ApiUser e lo popolava solo quando necessario. Questo risolve il problema di cui sopra poiché accediamo ad esso solo quando ne abbiamo bisogno.

Oppure il pensiero finale era quello di conservare una copia locale dei dati nel nostro database e sincronizzarli con l'API quando l'utente accede. Questo è un lavoro minimo in quanto è solo un processo di sincronizzazione - e non abbiamo il sovraccarico di colpire il DB e l'API ogni volta che vogliamo ottenere informazioni sull'utente. Tuttavia, ciò significa archiviare i dati in due posizioni e significa che i dati non sono aggiornati per tutti gli utenti che non effettuano l'accesso da un po 'di tempo.

Qualcuno ha qualche consiglio o suggerimento su come gestire al meglio questo tipo di scenario?


it's not really sensible to fetch it on an ad-hoc basis-- Perché? Per motivi di prestazioni?
Robert Harvey,

Non intendo colpire l'API su una base ad hoc - intendo solo mantenere la struttura di entità esistente così com'è, e quindi chiamare l'API ad hoc nell'applicazione Web quando necessario - Volevo solo dire che non sarebbe ragionevole in quanto dovrebbe essere fatto in molti posti.
stevehayter,

Risposte:


3

Il tuo caso

Nel tuo caso tutte e tre le opzioni sono praticabili. Penso che l'opzione migliore sia probabilmente quella di sincronizzare le tue fonti di dati da qualche parte che l'applicazione asp.net non è nemmeno a conoscenza. Cioè, evitare i due recuperi in primo piano ogni volta, sincronizzare l'API con il db in silenzio). Quindi se questa è un'opzione praticabile nel tuo caso, lo dico io.

Una soluzione in cui esegui il recupero "una volta" come suggerisce l'altra risposta non sembra molto praticabile poiché non persiste la risposta da nessuna parte e ASP.NET MVC eseguirà il recupero per ogni richiesta più e più volte.

Eviterei il singleton, non credo sia una buona idea per molte delle solite ragioni.

Se la terza opzione non è praticabile, un'opzione è quella di caricarla lentamente. Cioè, avere una classe estendere l'entità e farlo colpire sull'API in base alle necessità . Questa è un'astrazione molto pericolosa, poiché è ancora più magico e non ovvio.

Immagino che si riduce davvero a diverse domande:

  • Con quale frequenza vengono modificati i dati di chiamata dell'API? Non spesso? Terza opzione. Spesso? Improvvisamente la terza opzione non è troppo praticabile. Non sono sicuro di essere contrario alle chiamate ad hoc come te.
  • Quanto costa una chiamata API? Paghi per chiamata? Sono veloci? Gratuito? Se sono veloci, effettuare una chiamata ogni volta potrebbe funzionare, se sono lenti è necessario disporre di una sorta di previsione in atto ed effettuare le chiamate. Se costano soldi, questo è un grande incentivo per la memorizzazione nella cache.
  • Quanto deve essere veloce il tempo di risposta? Ovviamente più veloce è meglio, ma sacrificare la velocità per semplicità potrebbe valerne la pena in alcuni casi, specialmente se non è direttamente rivolto all'utente.
  • Quanto sono diversi i dati API dai tuoi dati? Sono due cose concettualmente diverse? In tal caso, potrebbe essere ancora meglio esporre l'API all'esterno anziché restituire il risultato API con il risultato direttamente e lasciare che l'altra parte effettui la seconda chiamata e gestisca la gestione.

Una parola o due sulla separazione delle preoccupazioni

Consentitemi di discutere contro ciò che Bobson sta dicendo sulla separazione delle preoccupazioni qui. Alla fine della giornata, mettere quella logica nelle entità del genere viola la separazione delle preoccupazioni altrettanto male.

Avere un archivio di questo tipo viola la separazione delle preoccupazioni altrettanto male mettendo la logica incentrata sulla presentazione nel livello della logica aziendale. Il tuo repository ora è improvvisamente a conoscenza delle cose relative alla presentazione come il modo in cui visualizzi l'utente nei tuoi controller mvc asp.net.

In questa domanda correlata ho chiesto di accedere alle entità direttamente da un controller. Mi permetta di citare una delle risposte lì:

"Benvenuto in BigPizza, la pizzeria personalizzata, posso prendere il tuo ordine?" "Beh, mi piacerebbe avere una pizza con le olive, ma la salsa di pomodoro in cima e il formaggio in fondo e cuocerla in forno per 90 minuti fino a quando è nera e dura come una roccia piatta di granito." "Ok, signore, le pizze personalizzate sono la nostra professione, ce la faremo."

La cassiera va in cucina. "C'è uno psicopatico al bancone, vuole avere una pizza con ... è una roccia di granito con ... aspetta ... dobbiamo prima avere un nome", dice al cuoco.

"No!", Urla il cuoco, "non di nuovo! Sai che ci abbiamo già provato." Prende una risma di carta con 400 pagine, "qui abbiamo roccia di granito del 2005, ma ... non aveva olive, ma paprica invece ... o qui è il pomodoro migliore ... ma il cliente lo voleva cotto solo mezzo minuto. " "Forse dovremmo chiamarlo TopTomatoGraniteRockSpecial?" "Ma non tiene conto del formaggio in fondo ..." La cassiera: "Questo è ciò che si suppone debba esprimere Special." "Ma avere la roccia della Pizza formata come una piramide sarebbe anche speciale", risponde il cuoco. "Hmmm ... è difficile ...", dice il cassiere disperato.

"LA MIA PIZZA È GIÀ NEL FORNO?", Improvvisamente urla attraverso la porta della cucina. "Smettiamo di discutere, dimmi solo come preparare questa pizza, non avremo una pizza del genere una seconda volta", decide il cuoco. "OK, è una pizza con olive, ma salsa di pomodoro in cima e formaggio in fondo e cuocila in forno per 90 minuti fino a quando è nera e dura come una roccia piatta di granito."

(Leggi il resto della risposta, è davvero bello imo).

È ingenuo ignorare il fatto che esiste un database : esiste un database e non importa quanto sia difficile sottrarlo, non andrà da nessuna parte. La tua applicazione sarà a conoscenza dell'origine dati. Non potrai "sostituirlo a caldo". Gli ORM sono utili ma perdono a causa di quanto sia complicato il problema che risolvono e per molti motivi di prestazioni (come selezionare ad esempio n + 1).


Grazie per la tua risposta molto approfondita @ Benjamin. Inizialmente ho iniziato a creare un prototipo usando la soluzione di Bobson sopra (anche prima che pubblicasse la sua risposta), ma tu sollevi alcuni punti importanti. Per rispondere alle tue domande: - La chiamata API non è molto costosa (sono gratuiti e anche veloci). - Alcune parti dei dati cambieranno abbastanza regolarmente (alcune anche ogni due ore). - La velocità è abbastanza importante, ma il pubblico dell'applicazione è tale che un caricamento rapido alleggerito non è un requisito assoluto.
Stevehayter,

@stevehayter In quel caso molto probabilmente eseguirò le chiamate all'API dal lato client. È più economico e veloce e ti offre un controllo più preciso.
Benjamin Gruenbaum,

1
Sulla base di queste risposte, mi propongo meno di conservare una copia locale dei dati. In realtà mi sto impegnando per esporre l'API separatamente e gestire i dati aggiuntivi in ​​quel modo. Penso che questo possa essere un buon compromesso tra la semplicità della soluzione di @ Bobson, ma aggiunge anche un grado di separazione di cui mi sento un po 'più a mio agio. Esaminerò questa strategia nel mio prototipo e riferirò con i miei risultati (e assegnerò il premio!).
Stevehayter,

@BenjaminGruenbaum - Non sono sicuro di seguire il tuo argomento. In che modo il mio suggerimento rende il repository consapevole della presentazione? Certo, è consapevole che è stato effettuato l'accesso a un campo supportato da API, ma ciò non ha nulla a che fare con ciò che la vista sta facendo con tali informazioni.
Bobson,

1
Ho scelto di spostare tutto sul lato client, ma come metodo di estensione su EFUser (che esiste nel livello di presentazione, in un assieme separato). Il metodo restituisce semplicemente i dati dall'API e imposta un singleton in modo che non venga colpito ripetutamente. La durata degli oggetti è così breve che non ho problemi a usare un singleton qui. In questo modo esiste un certo grado di separazione, ma ho ancora la comodità di lavorare con l'entità EFUser. Grazie a tutti gli intervistati per il loro aiuto. Sicuramente una discussione interessante :).
stevehayter,

2

Con un'adeguata separazione delle preoccupazioni , nulla al di sopra del livello Entity Framework / API dovrebbe persino rendersi conto da dove provengono i dati. A meno che la chiamata API non sia costosa (in termini di tempo o di elaborazione), l'accesso ai dati che la utilizza dovrebbe essere trasparente come l'accesso ai dati dal database.

Il modo migliore per implementare questo, quindi, sarebbe aggiungere proprietà extra EFUserall'oggetto che caricano lazy i dati dell'API secondo necessità. Qualcosa come questo:

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

Esternamente, la prima volta che viene utilizzato CompanyNameo JobTitleverrà eseguita una singola chiamata API (e quindi un piccolo ritardo), ma tutte le chiamate successive fino alla distruzione dell'oggetto saranno altrettanto veloci e facili dell'accesso al database.


Grazie @Bobson ... questo è stato in realtà il percorso iniziale che ho iniziato a scendere (con alcuni metodi di estensione aggiunti per caricare in blocco i dettagli per gli elenchi di utenti, ad esempio visualizzando il nome dell'azienda per un elenco di utenti). Finora sembra soddisfare le mie esigenze, ma Benjamin di seguito solleva alcuni punti importanti, quindi continuerò a valutare questa settimana.
Stevehayter,

0

Un'idea è quella di modificare ApiUser per non avere sempre le informazioni extra. Invece, metti un metodo su ApiUser per recuperarlo:

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

Puoi anche modificarlo leggermente per utilizzare il caricamento lento di dati extra, in modo da non dover estrarre UserExtraData dall'oggetto ApiUser:

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

In questo modo, quando si dispone di un elenco, i dati extra non verranno recuperati per impostazione predefinita. Ma puoi ancora accedervi mentre attraversi l'elenco!


Non sei davvero sicuro di cosa intendi qui - in backend.load (), stiamo già facendo un carico - così sicuramente che caricare i dati API?
stevehayter,

Voglio dire che dovresti aspettare a fare il carico extra fino a quando non viene esplicitamente richiesto - carica lazy i dati API.
Alexander Torstling,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.