Sollten Repositorys IQueryable zurückgeben?

66

Ich habe viele Projekte mit Repositorys gesehen, von denen Instanzen zurückgegeben werden IQueryable. Auf diese Weise können zusätzliche Filter und Sortierungen für den IQueryablevon anderen Code ausgeführt werden, wodurch andere SQL-Anweisungen generiert werden. Ich bin gespannt, woher dieses Muster stammt und ob es eine gute Idee ist.

Meine größte Sorge ist, dass IQueryablees ein Versprechen ist, die Datenbank einige Zeit später zu erreichen, wenn sie aufgelistet wird. Dies bedeutet, dass ein Fehler außerhalb des Repositorys ausgegeben wird. Dies kann bedeuten, dass eine Entity Framework-Ausnahme in einer anderen Ebene der Anwendung ausgelöst wird.

Ich bin in der Vergangenheit auch auf Probleme mit MARS (Multiple Active Result Sets) gestoßen (insbesondere bei der Verwendung von Transaktionen), und dieser Ansatz klingt so, als würde er dazu führen, dass dies häufiger vorkommt.

Ich habe immer AsEnumerableoder ToArrayam Ende jedes meiner LINQ-Ausdrücke aufgerufen , um sicherzustellen, dass die Datenbank gefunden wurde, bevor der Repository-Code verlassen wurde.

Ich frage mich, ob die Rückgabe IQueryableals Baustein für eine Datenschicht nützlich sein könnte. Ich habe einen ziemlich extravaganten Code mit einem Repository gesehen, das ein anderes Repository aufruft, um ein noch größeres zu erstellen IQueryable.

Travis Parks
quelle
Was wäre der Unterschied, wenn die Datenbank zum Zeitpunkt der verzögerten Abfrageausführung nicht verfügbar wäre, gegenüber dem Zeitpunkt der Abfrageerstellung? Der konsumierende Code müsste sich unabhängig davon mit dieser Situation auseinandersetzen.
Steven Evers
Interessante Frage, aber es wird wahrscheinlich schwer zu beantworten sein. Eine einfache Suche zeigt eine ziemlich hitzige Debatte über Ihre Frage: duckduckgo.com/?q=repository+return+iqueryable
Corbin
6
Es kommt darauf an, ob Sie Ihren Kunden erlauben möchten, Linq-Klauseln hinzuzufügen, die von einer verzögerten Ausführung profitieren könnten. Wenn sie einer zurückgestellten whereKlausel eine Klausel hinzufügen IQueryable, müssen Sie diese Daten nur drahtlos senden , nicht die gesamte Ergebnismenge.
Robert Harvey
Wenn Sie ein Repository-Muster verwenden - nein, sollten Sie IQueryable nicht zurückgeben. Hier ist ein schönes Warum .
Arnis Lapsa
1
@SteveEvers Es gibt einen großen Unterschied. Wenn eine Ausnahme in meinem Repository ausgelöst wird, kann ich sie mit einem für die Datenebene spezifischen Ausnahmetyp umbrechen. Wenn es später passiert, muss ich dort den ursprünglichen Ausnahmetyp erfassen. Je näher ich an der Quelle der Ausnahme bin, desto wahrscheinlicher weiß ich, was sie verursacht hat. Dies ist wichtig, um aussagekräftige Fehlermeldungen zu erstellen. Meiner Meinung nach verstößt eine EF-spezifische Ausnahme gegen die Kapselung, wenn sie mein Repository verlässt.
Travis Parks

Antworten:

47

Die Rückgabe von IQueryable bietet den Konsumenten des Repositorys definitiv mehr Flexibilität. Es überträgt die Verantwortung für die Einschränkung der Ergebnisse auf den Kunden, was natürlich sowohl ein Vorteil als auch eine Krücke sein kann.

Auf der anderen Seite müssen Sie nicht viele Repository-Methoden erstellen (zumindest auf dieser Ebene) - GetAllActiveItems, GetAllNonActiveItems usw. -, um die gewünschten Daten zu erhalten. Je nach Ihren Vorlieben kann dies wiederum gut oder schlecht sein. Sie werden (/ sollten) Verhaltensverträge definieren müssen, an die sich Ihre Implementierungen halten, aber wohin das führt, liegt bei Ihnen.

Sie können also die grobkörnige Abruflogik außerhalb des Repositorys platzieren und sie verwenden, wie der Benutzer dies wünscht. So Belichtungs IQueryable gibt die größtmögliche Flexibilität und ermöglicht eine effiziente Abfrage im Gegensatz zu In-Memory - Filterung, usw., und könnte die Notwendigkeit zur Herstellung einer Tonne spezifischen Daten - Abrufverfahren reduzieren.

