Dynamisches Filtern und Sortieren mit Entity Framework

117130
Kristof Claes

Ich entwickle eine Anwendung mit ASP.NET MVC 3 und Entity Framework 4.1. In dieser Anwendung habe ich viele Seitenlisten. Benutzer können diese Listen filtern und sortieren.

Dies führt zu Code wie dem folgenden. Ich bin mit diesem Code nicht wirklich glücklich. Gibt es eine bessere Methode zum Filtern und Sortieren mit Entity Framework?

Einige von Ihnen schlagen vor, diesen Code in eine Service-Klasse und nicht in den Controller zu integrieren, aber das würde den hässlichen Code nur an eine andere Stelle verschieben. Anstelle von hässlichem Code in einem Controller würde ich bei einem Dienst hässlichen Code bekommen.

public UsersController : Controller
{
    private const int PageSize = 25;

    public ActionResult Index(int page = 1, string sort = "", UserSearchViewModel search)
    {
        // Get an IQueryable<UserListItem>
        var users = from user in context.Users
                    select new UserListItem
                    {
                        UserId = user.UserId,
                        Email = user.Email,
                        FirstName = user.FirstName,
                        LastName = user.LastName,
                        UsertypeId = user.UsertypeId,
                        UsertypeDescription = users.Usertype.Description,
                        UsertypeSortingOrder = users.Usertype.SortingOrder
                    };

        // Filter on fields when needed
        if (!String.IsNullOrWhiteSpace(search.Name)) users = users.Where(u => u.FirstName.Contains(search.Name) || u.LastName.Contains(search.Name));
        if (!String.IsNullOrWhiteSpace(search.Email)) users = users.Where(u => u.Email.Contains(search.Email));
        if (search.UsertypeId.HasValue) users = users.Where(u => u.UsertypeId == search.UsertypeId.Value);

        // Calculate the number of pages based on the filtering
        int filteredCount = users.Count();
        int totalPages = Convert.ToInt32(Math.Ceiling((decimal)filteredCount / (decimal)PageSize));

        // Sort the items
        switch(sort.ToLower())
        {
            default:
                users = users.OrderBy(u => u.FirstName).ThenBy(u => u.LastName);
                break;
            case "namedesc":
                users = users.OrderByDescending(u => u.FirstName).ThenByDescending(u => u.LastName);
                break;
            case "emailasc":
                users = users.OrderBy(u => u.Email);
                break;
            case "emaildesc":
                users = users.OrderByDescending(u => u.Email);
                break;
            case "typeasc":
                users = users.OrderBy(u => u.UsertypeSortingOrder);
                break;
            case "typedesc":
                users = users.OrderByDescending(u => u.UsertypeSortingOrder);
                break;
        }

        // Apply the paging
        users = users.Skip(PageSize * (page - 1)).Take(PageSize);

        var viewModel = new UsersIndexViewModel
                        {
                            Users = users.ToList(),
                            TotalPages = totalPages
                        };

        return View(viewModel);
    }
}
Antworten
33
Ich bin mir nicht sicher, was Ihre eigentliche Sorge ist. Der Code ist lesbar und zumindest ein erster Durchlauf sieht so aus, als sollte er wie erwartet funktionieren. Es ist leicht zu folgen. Manchmal erfordert die Logik diese Art von Komplexität, aber für mich sieht sie ziemlich sauber aus. Ich könnte die Abfrage als eine einzige Zeile schreiben, aber die Laufzeit wird effektiv gleich sein, aber es wäre viel schwieriger zu verstehen. Chad vor 9 Jahren 0
Ich hatte gehofft, dass es einen saubereren / kürzeren Weg gibt, die Filterung (die drei if-Anweisungen in diesem Fall) und die Sortierung (die switch-Anweisung) durchzuführen. In diesem Beispiel filtere und sortiere ich nur nach 3 Feldern, aber ich habe auch eine Liste, in der ich 6 oder mehr Felder benötige. Das führt schnell zu viel Code und sieht hässlich aus. Wie würdest du es in eine einzige Zeile schreiben und wo saugt meine Logik? :-) Kristof Claes vor 9 Jahren 0
Ich könnte die gesamte Zeile als eine einzige Codezeile schreiben. Das würde es nicht gerade noch kürzer machen. Code-Golf eignet sich gut für das Honen von Fähigkeiten, aber lesbaren und wartbaren Code ist viel wertvoller. Chad vor 9 Jahren 0
Und ich habe meinen ersten Kommentar bearbeitet. Deine Logik saugt nicht. Manchmal wird die Logik komplex, was nervt. Wie gesagt, ich wäre sehr glücklich, wenn mir dies zur Aufrechterhaltung übergeben würde. Im Gegensatz dazu würde eine einzige Befehlszeile viele Vergleiche enthalten, die die if-Anweisungen ersetzen. Die Endergebnisabfrage, die ausgeführt wird, ist wahrscheinlich gleich oder möglicherweise sogar schlechter. Es wäre jedoch viel schwieriger zu folgen. Chad vor 9 Jahren 0
Oh ok ich verstehe. Ich bin damit einverstanden, dass der Code einfach zu lesen und zu verstehen ist, aber es ist eine Art Drag _write_ :-) Ich hatte gehofft, in dieser Abteilung Verbesserungen zu erzielen, ohne die Lesbarkeit zu beeinträchtigen. Kristof Claes vor 9 Jahren 0

