IQueryable und LINQ erweitern mit Expression Trees

10 Januar 2018
Ingo Baasch
IQueryable und LINQ mit eigenen Funktionen erweitern und mit Expression Trees dynamisieren

Expression Trees? Wozu? Und wie? Ein einfaches Beispiel aus der Praxis.

Mit der Umstellung einer Legacy Anwendung auf das Entity-Framework begann für uns die nähere Auseinandersetzung mit den Möglichkeiten der Expression Trees. Die Legacy Anwendung verwendete NHibernate als ORM und machte exzessiven Gebrauch von dynamisch zusammengebauten SQL-Strings. Zusätzlich kann in dieser Anwendung der Aufbau einer Listenansicht konfiguriert und in der Datenbank gespeichert werden. Das bedeutet also, die Anzahl, Auswahl und Reihenfolge der angezeigten Spalten ist dynamisch. Auf jede dieser Spalten kann sortiert und gefiltert werden. In der Datenbank sind die Namen dieser Spalten als Strings gespeichert. In einigen Fällen auch als Pfad über mehrere Tabellen hinweg ( Z. B. "User.Account.Name" ). Da man IQueryable nur typisiert verwenden kann, würde man ohne die Möglichkeit einer Dynamisierung in diesem Szenario schnell in einer endlosen Auflistung von IF-ELSE Ketten enden, die eigentlich niemand haben will. Außerdem würde der generische Ansatz verloren gehen. So entsteht schnell der Wunsch, solch einen Pfad als String an LINQ übergeben zu können. Und genau hier kommen die Expression Trees ins Spiel. Realisiert haben wir damit eine dynamische Filterung, Projektion und Sortierung ( IQueryable.Where(),  IQueryable.Select(),  IQueryable.OrderBy() ).


Als Beispiel soll hier eine einfache Implementierung zur dynamischen Sortierung vorgestellt werden

Um das Beispiel einfach zu halten, wurde darauf verzichtet, die Funktionalität mit einzubinden, die es ermöglicht einen Pfad auflösen ( Z. B. "User.Account.Name" ). Der Code funktioniert nur mit Properties, die direkt verfügbar sind. Die Methode AddOrderBy ( siehe Zeile 10 ) ist als Erweiterungsmethode von IQueryable ausgelegt. Dadurch ist es möglich, sie wie eine gewöhnliche LINQ-Methode aufzurufen und als Fluent Interface zu verwenden.

40  IQueryable<User> queryUser = DBContext.Users;
41  IQueryable<User> querySortedUser =
42      queryUser
43      .Where(entity => entity.Age > 30)
44      .AddOrderBy<User, int>("Age", true);

Hier im Beispielaufruf wird also queryUser zunächst gefiltert und anschließend in Zeile 44 mit unserer Erweiterungsmethode AddOrderBy sortiert. Sie nimmt zwei Parameter entgegen. Den Namen der Property, nach der sortiert werden soll ( "Age" ) und die Richtung der Sortierung als bool. Der Rückgabewert ist wiederum ein IQueryable<User>. Die Variablen queryUser und querySortedUser sind also beide vom gleichen Typ.  Die beiden generischen Parameter T und P müssen angegeben werden. Den Parameter T ( User ) kann der Compiler ableiten, nicht aber den Parameter P ( int ), der den Datentyp von "Age" angibt. Da der Compiler darauf besteht in Zeile 15 über den Rückgabetyp P informiert zu werden, kann diese Information nicht erst zur Laufzeit bereitgestellt werden. Wir haben dieses Problem gelöst, indem die Methode AddOrderBy nicht direkt verwendet wird, sondern von einer weiteren Methode gekapselt wird, in der eine Fallunterscheidung für die neun vorkommenden Datentypen vorgenommen wird.

Implementierung mit Expression Trees

Um die Struktur von Expression Trees besser zu verstehen, kann es hilfreich sein sie vom Ende aus zu betrachten.

  • Beginnen wir in Zeile 19 bzw. Zeile 23:
    return query.OrderBy() ist ganz normales LINQ. Als Parameter für OrderBy() würden wir normalerweise den Lambda-Ausdruck entity => entity.Age hinschreiben. Diesen Lambda-Ausdruck müssen wir als Expression nachbauen. Dies geschieht in
  • Zeile 15:
    Expression.Lambda() erzeugt eine Lambda-Expression und nimmt als Parameter zwei Expressions entgegen. Zum einen die ParameterExpression ( dies entspricht entity => ) und zum anderen die MemberExpression ( dies entspricht entity.Age ). Die Rückgabe entspricht entity => entity.Age.
  • Zeile 13:
    Expression.Property() erzeugt eine MemberExpression und nimmt als Parameter zum einen eine ParameterExpression entgegen ( dies entspricht entity ) und zum anderen den Namen des Members auf den zugegriffen werden soll ( dies entspricht .Age ). Die Rückgabe entspricht entity.Age.
  • Zeile 12:
    Expression.Parameter() erzeugt eine ParameterExpression und nimmt als Parameter zum einen den Typ des Parameters entgegen, ( der später als Parameter des Lambda-Ausdrucks verwendet wird ) und zum anderen den Namen, den dieser Parameter später im Lambda-Ausdruck haben soll ( dies ist frei wählbar ). Die Rückgabe entspricht entity.
 1  namespace application.services.persistence
 2  {
 3      using System;
 4      using System.Linq;
 5      using System.Linq.Expressions;
 6  
 7  
 8     public static class IQueryableExtention
 9     {
10         public static IQueryable<T> AddOrderBy<T, P>(this IQueryable<T> query, string orderByProperty, bool orderByDescending)
11         {
12              ParameterExpression entity = Expression.Parameter(typeof(T), "entity");
13              MemberExpression property = Expression.Property(entity, orderByProperty);
14  
15              Expression<Func<T, P>> orderByExpression = Expression.Lambda<Func<T, P>>(property, entity);
16  
17              if (orderByDescending == true)
18              {
19                  return query.OrderByDescending(orderByExpression);
20              }
21              else
22              {
23                  return query.OrderBy(orderByExpression);
24              }
25          }
26      }
27  }

Es ist übrigens sehr hilfreich den Aufbau eines Expression Trees einmal mit dem Debugger zu verfolgen. Die einzelnen Expressions werden dabei sowohl in der gewohnten Form eines Lambda-Ausdrucks dargestellt, als auch in ihrer eigentlichen Form, "code as data".

<div>

    <HTML-Fragment>

        ∙∙∙

    </HTML-Fragment>

    <HTML-Fragment>

        ∙∙∙

    </HTML-Fragment>

</div>

40  var querySortedUser =