Andererseits haben Sie Ihren Benutzern jetzt eine Schrotflinte gegeben. Sie können Dinge tun, die Sie möglicherweise nicht beabsichtigt haben (übermäßige Verwendung von .include (), Durchführung schwerer schwerer Abfragen und Durchführung von In-Memory- Filtern in ihren jeweiligen Implementierungen usw.) voller Zugriff.

Abhängig vom Team, der Erfahrung, der Größe der App, der Gesamtschichtung und der Architektur, kommt es also darauf an:

Hanzolo
quelle
Beachten Sie, dass Sie, wenn Sie daran arbeiten möchten, IQueryable zurückgeben können, das wiederum die Datenbank aufruft, aber Layering- und Verhaltenssteuerelemente hinzufügt.
PSR
1
@psr Kannst du erklären, was du damit meinst?
Travis Parks
1
@TravisParks - IQueryable muss nicht von der Datenbank implementiert werden, Sie können IQueryable-Klassen selbst schreiben - es ist einfach ziemlich schwierig. Sie können also einen IQueryable-Wrapper um eine Datenbank IQueryable schreiben und Ihr IQueryable-Limit auf den vollständigen Zugriff auf die zugrunde liegende Datenbank beschränken. Aber das ist schwer genug, dass ich nicht erwarten würde, dass es weit verbreitet ist.
PSR
2
Beachten Sie, dass Sie auch dann, wenn Sie IQueryables nicht zurückgeben, verhindern können, dass viele Methoden (GetAllActiveItems, GetAllNonActiveItems usw.) erstellt werden müssen, indem Sie einfach ein Expression<Func<Foo,bool>>an Ihre Methode übergeben. Diese Parameter können direkt an die WhereMethode übergeben werden, sodass der Konsument seinen Filter auswählen kann, ohne direkten Zugriff auf IQueryable <Foo> zu benötigen.
Flater
1
@hanzolo: Kommt darauf an, was du mit Refactoring meinst. Es ist wahr, dass das Ändern des Designs (z. B. Hinzufügen neuer Felder oder Ändern der Beziehungen) bei einer geschichteten und lose gekoppelten Codebasis eher zu Problemen führt. Aber Refactoring ist weniger schmerzhaft, wenn Sie nur eine Schicht wechseln müssen. Es hängt alles davon ab, ob Sie die Anwendung neu entwerfen möchten oder ob Sie einfach die Implementierung derselben Basisarchitektur überarbeiten. Ich würde in beiden Fällen immer noch eine lose Kopplung befürworten, aber Sie irren sich nicht, dass dies das Refactoring bei der Änderung von Kerndomänenkonzepten verstärkt.
Flater
29

Es ist keine gute Praxis, IQueryable öffentlichen Schnittstellen auszusetzen. Der Grund ist grundlegend: Sie können IQueryable nicht so realisieren, wie es angeblich ist.

Öffentliche Schnittstelle ist ein Vertrag zwischen Anbieter und Kunden. In den meisten Fällen wird eine vollständige Implementierung erwartet. IQueryable ist ein Cheat:

  1. IQueryable ist nahezu unmöglich zu implementieren. Und derzeit gibt es keine richtige Lösung. Sogar Entity Framework weist viele nicht unterstützte Ausnahmen auf .
  2. Abfragbare Anbieter sind heute stark von der Infrastruktur abhängig. Sharepoint verfügt über eine eigene teilweise Implementierung, und My SQL Provider verfügt über eine separate teilweise Implementierung.

Das Repository-Muster gibt uns eine klare Darstellung durch Schichtung und vor allem Unabhängigkeit bei der Realisierung. So können wir in Zukunft die Datenquelle ändern.

Die Frage lautet " Sollte die Möglichkeit einer Datenquellensubstitution bestehen? ".

Wenn ein Teil der Daten morgen in SAP-Services, NoSQL-Datenbanken oder einfach in Textdateien verschoben werden kann, können wir eine ordnungsgemäße Implementierung der IQueryable-Schnittstelle gewährleisten?

( mehr gute Punkte, warum dies eine schlechte Praxis ist)

Artru
quelle
Diese Antwort steht nicht für sich allein, ohne den flüchtigen externen Link zu konsultieren. Fassen Sie mindestens die wichtigsten Punkte der externen Ressource zusammen und versuchen Sie, Ihre persönliche Meinung aus Ihrer Antwort herauszuhalten ("schlechteste Idee, die ich gehört habe"). Ziehen Sie auch in Betracht, das Code-Snippet zu entfernen, das nichts mit IQueryable zu tun hat.
Lars Viklund
1
Vielen Dank @ LarsViklund, die Antwort ist mit Beispielen und Problembeschreibung aktualisiert
Artru
19