7 Antworten auf die Frage

21
Allon Guralnek

EDIT: Ich entschuldige mich bei meinem früheren Sample, das nicht ganz kompiliert wurde. Ich habe es behoben und ein vollständigeres Beispiel hinzugefügt.

Sie können jede Bedingung mit einer Strategie zum Ändern der Abfrage verknüpfen . Jede Strategie ( SearchFieldMutatorin diesem Beispiel genannt) enthält zwei Dinge:

  1. Eine Möglichkeit zu entscheiden, ob die Strategie angewendet werden soll.
  2. Die Strategie selbst.

Der erste Teil ist ein Delegat (vom Typ Predicate<TSearch>), der die Daten in (oder einem anderen Typ ) zurückgibt trueoder darauf falsebasiert UserSearchViewModel, da er nur einen generischen Typ definiert TSearch. Wenn es zurückkehrt true, wird die Strategie angewendet. Wenn es zurückkehrt false, wird es nicht angewendet. Dies ist der Typ des Delegierten:

Predicate<TSearch>

(hätte auch geschrieben werden können Func<TSearch, bool>)

Der zweite Teil ist die Strategie selbst. Es soll die Abfrage durch Anwenden eines LINQ-Operators "mutieren". In Wirklichkeit wird jedoch nur eine neue Abfrage mit dem hinzugefügten Operator zurückgegeben, und der Aufrufer sollte die alte Abfrage verwerfen und die neue behalten. Es ist also nicht wirklich eine Mutation, aber es hat den gleichen Effekt. Ich habe einen neuen Delegatentyp für ihn erstellt, sodass die Verwendung klar ist:

public delegate IQueryable<TItem> QueryMutator<TItem, TSearch>(IQueryable<TItem> items, TSearch search);

HINWEIS: I definiert habe sowohl den Elementtyp und die Suchdaten als generische Typen (wie TItemund TSearchjeweils), so ist dieser Code an mehreren Stellen im Code verwendet werden . Wenn dies jedoch verwirrend ist, können Sie die Generika vollständig entfernen und alle TItemdurch UserListItemund alle TSearchdurch ersetzen UserSearchViewModel.

Nachdem wir nun die zwei Arten der Strategie definiert haben, können wir eine Klasse erstellen, die beide enthält und auch die (simulierte) Mutation ausführt:

public class SearchFieldMutator<TItem, TSearch>
{
    public Predicate<TSearch> Condition { get; set; }
    public QueryMutator<TItem, TSearch> Mutator { get; set; }

    public SearchFieldMutator(Predicate<TSearch> condition, QueryMutator<TItem, TSearch> mutator)
    {
        Condition = condition;
        Mutator = mutator;
    }

