Les référentiels doivent-ils retourner IQueryable?

66

J'ai vu beaucoup de projets qui ont des référentiels qui renvoient des instances de IQueryable. Cela permet des filtres supplémentaires et le tri peut être effectué sur un IQueryableautre code, ce qui se traduit par la génération de SQL différent. Je suis curieux de savoir d'où vient ce modèle et si c'est une bonne idée.

Ma plus grande préoccupation est qu’il IQueryables’agisse d’une promesse de consulter la base de données quelque temps plus tard, lorsqu’elle est énumérée. Cela signifie qu'une erreur serait renvoyée à l'extérieur du référentiel. Cela pourrait signifier qu'une exception Entity Framework est renvoyée dans une couche différente de l'application.

J'ai également rencontré des problèmes avec plusieurs ensembles de résultats actifs (MARS) dans le passé (en particulier lors de l'utilisation de transactions) et cette approche semble donner lieu à ce que cela se produise plus souvent.

J'ai toujours appelé AsEnumerableou ToArrayà la fin de chacune de mes expressions LINQ pour m'assurer que la base de données est touchée avant de quitter le code du référentiel.

Je me demande si le retour IQueryablepourrait être utile en tant que bloc de construction d'une couche de données. J'ai vu du code assez extravagant avec un référentiel appelant un autre référentiel pour en créer un encore plus volumineux IQueryable.

Parcs Travis
la source
Quelle serait la différence si la base de données n'était pas disponible au moment de l'exécution de la requête différée par rapport au moment de la construction de la requête? Le code de consommation devrait faire face à cette situation de toute façon.
Steven Evers
Question intéressante, mais il sera probablement difficile de répondre à cette question. Une recherche simple révèle un débat assez animé sur votre question: duckduckgo.com/?q=repository+return+iqueryable
Corbin Mars
6
Tout se résume à savoir si vous souhaitez autoriser vos clients à ajouter des clauses Linq pouvant bénéficier d'une exécution différée. S'ils ajoutent une whereclause à un différé IQueryable, il vous suffit d'envoyer ces données par câble, et non l'ensemble du résultat.
Robert Harvey
Si vous utilisez un modèle de référentiel - non, vous ne devez pas renvoyer IQueryable. Voici un bon pourquoi .
Arnis Lapsa
1
@SteveEvers Il y a une énorme différence. Si une exception est levée dans mon référentiel, je peux l'envelopper avec un type d'exception spécifique à la couche de données. Si cela se produit plus tard, je dois capturer le type d'exception original à cet endroit. Plus je serai proche de la source de l'exception, plus je saurai quelles en sont les causes. Ceci est important pour créer des messages d'erreur significatifs. IMHO, permettre à une exception spécifique à EF de quitter mon référentiel est une violation de l'encapsulation.
Travis Parks

Réponses:

47

Renvoyer IQueryable donnera certainement plus de flexibilité aux utilisateurs du référentiel. Il incombe au client de limiter les résultats, ce qui peut naturellement être à la fois un avantage et une béquille.

Du côté positif, vous n'aurez pas besoin de créer des tonnes de méthodes de référentiel (du moins sur cette couche) - GetAllActiveItems, GetAllNonActiveItems, etc. - pour obtenir les données souhaitées. Selon vos préférences, encore une fois, cela pourrait être bon ou mauvais. Vous devrez (/ devriez) définir les contrats de comportement auxquels adhèrent vos implémentations, mais vous en êtes responsable.

Vous pouvez donc placer la logique d'extraction saccadée à l'extérieur du référentiel et la laisser être utilisée comme l'utilisateur le souhaite. Ainsi, exposer IQueryable donne le maximum de flexibilité et permet une interrogation efficace par opposition au filtrage en mémoire, etc., et pourrait réduire la nécessité de créer une tonne de méthodes d'extraction de données spécifiques.

D'autre part, vous avez maintenant donné un fusil de chasse à vos utilisateurs. Ils peuvent faire des choses que vous n’avez peut-être pas prévues (utiliser excessivement .include (), faire de lourdes requêtes et faire du filtrage en mémoire dans leurs implémentations respectives, etc.), ce qui détournerait des contrôles de superposition et de comportement, car vous avez accès total.