Realistisch gesehen haben Sie drei Alternativen, wenn Sie eine verzögerte Ausführung wünschen:

  • Tun Sie es auf diese Weise - belichten Sie eine IQueryable.
  • Implementieren Sie eine Struktur, die bestimmte Methoden für bestimmte Filter oder "Fragen" verfügbar macht. ( GetCustomersInAlaskaWithChildrenunten)
  • Implementieren Sie eine Struktur, die eine stark typisierte Filter- / Sortier- / Seiten-API verfügbar macht und die IQueryableintern erstellt.

Ich bevorzuge den dritten (obwohl ich Kollegen auch bei der Implementierung des zweiten geholfen habe), aber es sind offensichtlich einige Einstellungen und Wartungsarbeiten erforderlich. (T4 zur Rettung!)

Bearbeiten : Um die Verwirrung um das, wovon ich spreche, zu beseitigen , betrachten Sie das folgende Beispiel, das IQueryable gestohlen wurde. Kann Ihren Hund töten, Ihre Frau stehlen, Ihren Lebenswillen töten usw.

In dem Szenario, in dem Sie so etwas aussetzen würden:

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

Mit der API, über die ich spreche, können Sie eine Methode bereitstellen, mit der Sie GetCustomersInAlaskaWithChildren(oder eine andere Kombination von Kriterien) flexibel ausdrücken können , und das Repo führt sie als IQueryable aus und gibt die Ergebnisse an Sie zurück. Wichtig ist jedoch, dass es in der Repository-Ebene ausgeführt wird, sodass die verzögerte Ausführung weiterhin genutzt wird. Sobald Sie die Ergebnisse zurückerhalten haben, können Sie nach Herzenslust noch LINQ-to-Objekte darauf erstellen.

Ein weiterer Vorteil eines solchen Ansatzes ist, dass dieses Repository, da es sich um POCO-Klassen handelt, sich hinter einem Web- oder WCF-Dienst befinden kann. Es kann AJAX oder anderen Anrufern ausgesetzt sein, die sich mit LINQ nicht auskennen.

GalacticCowboy
quelle
4
Sie möchten also eine Abfrage-API erstellen, die die in .NET integrierte Abfrage-API ersetzt?
Michael Brown
4
Klingt für mich ziemlich sinnlos
ozz
7
@MikeBrown Der Sinn dieser Frage - und meine Antwort - ist , dass Sie das machen möchten Verhalten oder die Vorteile der IQueryable ohne Freilegung der IQueryableselbst . Wie machen Sie das mit der eingebauten API?
GalacticCowboy
2
Das ist eine sehr gute Tautologie. Sie haben nicht erklärt, warum Sie das vermeiden wollen. Heck WCF Data Services macht IQueryable drahtlos verfügbar. Ich versuche nicht, Sie zu finden, ich verstehe nur nicht, welche Abneigung IQueryable gegenüber Menschen zu haben scheint.
Michael Brown
2
Wenn Sie nur ein IQueryable verwenden, warum sollten Sie dann überhaupt ein Repository verwenden? Repositorys sollen Implementierungsdetails verbergen. (Außerdem ist WCF Data Services ein bisschen neuer als die meisten Lösungen, an denen ich in den letzten 4 Jahren gearbeitet habe und die ein Repository wie dieses verwendeten. Es ist also unwahrscheinlich, dass sie in Kürze aktualisiert werden, um nur eine Lösung zu finden.) Verwendung eines glänzenden neuen Spielzeugs.)
GalacticCowboy
8

Es gibt wirklich nur eine legitime Antwort: Es hängt davon ab, wie das Repository verwendet werden soll.

In einem Extremfall ist Ihr Repository ein sehr dünner Wrapper um einen DBContext, sodass Sie eine datenbankgesteuerte App mit einem Testfurnier versehen können. Es gibt wirklich keine reale Erwartung, dass dies getrennt verwendet wird, ohne dass eine LINQ-freundliche Datenbank dahinter steckt, denn Ihre CRUD-App wird das nie brauchen. Dann nutzen Sie doch IQueryable. Ich würde wahrscheinlich IEnumarable vorziehen, da Sie die meisten Vorteile erhalten [sprich: verzögerte Ausführung] und es sich nicht so schmutzig anfühlt.

Wenn Sie nicht so extrem sind, würde ich mich bemühen, den Geist des Repository-Musters auszunutzen und geeignete materialisierte Sammlungen zurückzugeben, die keine Auswirkungen auf eine zugrunde liegende Datenbankverbindung haben.

Wyatt Barnett
quelle
6
In Bezug auf die Rückkehr IEnumerableist es wichtig zu beachten, dass dies ((IEnumerable<T>)query).LastOrDefault()sehr unterschiedlich ist query.LastOrDefault()(vorausgesetzt, es queryist ein IQueryable). Es ist also nur sinnvoll, zurückzukehren, IEnumerablewenn Sie trotzdem alle Elemente durchlaufen.
Lou
7
 > Should Repositories return IQueryable?