    public IQueryable<TItem> Apply(TSearch search, IQueryable<TItem> query)
    {
        return Condition(search) ? Mutator(query, search) : query;
    }
}

Diese Klasse enthält sowohl die Bedingung als auch die Strategie selbst. Durch Verwendung der Apply()Methode können Sie sie problemlos auf eine Abfrage anwenden, wenn die Bedingung erfüllt ist.

Wir können jetzt eine Liste von Strategien erstellen. Wir definieren einen Platz für sie (in einer Ihrer Klassen), da sie nur einmal erstellt werden müssen (sie sind doch zustandslos):

List<SearchFieldMutator<UserListItem, UserSearchViewModel>> SearchFieldMutators { get; set; }

Wir füllen dann die Liste auf:

SearchFieldMutators = new List<SearchFieldMutator<UserListItem, UserSearchViewModel>>
{
    new SearchFieldMutator<UserListItem, UserSearchViewModel>(search => !string.IsNullOrWhiteSpace(search.Name), (users, search) => users.Where(u => u.FirstName.Contains(search.Name) || u.LastName.Contains(search.Name))),
    new SearchFieldMutator<UserListItem, UserSearchViewModel>(search => !string.IsNullOrWhiteSpace(search.Email), (users, search) => users.Where(u => u.Email.Contains(search.Email))),
    new SearchFieldMutator<UserListItem, UserSearchViewModel>(search => search.UsertypeId.HasValue, (users, search) => users.Where(u => u.UsertypeId == search.UsertypeId.Value)),
    new SearchFieldMutator<UserListItem, UserSearchViewModel>(search => search.CurrentSort.ToLower() == "namedesc", (users, search) => users.OrderByDescending(u => u.FirstName).ThenByDescending(u => u.LastName)),
    new SearchFieldMutator<UserListItem, UserSearchViewModel>(search => search.CurrentSort.ToLower() == "emailasc", (users, search) => users.OrderBy(u => u.Email)),
    // etc...
};

Wir können dann versuchen, es für eine Abfrage auszuführen. Anstelle einer eigentlichen Entity Framework-Abfrage verwende ich ein einfaches Array von UserListItems und füge ein .ToQueryable()on hinzu. Es funktioniert genauso, wenn Sie es durch eine tatsächliche Datenbankabfrage ersetzen. Ich werde aus Gründen des Beispiels auch eine einfache Suche erstellen:

// This is a mock EF query.
var usersQuery = new[]
{
    new UserListItem { FirstName = "Allon", LastName = "Guralnek", Email = null, UsertypeId = 7 },
    new UserListItem { FirstName = "Kristof", LastName = "Claes", Email = "whoknows@example.com", UsertypeId = null },
    new UserListItem { FirstName = "Tugboat", LastName = "Captain", Email = "tugboat@ahoy.yarr", UsertypeId = 12 },
    new UserListItem { FirstName = "kiev", LastName = null, Email = null, UsertypeId = 7 },
}.AsQueryable();

var searchModel = new UserSearchViewModel { UsertypeId = 7, CurrentSort = "NameDESC" };

Das Folgende erledigt eigentlich die ganze Arbeit, es ändert die Abfrage innerhalb der usersQueryVariablen in die von allen Suchstrategien angegebene:

foreach (var searchFieldMutator in SearchFieldMutators)
    usersQuery = searchFieldMutator.Apply(searchModel, usersQuery);

Das ist es! Dies ist das Ergebnis der Abfrage:

Ergebnis der Abfrage

Sie können es selbst ausführen. Hier ist eine LINQPad- Abfrage, mit der Sie herumspielen können :

http://share.linqpad.net/7bud7o.linq