Donc, en fonction de l’équipe, de leur expérience, de la taille de l’application, de la superposition en couches et de l’architecture… tout dépend: - \

hanzolo
la source
Notez que vous pouvez, si vous le souhaitez, retourner votre IQueryable, qui à son tour appelle la base de données, mais ajoute des contrôles de superposition et de comportement.
psr
1
@ psr Pourriez-vous expliquer ce que vous entendez par là?
Travis Parks
1
@TravisParks - IQueryable n'a pas à être implémenté par la base de données, vous pouvez écrire des classes IQueryable vous-même - c'est assez difficile. Vous pouvez donc écrire un wrapper IQueryable autour d'une base de données IQueryable et demander à IQueryable de limiter l'accès complet à la base de données sous-jacente. Mais c'est assez difficile pour que je ne m'attende pas à ce que ce soit fait largement.
psr
2
Notez que même lorsque vous ne renvoyez pas IQueryables, vous pouvez toujours éviter de créer de nombreuses méthodes (GetAllActiveItems, GetAllNonActiveItems, etc.) en transmettant simplement une Expression<Func<Foo,bool>>méthode à votre méthode. Ces paramètres peuvent être passés Wheredirectement à la méthode, permettant ainsi au consommateur de choisir son filtre sans avoir besoin d'un accès direct à IQueryable <Foo>.
Flater
1
@hanzolo: Cela dépend de ce que vous entendez par refactorisation. Il est vrai que la modification de la conception (par exemple, l'ajout de nouveaux champs ou la modification des relations) est plus difficile lorsque vous avez une base de code superposée et faiblement couplée. mais la refactorisation est moins pénible quand il suffit de changer un calque. Tout dépend si vous envisagez de repenser l'application ou s'il s'agit simplement de refactoriser votre implémentation de la même architecture de base. Je recommanderais toujours le couplage lâche dans les deux cas, mais vous ne vous trompez pas: cela ajoute au refactoring lors de la modification des concepts de domaine de base.
Flater
29

Exposer IQueryable aux interfaces publiques n'est pas une bonne pratique. La raison est fondamentale: vous ne pouvez pas fournir la réalisation IQueryable telle qu'elle est dite.

L'interface publique est un contrat entre fournisseur et clients. Et dans la plupart des cas, on s'attend à une mise en œuvre complète. IQueryable est un tricheur:

  1. IQueryable est presque impossible à mettre en œuvre. Et actuellement, il n'y a pas de solution appropriée. Même Entity Framework a beaucoup d' exceptions UnsupportedExceptions .
  2. Aujourd'hui, les fournisseurs interrogeables dépendent fortement de l'infrastructure. Sharepoint a sa propre implémentation partielle et le fournisseur My SQL a une implémentation partielle distincte.

Le modèle de référentiel nous donne une représentation claire par couches et, plus important encore, une indépendance de réalisation. Donc, nous pouvons changer dans la source de données future.

La question est " Devrait-il y avoir une possibilité de substitution de source de données? ".

Si demain une partie des données peut être déplacée vers les services SAP, la base de données NoSQL ou simplement vers les fichiers texte, pouvons-nous garantir une mise en œuvre correcte de l'interface IQueryable?

( plus de bons points sur pourquoi c'est une mauvaise pratique)

Artru
la source
Cette réponse ne va pas de soi sans consulter le lien externe volatile. Essayez de résumer au moins les points clés soulevés par la ressource externe et essayez de garder votre opinion personnelle en dehors de votre réponse ("pire idée que j'ai entendue"). Pensez également à supprimer l'extrait de code qui semble ne pas être lié à quoi que ce soit IQueryable.
Lars Viklund
1
Merci @LarsViklund, la réponse est mise à jour avec des exemples et une description du problème
Artru
19

De manière réaliste, vous avez trois alternatives si vous souhaitez une exécution différée:

  • Faites-le de cette façon - exposez un IQueryable.
  • Implémentez une structure qui expose des méthodes spécifiques pour des filtres ou des "questions" spécifiques. ( GetCustomersInAlaskaWithChildrenci-dessous)
  • Implémentez une structure qui expose une API de filtre / tri / page fortement typée et la construit en IQueryableinterne.

Je préfère le troisième (bien que j'aie aidé des collègues à mettre en œuvre le second également), mais il est évident que certaines opérations de configuration et de maintenance sont nécessaires. (T4 à la rescousse!)

Edit : Pour dissiper la confusion qui entoure ce dont je parle, prenons l'exemple suivant, volé à IQueryable: peut tuer votre chien, voler votre femme, tuer votre volonté de vivre, etc.

Dans le cas où vous exposeriez quelque chose comme ceci:

public class CustomerRepo : IRepo 
{ 
     private DataContext ct; 
     public Customer GetCustomerById(int id) { ... } 
     public Customer[] GetCustomersInAlaskaWithChildren() { ... } 
} 

L'API dont je parle vous permettrait d'exposer une méthode qui vous permettrait d'exprimer GetCustomersInAlaskaWithChildren(ou toute autre combinaison de critères) de manière flexible, et le référentiel l'exécuterait comme un IQueryable et vous renverrait les résultats. Mais l’important est qu’il s’exécute à l’intérieur de la couche de référentiel, de sorte qu’il profite toujours de l’exécution différée. Une fois que vous avez obtenu les résultats, vous pouvez toujours utiliser LINQ-to-Objects à votre guise.

Un autre avantage d’une approche comme celle-ci est que, comme il s’agit de classes POCO, ce référentiel peut vivre derrière un service Web ou WCF; il peut être exposé à AJAX ou à d'autres appelants qui ne connaissent pas LINQ en premier lieu.

Galactic Cowboy
la source
4
Vous construirez donc une API de requête pour remplacer l'API de requête intégrée de .NET?
Michael Brown
4
Cela
7
@MikeBrown L'objet de cette question - et ma réponse - est que vous souhaitez exposer le comportement ou les avantages de IQueryable sans exposer le IQueryablelui - même . Comment allez-vous faire cela avec l'API intégrée?
GalacticCowboy
2
C'est une très bonne tautologie. Vous n'avez pas expliqué pourquoi vous voulez éviter cela. Heck WCF Data Services expose IQueryable sur le fil. Je n'essaie pas de vous prendre au dépourvu, je ne comprends tout simplement pas cette aversion pour IQueryable que les gens semblent avoir.
Michael Brown
2
Si vous souhaitez simplement utiliser IQueryable, pourquoi voudriez-vous même utiliser un référentiel? Les référentiels sont destinés à masquer les détails de la mise en œuvre. (En outre, WCF Data Services est un peu plus récent que la plupart des solutions sur lesquelles j'ai travaillé au cours des 4 dernières années qui utilisaient un référentiel comme celui-ci ... Il est donc peu probable qu'elles soient mises à jour de si tôt utilisation d'un nouveau jouet brillant.)
GalacticCowboy
8

Il n'y a vraiment qu'une seule réponse légitime: cela dépend de la manière dont le référentiel doit être utilisé.

À un extrême, votre référentiel est un très fin wrapper autour d'un DBContext afin que vous puissiez injecter un vernis de testabilité autour d'une application basée sur une base de données. Il n'y a vraiment aucune attente du monde à ce que cela puisse être utilisé de manière déconnectée sans un DB compatible avec LINQ, car votre application CRUD n'en aura jamais besoin. Alors pourquoi ne pas utiliser IQueryable? Je préférerais probablement IEnumarable car vous bénéficiez de la plupart des avantages [lisez: exécution différée] et vous ne vous sentez pas aussi sale.

À moins que vous ne soyez assis à cette extrémité, j'essaierais de tirer parti de l'esprit du modèle de référentiel et de renvoyer les collections matérialisées appropriées qui ne impliquent pas une connexion de base de données sous-jacente.

Wyatt Barnett
la source
6
En ce qui concerne le retour IEnumerable, il est important de noter que cela ((IEnumerable<T>)query).LastOrDefault()est très différent de query.LastOrDefault()(présumer queryest un IQueryable). Donc, il n’est logique de revenir que IEnumerablesi vous allez de toute façon parcourir tous les éléments.
Lou
7
 > Should Repositories return IQueryable?

NO si vous souhaitez effectuer un test / développement basé sur des tests car il n'existe pas de moyen facile de créer un référentiel factice pour tester votre businesslogic (= repository-consumer) indépendamment de la base de données ou d'un autre fournisseur IQueryable.

k3b
la source
6

D'un point de vue purement architectural, IQueryable est une abstraction qui fuit. En général, cela donne trop de puissance à l'utilisateur. Cela étant dit, il y a des endroits où cela a du sens. Si vous utilisez OData, IQueryable facilite extrêmement la fourniture d'un point de terminaison facile à filtrer, à trier, à grouper, etc. Je préfère créer des DTO sur lesquels mes entités sont mappées et IQueryable renvoie le DTO. De cette manière, je filtre mes données et utilise uniquement la méthode IQueryable pour permettre la personnalisation des résultats par le client.

Slick86
la source
6

J'ai fait face à un tel choix aussi.

Résumons donc les côtés positifs et négatifs:

Points positifs:

  • Souplesse. Il est définitivement flexible et pratique de permettre au côté client de créer des requêtes personnalisées avec une seule méthode de référentiel.

Points négatifs:

  • Untestable . (vous allez réellement tester l'implémentation sous-jacente d'IQueryable, qui correspond généralement à votre ORM. Mais vous devriez plutôt tester votre logique)
  • Brouille la responsabilité . Il est impossible de dire ce que cette méthode particulière de votre référentiel fait. En fait, c’est une méthode divine, qui peut retourner tout ce que vous voulez dans votre base de données
  • Dangereux . En l'absence de restrictions sur ce qui peut être interrogé par cette méthode de référentiel, dans certains cas, de telles implémentations peuvent être dangereuses du point de vue de la sécurité.
  • Problèmes de cache (ou, pour être générique, tout problème de pré ou post-traitement). Par exemple, il est généralement déconseillé de tout mettre en cache dans votre application. Vous allez essayer de mettre en cache quelque chose qui cause une charge importante à votre base de données. Mais comment faire cela, si vous n’avez qu’une seule méthode de référentiel, que les clients utilisent pour émettre pratiquement toute requête sur la base de données?
  • Problèmes de journalisation . En enregistrant quelque chose au niveau de la couche de référentiel, vous allez d'abord inspecter IQueryable pour vérifier si cet appel particulier est enregistré ou non.

Je recommande de ne pas utiliser cette approche. Pensez plutôt à créer plusieurs spécifications et à implémenter des méthodes de référentiel pouvant interroger la base de données à l'aide de celles-ci. Le modèle de spécification est un excellent moyen d'encapsuler un ensemble de conditions.

Vladislav Rastrusny
la source
5

Je pense qu'exposer l'IQueryable sur votre référentiel est parfaitement acceptable au cours des premières phases de développement. Certains outils d'interface utilisateur peuvent fonctionner directement avec IQueryable et gérer le tri, le filtrage, le regroupement, etc.

Cela étant dit, je pense que le simple fait d'exposer crud sur le référentiel et de l'appeler un jour est irresponsable. Disposer d'opérations utiles et d'assistants de requête pour les requêtes courantes vous permet de centraliser la logique d'une requête tout en exposant une surface d'interface plus petite aux utilisateurs du référentiel.

Pour les premières itérations d'un projet, exposer publiquement IQueryable sur le référentiel permet une croissance plus rapide. Avant la première version complète, je vous recommande de rendre l'opération de requête privée ou protégée et de regrouper les requêtes génériques sous un même toit.

Michael Brown
la source
4

Ce que j'ai vu faire (et ce que je m’implémente moi-même) est le suivant:

public interface IEntity<TKey>
{
    TKey Id { get; set; }
}

public interface IRepository<TEntity, in TKey> where TEntity : IEntity<TKey>
{
    void Create(TEntity entity);
    TEntity Get(TKey key);
    IEnumerable<TEntity> GetAll();
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> expression);
    void Update(TEntity entity);
    void Delete(TKey id);
}

Cela vous permet d’avoir la possibilité de faire une IQueryable.Where(a => a.SomeFilter)requête sur votre concreteRepo.GetAll(a => a.SomeFilter)référentiel sans exposer d’affaires LINQy.

dav_i
la source