Entity Framework Entities - Certaines données du service Web - Meilleure architecture?

10

Nous utilisons actuellement Entity Framework comme ORM dans quelques applications Web, et jusqu'à présent, cela nous convenait bien car toutes nos données sont stockées dans une seule base de données. Nous utilisons le modèle de référentiel et avons des services (la couche de domaine) qui les utilisent et renvoyons les entités EF directement aux contrôleurs ASP.NET MVC.

Cependant, une exigence est venue d'utiliser une API tierce (via un service Web) qui nous donnera des informations supplémentaires concernant l'utilisateur dans notre base de données. Dans notre base de données d'utilisateurs locale, nous stockons un ID externe que nous pouvons fournir à l'API pour obtenir des informations supplémentaires. Il y a beaucoup d'informations disponibles, mais pour des raisons de simplicité, l'une d'elles concerne l'entreprise de l'utilisateur (nom, responsable, chambre, titre du poste, emplacement, etc.). Ces informations seront utilisées à divers endroits dans nos applications Web - par opposition à être utilisées dans un seul endroit.

Ma question est donc la suivante: quel est le meilleur endroit pour remplir et accéder à ces informations? Comme il est utilisé à divers endroits, il n'est pas vraiment judicieux de le récupérer sur une base ad hoc partout où nous l'utilisons dans l'application Web - il est donc logique de renvoyer ces données supplémentaires à partir de la couche de domaine.

Ma pensée initiale était juste de créer une classe de modèle wrapper qui contiendrait l'entité EF (EFUser), et une nouvelle classe 'ApiUser' contenant les nouvelles informations - et lorsque nous obtenons un utilisateur, nous obtenons l'EFUser, puis les informations supplémentaires informations de l'API et remplissez l'objet ApiUser. Cependant, bien que ce soit bien pour obtenir des utilisateurs uniques, cela tombe lorsque vous obtenez plusieurs utilisateurs. Nous ne pouvons pas atteindre l'API lors de l'obtention d'une liste d'utilisateurs.

Ma deuxième pensée a été simplement d'ajouter une méthode singleton à l'entité EFUser qui renvoie l'ApiUser, et de la remplir en cas de besoin. Cela résout le problème ci-dessus car nous n'y accédons que lorsque nous en avons besoin.

Ou la dernière pensée était de conserver une copie locale des données dans notre base de données et de la synchroniser avec l'API lorsque l'utilisateur se connecte. C'est un travail minimal car c'est juste un processus de synchronisation - et nous n'avons pas la surcharge de frapper la base de données et l'API chaque fois que nous voulons obtenir des informations sur les utilisateurs. Cependant, cela signifie stocker les données à deux endroits et signifie également que les données sont obsolètes pour tout utilisateur qui ne s'est pas connecté depuis un certain temps.

Quelqu'un a-t-il des conseils ou des suggestions sur la meilleure façon de gérer ce type de scénario?

stevehayter
la source
it's not really sensible to fetch it on an ad-hoc basis-- Pourquoi? Pour des raisons de performances?
Robert Harvey
Je ne veux pas dire frapper l'API sur une base ad hoc - je veux juste dire garder la structure d'entité existante telle qu'elle est, puis appeler l'API ad hoc dans l'application Web lorsque cela est nécessaire - je voulais juste dire que ce ne serait pas sensé car cela devrait être fait dans beaucoup d'endroits.
stevehayter

Réponses:

3

Ton cas

Dans votre cas, les trois options sont viables. Je pense que la meilleure option est probablement de synchroniser vos sources de données à un endroit dont l'application asp.net n'est même pas au courant. Autrement dit, évitez les deux récupérations au premier plan à chaque fois, synchronisez l'API avec la base de données en silence). Donc, si c'est une option viable dans votre cas - je dis de le faire.

Une solution où vous effectuez la récupération `` une fois '' comme le suggère l'autre réponse ne semble pas très viable car elle ne persiste pas la réponse n'importe où et ASP.NET MVC effectuera simplement la récupération pour chaque demande encore et encore.

J'éviterais le singleton, je ne pense pas que ce soit une bonne idée du tout pour plein de raisons habituelles.

Si la troisième option n'est pas viable - une option est de la charger paresseusement. Autrement dit, demandez à une classe d'étendre l'entité et faites-la toucher l'API au besoin . C'est une abstraction très dangereuse car c'est un état encore plus magique et non évident.