NEIN, wenn Sie testgesteuerte Entwicklung / Unittesting durchführen möchten, da es keine einfache Möglichkeit gibt, ein Schein-Repository zu erstellen, um Ihre Geschäftslogik (= Repository-Consumer) isoliert von der Datenbank oder einem anderen IQueryable-Anbieter zu testen.

k3b
quelle
6

Aus rein architektonischer Sicht ist IQueryable eine undichte Abstraktion. Im Allgemeinen bietet es dem Benutzer zu viel Leistung. Davon abgesehen gibt es Orte, an denen es Sinn macht. Wenn Sie OData verwenden, ist es mit IQueryable sehr einfach, einen Endpunkt bereitzustellen, der leicht zu filtern, zu sortieren, zu gruppieren ist usw. Eigentlich ziehe ich es vor, DTOs zu erstellen, denen meine Entitäten zugeordnet sind, und IQueryable gibt stattdessen den DTO zurück. Auf diese Weise filtere ich meine Daten vor und verwende wirklich nur die IQueryable-Methode, um die Client-Anpassung der Ergebnisse zu ermöglichen.

Slick86
quelle
6

Ich habe auch vor einer solchen Wahl gestanden.

Fassen wir also die positiven und negativen Seiten zusammen:

Positives:

  • Flexibilität. Es ist auf jeden Fall flexibel und bequem, es dem Client zu ermöglichen, benutzerdefinierte Abfragen mit nur einer Repository-Methode zu erstellen

Negative:

  • Untestable . (Sie testen tatsächlich die zugrunde liegende IQueryable-Implementierung, in der Regel Ihr ORM. Sie sollten jedoch stattdessen Ihre Logik testen.)
  • Verwischt die Verantwortung . Es ist unmöglich zu sagen, was diese bestimmte Methode Ihres Repositorys tut. Tatsächlich ist es eine Gottmethode, die alles, was Sie wollen, in Ihrer gesamten Datenbank zurückgeben kann
  • Unsicher . In einigen Fällen können solche Implementierungen aus Sicherheitsgründen unsicher sein, ohne dass Einschränkungen hinsichtlich der Abfragbarkeit dieser Repository-Methode bestehen.
  • Caching-Probleme (oder generische Probleme vor oder nach der Verarbeitung). Es ist im Allgemeinen nicht ratsam, beispielsweise alles in Ihrer Anwendung zwischenzuspeichern. Sie werden versuchen, etwas zwischenzuspeichern, was zu einer erheblichen Auslastung Ihrer Datenbank führt. Aber wie geht das, wenn Sie nur über eine Repository-Methode verfügen, mit der Clients praktisch jede Abfrage für die Datenbank ausführen?
  • Protokollierungsprobleme . Wenn Sie etwas auf der Repository-Ebene protokollieren, überprüfen Sie IQueryable zuerst, ob dieser bestimmte Aufruf protokolliert wurde oder nicht.

Ich würde gegen diesen Ansatz empfehlen. Erstellen Sie stattdessen mehrere Spezifikationen und implementieren Sie Repository-Methoden, mit denen die Datenbank abgefragt werden kann. Das Spezifikationsmuster ist eine hervorragende Möglichkeit, eine Reihe von Bedingungen zu kapseln.

Vladislav Rastrusny
quelle
5

Ich denke, dass es in den Anfangsphasen der Entwicklung durchaus akzeptabel ist, das IQueryable in Ihrem Repository verfügbar zu machen. Es gibt UI-Tools, die direkt mit IQueryable arbeiten und das Sortieren, Filtern, Gruppieren usw. übernehmen können.

Davon abgesehen denke ich, dass es unverantwortlich ist, nur das Rohöl im Endlager offenzulegen und es als Tag zu bezeichnen. Wenn Sie nützliche Operationen und Abfragehilfen für allgemeine Abfragen haben, können Sie die Logik für eine Abfrage zentralisieren und gleichzeitig den Konsumenten des Repository eine kleinere Schnittstellenoberfläche zur Verfügung stellen.

In den ersten Iterationen eines Projekts kann IQueryable schneller im Repository verfügbar gemacht werden. Vor der ersten vollständigen Version würde ich empfehlen, die Abfrageoperation privat oder geschützt zu machen und die wilden Abfragen unter einem Dach zusammenzufassen.

Michael Brown
quelle
4

Was ich gesehen habe (und was ich selbst implementiere) ist Folgendes:

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

Auf diese Weise haben Sie die Flexibilität, eine IQueryable.Where(a => a.SomeFilter)Abfrage in Ihrem Repo concreteRepo.GetAll(a => a.SomeFilter)durchzuführen , ohne ein LINQy-Geschäft zu gefährden.

dav_i
quelle