Dies sollte die akzeptierte Antwort sein. Der veröffentlichte Code wird jedoch nicht kompiliert, was wahrscheinlich der Grund dafür ist, dass er nicht mehr Stimmen hat und aufgrund der beteiligten Generika wahrscheinlich für viele ein Hindernis ist. TugboatCaptain vor 5 Jahren 2
jemand diesen Ansatz erfolgreich umsetzen? Diese Linienbomben delegieren IQueryable QueryMutator(IQueryableArtikel, T1-Zustand); CS1960 C # Ungültiger Abweichungsmodifizierer. Als Variante können nur Schnittstellen- und Delegattyp-Parameter angegeben werden. ,> kiev vor 5 Jahren 0
@kiev: Ich habe den Code so korrigiert, dass er übereinstimmt (plus ein Arbeitsprobe). Ich habe auch eine ausführlichere Erklärung hinzugefügt. Allon Guralnek vor 5 Jahren 1
15
shuniar

Ich weiß, das ist alt, aber ich dachte, es könnte für jeden hilfreich sein, der dies liest. Wenn Sie den Code bereinigen möchten, können Sie ihn jederzeit umgestalten. So etwas ist lesbarer als das Original:

public UsersController : Controller
{
    private const int PageSize = 25;

    public ActionResult Index(int page = 1, string sort = "", UserSearchViewModel search)
    {
        var users = GetUsers(search, sort);
        var totalPages = GetTotalPages(users);

        var viewModel = new UsersIndexViewModel
        {
            Users = users.Skip(PageSize * (page - 1)).Take(PageSize).ToList(),
            TotalPages = totalPages
        };

        return View(viewModel);
    }

    private UserListItem GetUsers(UserSearchViewModel search, string sort)
    {
        var users = from user in context.Users
                    select new UserListItem
                    {
                        UserId = user.UserId,
                        Email = user.Email,
                        FirstName = user.FirstName,
                        LastName = user.LastName,
                        UsertypeId = user.UsertypeId,
                        UsertypeDescription = users.Usertype.Description,
                        UsertypeSortingOrder = users.Usertype.SortingOrder
                    };

        users = FilterUsers(users, search);
        users = SortUsers(users, sort);

        return users;
    }

    private UserListItem SortUsers(object users, string sort)
    {
        switch (sort.ToLower())
        {
            default:
                users = users.OrderBy(u => u.FirstName).ThenBy(u => u.LastName);
                break;
            case "namedesc":
                users = users.OrderByDescending(u => u.FirstName).ThenByDescending(u => u.LastName);
                break;
            case "emailasc":
                users = users.OrderBy(u => u.Email);
                break;
            case "emaildesc":
                users = users.OrderByDescending(u => u.Email);
                break;
            case "typeasc":
                users = users.OrderBy(u => u.UsertypeSortingOrder);
                break;
            case "typedesc":
                users = users.OrderByDescending(u => u.UsertypeSortingOrder);
                break;
        }
        return users;
    }

    private UserListItem FilterUsers(object users, UserSearchViewModel search)
    {
        if (!String.IsNullOrWhiteSpace(search.Name)) users = users.Where(u => u.FirstName.Contains(search.Name)
                                                                              || u.LastName.Contains(search.Name));
        if (!String.IsNullOrWhiteSpace(search.Email)) users = users.Where(u => u.Email.Contains(search.Email));
        if (search.UsertypeId.HasValue) users = users.Where(u => u.UsertypeId == search.UsertypeId.Value);
        return users;
    }

    private int GetTotalPages(UserListItem users)
    {
        var filteredCount = users.Count();
        return Convert.ToInt32(Math.Ceiling((decimal)filteredCount / (decimal)PageSize));
    }
}

Sie können dies dann weiter umgestalten, indem Sie diese Methoden in eine Serviceklasse verschieben, wenn Sie möchten.

6
Danil

Die Sortierfunktionalität kann in einer deklarativeren Syntax implementiert werden. Zuerst erklären Sie das assoziative Wörterbuch als privates Mitglied der Klasse.

    private Dictionary<string, Func<IQueryable<UserListItem>, IQueryable<UserListItem>>> _sortAssoc = new Dictionary<string, Func<IQueryable<UserListItem>, IQueryable<UserListItem>>>(StringComparer.OrdinalIgnoreCase)
    {
        { default(string),      users => users.OrderBy(u => u.FirstName).ThenBy(u => u.LastName)},
        { "namedesc",           users => users.OrderByDescending(u => u.FirstName).ThenByDescending(u => u.LastName)} ,
        { "emailasc",           users => users.OrderBy(u => u.Email) },
        { "emaildesc",          users => users.OrderByDescending(u => u.Email) },
        //...           
    };