Je suppose que cela se résume vraiment à plusieurs questions:

  • À quelle fréquence les données d'appel de l'API changent-elles? Pas souvent? Troisième option. Souvent? Du coup, la troisième option n'est pas trop viable. Je ne suis pas sûr d'être aussi contre les appels ad hoc que vous.
  • Combien coûte un appel API? Payez-vous par appel? Sont-ils rapides? Gratuit? S'ils sont rapides, passer un appel à chaque fois peut fonctionner, s'ils sont lents, vous devez avoir une sorte de prédiction en place et passer les appels. S'ils coûtent de l'argent - c'est une grande incitation à la mise en cache.
  • À quelle vitesse le temps de réponse doit-il être? Évidemment, plus vite est mieux, mais sacrifier la vitesse pour la simplicité peut valoir la peine dans certains cas, surtout si ce n'est pas directement face à un utilisateur.
  • Quelle est la différence entre les données API et vos données? S'agit-il de deux choses conceptuellement différentes? Si c'est le cas, il pourrait être encore mieux d'exposer l'API à l'extérieur plutôt que de renvoyer directement le résultat de l'API avec le résultat et de laisser l'autre côté effectuer le deuxième appel et gérer sa gestion.

Un mot ou deux sur la séparation des préoccupations

Permettez-moi de contester ce que Bobson dit à propos de la séparation des préoccupations ici. À la fin de la journée - mettre cette logique dans des entités comme ça viole tout aussi mal la séparation des préoccupations.

Le fait d'avoir un tel référentiel viole tout autant la séparation des préoccupations en plaçant la logique centrée sur la présentation dans la couche logique métier. Votre référentiel est maintenant soudain au courant des choses liées à la présentation, comme la façon dont vous affichez l'utilisateur dans vos contrôleurs mvc asp.net.

Dans cette question connexe, j'ai posé des questions sur l'accès aux entités directement à partir d'un contrôleur. Permettez-moi de citer l'une des réponses:

"Bienvenue chez BigPizza, le magasin de pizza personnalisé, puis-je prendre votre commande?" "Eh bien, j'aimerais avoir une pizza aux olives, mais de la sauce tomate sur le dessus et du fromage au fond et la faire cuire au four pendant 90 minutes jusqu'à ce qu'elle soit noire et dure comme une roche plate de granit." "OK, monsieur, les pizzas personnalisées sont notre profession, nous allons y arriver."

La caissière va à la cuisine. "Il y a un psycho au comptoir, il veut avoir une pizza avec ... c'est un rocher de granit avec ... attendez ... nous devons d'abord avoir un nom", dit-il au cuisinier.

"Non!", Crie le cuisinier, "pas encore! Tu sais qu'on a déjà essayé ça." Il prend une pile de papier de 400 pages, "ici nous avons un rocher de granit de 2005, mais ... il n'y avait pas d'olives, mais du paprica à la place ... ou voici du top tomate ... mais le client le voulait cuit au four seulement une demi-minute. " "Peut-être que nous devrions l'appeler TopTomatoGraniteRockSpecial?" "Mais cela ne tient pas compte du fromage du fond ..." La caissière: "C'est ce que Special est censé exprimer." "Mais avoir la roche Pizza formée comme une pyramide serait aussi spécial", répond le cuisinier. "Hmmm ... c'est difficile ...", dit la caissière désespérée.

"MA PIZZA EST-ELLE DÉJÀ DANS LE FOUR?", Crie soudain la porte de la cuisine. "Arrêtons cette discussion, dis-moi juste comment faire cette pizza, on ne va pas avoir une telle pizza une deuxième fois", décide le cuisinier. "D'accord, c'est une pizza aux olives, mais de la sauce tomate sur le dessus et du fromage au fond et faites-la cuire au four pendant 90 minutes jusqu'à ce qu'elle soit noire et dure comme une roche plate de granit."

