Dynamische LINQ-Expressions

Heute kam ich an ein kleines Problem in Zusammenhang mit LINQ. Was, wenn man zur Entwurfszeit noch nicht weiß, welche Filter später an eine LINQ-Abfrage übergeben werden sollen? Die Lösung ist elegant und einfach.

Worum gehts?

Am besten erklärt sich mein Bedarf wohl an einem Beispiel. Man stelle sich vor, man will eine Anwendung bauen und es später Benutzern erlauben, Suchkriterien in einer Maske festzulegen. Wenn also beispielsweise eine Liste von Personen gezeigt werden soll, dann soll der Benutzer in der Lage sein, sich eine beliebige Anzahl von Kriterien zusammen zu klicken, die dann per LINQ auf die Daten abgefeuert werden sollen.

Abb. 1: Sample-UI
Abb. 1: Sample-UI

Zugegeben, das Beispiel ist etwas konstruiert, aber die Idee sollte klar werden. Nur, wenn die Checkbox an einem Suchfeld gesetzt ist, soll nach dem entsprechenden Wert gesucht werden. Mein Ziel ist es nun, eine möglichst generische und flexible Lösung für das Problem zu finden.

Lösung

Nach einigem Überlegen kam ich auf die Klasse Expression. Diese hat einige interessante Methoden und einige davon sind nichts anderes als Factories für LINQ-Elemente. Eines dieser Elemente ist eine Lambda-Expression. Das ist etwas, dass wir als C#-Entwickler bereits seit einiger Zeit verwenden. In meinem Beispiel oben wäre die statische Verwendung dessen, was ich vorhabe ca das:


var dataSource = people.Where(p => p.Lastname.StartsWith("S"));

Die Lambda-Expression ist das, was an die Where-Erweiterungsmethode übergeben wird, also genau das, was ich gern dynamisch hätte.

Um das ganze wiederverwendbar zu machen, kam ich auf folgende eigene Extension:

public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        if (first == null)
        {
            return second;
        }
        var secondInvoked = Expression.Invoke(second, first.Parameters);
        return Expression.Lambda<Func<T, bool>>(Expression.And(first.Body, secondInvoked), first.Parameters);
    }
}

Der Trick ist hier der, dass 2 Expressions per AND miteinander verknüpft werden können, sodass wiederum eine dritte Expression dabei heraus kommt.

In meinem Beispiel oben könnte man das nun wie folgt verwenden (zur Vereinfachung als Consolen-Anwendung):


class Program
{
    static void Main(string[] args)
    {
        var people = new List<Person>()
        {
            new Person()
            {
                Firstname = "Carl",
                Lastname = "Sample"
            },
            new Person()
            {
                Firstname = "Mark",
                Lastname = "Schulz"
            }
        };
        Expression<Func<Person, bool>> result = null;
        foreach (var ex in GetSearchExpressions("S"))
        {
            result = result.Or(ex);
        }
        if (result == null)
        {
            throw new InvalidOperationException("Search not possible!");
        }
        var filtered = people.Where(result.Compile());
        Console.WriteLine(filtered.Count());
        Console.ReadKey();
    }

    static IEnumerable<Expression<Func<Person, bool>>> GetSearchExpressions(string searchValue)
    {
        var queryFirstnames = false;
        var queryLastnames = true;
        var result = new List<Expression<Func<Person, bool>>>();
        if (queryFirstnames)
        {
            result.Add(p => p.Firstname.StartsWith(searchValue));
        }
        if (queryLastnames)
        {
            result.Add(p => p.Lastname.StartsWith(searchValue));
        }
        return result;
    }
}

static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        if (first == null)
        {
            return second;
        }
        var secondInvoked = Expression.Invoke(second, first.Parameters);
        return Expression.Lambda<Func<T, bool>>(Expression.And(first.Body, secondInvoked), first.Parameters);
    }
}

class Person
{
    public string Firstname { get; set; }

    public string Lastname { get; set; }

}

Ganz unten ist zunächst einmal die Definition einer Person (ohne Geb.-Datum). Darüber ist die Extension-Klasse, damit der Code gleich ein wenig wiederverwendbarer ist. Interessant ist eigentlich nur Zeile 19 und die von ihr aufgerufene Methode GetSearchExpressions. Letztere dient einfach nur dazu, um je nach eingestelltem UI (simuliert durch die beiden bool-Variablen) eine Liste von Teil-Expressions zur liefern. Die ForEach in Zeile 19 nutzt nun einfach unsere neue Extension-Methode, um die „große“ Expression zu bauen. Wenn das geklappt hat, benutzt Zeile 27 diese Expression (man achte auf das Compile()!!!), um diese nun per LINQ an die Where-Methode zu übergeben.

Fazit

Manchmal sind es gerade die kleinen Lösungen, die einem die meiste Freude bereiten. Ich kann das jedenfalls hervorragend in einem meiner aktuellen Projekte verwenden. Vielleicht hilfts halt auch anderen.

2 Antworten auf „Dynamische LINQ-Expressions“

    1. Hallo Mario! Danke für das Feedback. Deine Lösung ist natürlich dann empfehlenswerter, wenn man es direkt in einem Formular macht. Was aber, wenn man die Logik nur einmal hinterlegen und dann durcherben will? Dann wird es mit meinem Ansatz etwas leichter.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.