Dann können Sie eine geeignete Sortiermethode auf folgende Weise aufrufen:

users = _sortAssoc.ContainsKey(sort) ? _sortAssoc[sort](users) : _sortAssoc[default(string)](users);
2
DAEMYO

Wenn Sie den ADO.NET Entity Framework Generator für EF 4.1 verwendet haben, können Sie Ihren Code wie folgt schreiben.

Der Weg ist, eine Sortierfolge zu konstruieren. "order by personname asc" wird wie folgt geschrieben "it.personname asc" - das " it " wird intern von EF verwendet.

string sortfield = "personname";
string sortdir= "asc";

IQueryable<vw_MyView> liststore= context.vw_MyView
                            .OrderBy("it." + sortfield  + " " + sortdir)
                            .Where(c => c.IsActive == true
                                && c.FundGroupId == fundGroupId
                                && c.Type == 1
                                );

Ich habe dies nur für den ADO.NET EF-Generator verwendet. DBcontext für EF 4.3.1 unterstützt diese Funktion nicht.

1
Eugene Wechsler

Ich würde überlegen, EntitySQL als Ersatz für LINQ to Entities zu verwenden. Mit EntitySQL würden Sie den Namen des Sortierfeldes mit einer Anweisung verketten. Obwohl es einen großen Nachteil gibt, keine Überprüfung der Kompilierzeit.

0
Mike Curtis

Ich habe auch eine Menge Code so geschrieben ...

Ich habe über "dynamisches LINQ" gelesen, aber das wird das Problem nicht ansprechen, wenn Sie orderby (). Thenby () benötigen. Das einzige, was ich anders gemacht habe, ist die Verwendung von Enums für Feldnamen und Sortierrichtungen, was in meinen WebForms-Apps mit ViewState hervorragend funktioniert, aber ich bin mir nicht sicher, wie ich den Sortierstatus in MVC beibehalten würde.

Wenn Ihre Tabelle groß wird, können Sie serverseitiges Paging in gespeicherten Prozeduren in Betracht ziehen, anstatt die gesamte Datenbank zurückzubringen und sie lokal zu sortieren. Dies ist jedoch ein anderes Thema :)

Die verzögerte Ausführung von Entity Framework stellt sicher, dass das Paging serverseitig erfolgt. Tatsächlich wird ein Aufruf der Datenbank nur dann ausgeführt, wenn ich 'users.ToList ()' mache. (Ein Aufruf erfolgt auch für `users.Count ()`) Kristof Claes vor 9 Jahren 4
0
Michael

Andere Option:

  • Erstellen Sie ein Domänenobjekt, das die für den Filter erforderlichen Parameter übernimmt
  • Erstellen Sie im Repository eine Funktion, die das Filterdomänenobjekt übernimmt, und innerhalb dieser Repository-Funktion das Filtern / Sortieren / Paging ausführen
  • Rufen Sie diese Funktion von Ihrer oberen Schicht (ob es der Business - Schicht / Service oder MVC - Controller), die in dem Filterdomänenobjekt

Vorteile:

  1. Wenn Sie den Filter jemals vergrößern möchten, müssen Sie nur das Domänenobjekt ' filter ' hinzufügen
  2. UND modifizieren Sie das Repository, um diesen neuen Filter zu verarbeiten
  3. Überall, wo Sie diese Daten erhalten möchten, muss der Anrufer nur den Filter auffüllen, der in dieser Situation benötigt wird

Wie auch immer, für das, was es wert ist, hat sich dies als der beste Weg erwiesen, um mit dieser Situation im Hinblick auf die Skalierbarkeit umzugehen und die Wahrscheinlichkeit einer Duplizierung von Code zu verringern.