(Lisez le reste de la réponse, c'est vraiment sympa imo).

Il est naïf d'ignorer le fait qu'il existe une base de données - il existe une base de données, et peu importe à quel point vous voulez en résumer, cela ne va nulle part. Votre demande sera être au courant de la source de données. Vous ne pourrez pas le «remplacer à chaud». Les ORM sont utiles mais ils fuient en raison de la complexité du problème qu'ils résolvent et pour de nombreuses raisons de performances (comme Select n + 1 par exemple).

Benjamin Gruenbaum
la source
Merci pour votre réponse très approfondie @Benjamin. J'ai d'abord commencé à créer un prototype en utilisant la solution de Bobson ci-dessus (avant même qu'il ne poste sa réponse), mais vous soulevez quelques points importants. Pour répondre à vos questions,: - L'appel API n'est pas très cher (ils sont gratuits et aussi rapides). - Certaines parties des données changeront assez régulièrement (certaines même toutes les deux heures). - La vitesse est assez importante, mais le public de l'application est tel que l'allégement du chargement rapide n'est pas une exigence absolue.
stevehayter
@stevehayter Dans ce cas, je ferais très probablement les appels à l'API du côté client. C'est moins cher et plus rapide, et cela vous donne un contrôle plus fin.
Benjamin Gruenbaum
1
Sur la base de ces réponses, je penche moins vers la conservation d'une copie locale des données. Je penche en fait pour exposer l'API séparément et pour gérer les données supplémentaires de cette façon. Je pense que cela peut être un bon compromis entre la simplicité de la solution de @ Bobson, mais ajoute également un degré de séparation sur lequel je suis un peu plus à l'aise. J'examinerai cette stratégie dans mon prototype, et je ferai rapport de mes découvertes (et récompenserai la prime!).
stevehayter
@BenjaminGruenbaum - Je ne suis pas sûr de suivre votre argument. Comment ma suggestion rend-elle le référentiel conscient de la présentation? Bien sûr, il sait qu'un champ soutenu par l'API a été consulté, mais cela n'a rien à voir avec ce que la vue fait avec ces informations.
Bobson
1
J'ai choisi de tout déplacer du côté client - mais comme méthode d'extension sur l'EFUser (qui existe dans la couche de présentation, dans un assemblage séparé). La méthode renvoie simplement les données de l'API et définit un singleton afin qu'il ne soit pas frappé à plusieurs reprises. La durée de vie des objets est si courte que je n'ai aucun problème à utiliser un singleton ici. De cette façon, il y a un certain degré de séparation, mais j'ai toujours la possibilité de travailler avec l'entité EFUser. Merci à tous les répondants pour leur aide. Certainement une discussion intéressante :).
stevehayter
2

Avec une séparation adéquate des préoccupations , rien au-dessus du niveau Entity Framework / API ne devrait même réaliser d'où viennent les données. À moins que l'appel d'API soit coûteux (en termes de temps ou de traitement), l'accès aux données qui l'utilisent doit être aussi transparent que l'accès aux données de la base de données.

La meilleure façon de l'implémenter serait alors d'ajouter des propriétés supplémentaires à l' EFUserobjet qui chargeraient les données API paresseux selon les besoins. Quelque chose comme ça:

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

Extérieurement, la première fois que l'un CompanyNameou l' autre JobTitleest utilisé, il y aura un seul appel API (et donc un petit délai), mais tous les appels suivants jusqu'à ce que l'objet soit détruit seront aussi rapides et faciles que l'accès à la base de données.

Bobson
la source
Merci @Bobson ... c'était en fait l'itinéraire initial que j'ai commencé à suivre (avec quelques méthodes d'extension ajoutées pour charger en masse les détails des listes d'utilisateurs - par exemple, afficher le nom de l'entreprise pour une liste d'utilisateurs). Cela semble bien répondre à mes besoins jusqu'à présent - mais Benjamin ci-dessous soulève quelques points importants, donc je vais continuer à évaluer cette semaine.
stevehayter
0

Une idée est de modifier ApiUser pour ne pas toujours avoir les informations supplémentaires. Au lieu de cela, vous mettez une méthode sur ApiUser pour la récupérer:

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

Vous pouvez également modifier légèrement ceci pour utiliser le chargement paresseux de données supplémentaires, de sorte que vous n'ayez pas à extraire UserExtraData de l'objet ApiUser:

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

De cette façon, lorsque vous avez une liste, les données supplémentaires ne seront pas récupérées par défaut. Mais vous pouvez toujours y accéder tout en parcourant la liste!

Alexander Torstling
la source
Vous ne savez pas vraiment ce que vous voulez dire ici - dans backend.load (), nous effectuons déjà un chargement - alors cela chargerait sûrement les données de l'API?
stevehayter
Je veux dire que vous devez attendre de faire le chargement supplémentaire jusqu'à ce qu'il soit explicitement demandé - charger paresseusement les données de l'API.
Alexander Torstling