Verfasst von: Michael | 24/12/2010

LINQ: Versuche mit Expression Trees

LINQ (Language Integrated Query) ist eine coole Sache, keine Frage. Das Filtern von Collections wird zum Kinderspiel, Umformungen lassen sich wunderbar einfach ausdrücken und zusammen mit den LINQ-Providern für SQL-Datenbanken oder OR-Mappern wie NHibernate kann man für den Datenbank-Zugriff auf SQL weitestgehend verzichten.

Wenn man hinter die Kulissen von LINQ schaut, entdeckt man schnell Komponenten, mit denen aber noch deutlich fortgeschrittenere Szenarien möglich sind. Zum Beispiel rückt die Implementierung einer eigenen Abfragesprache (z. B. für DSLs) in greifbare Nähe.

Ganz von vorne …

Ich fang mal bei Null an. Nehmen wir an, wir haben folgendes Array mit Namen:

var names = new[] {
  "Axel", "Bernd", "Claus",
  "Dieter", "Erich", "Fritz"
};

Jetzt möchte ich rausfinden, welcher Name die Buchstaben “e” und “r” enthält:

var somenames = names.Where(n => n.Contains("e") && n.Contains("r"));

Der mit großem “E” geschriebene Erich fällt durch, daher bleiben Bernd und Dieter übrig. Für die weitere Untersuchung ziehe ich den Lambda-Ausdruck jetzt raus:

Func<string, bool> p = n => n.Contains("e") && n.Contains("r");
var somenames = names.Where(p);

Was auffällt: der Variable p muss explizit der Typ “Func<string, bool>” zugewiesen werden, ein “var p = …” funktioniert nicht. Das hat seinen Grund, ich komm später darauf zurück. Wenn ich die Variable p auf der Konsole ausgebe, kommt folgendes heraus:

System.Func`2[System.String,System.Boolean]

Also in der Tat ein Delegate vom Typ Func<string, bool>.

Aus Delegate wird Expression

Jetzt ändere ich den Variablentyp ab in

Expression<Func<string, bool>> p =
  n => n.Contains("e") && n.Contains("r");

Der Compiler meckert nicht, das ist eine legale Zuweisung. Was hier passiert: der Compiler merkt anhand des Variablentyps, dass der Lambda-Ausdruck nicht in ein Delegate umgewandelt wird, sondern statt dessen ein sog. Expression-Tree erzeugt wird. Aus diesem Grund muss der Variablen-Typ auch explizit angegeben werden, da der Compiler wissen muss, welche Umwandlung er machen soll.

Schauen wir uns den Expression-Tree mit Console.WriteLine an:

n => (n.Contains("e") AndAlso n.Contains("r"))

Es sieht fast genauso aus wie unser Lambda-Ausdruck, allerdings statt des && ein etwas gequältes “AndAlso” (“And” gibt es auch, dass drückt aber ein bitweises UND aus).

Aus IEnumerable wird IQueryable

Leider kommt es jetzt in der Where-Condition zu einem Fehler. Die Where-Bedingung für IEnumerable erwartet einen Delegate und keinen Expression-Tree. Um den Expression-Tree verwenden zu können, muss IEnumerable in IQueryable umgewandelt werden:

var somenames = names.AsQueryable().Where(p);

IQueryable ist das Interface, dass sog. LINQ-Provider implementieren müssen, die eigene Abfragesprachen besitzen. Z. B. produziert LINQ to SQL aus einem Expression-Tree eine SQL-Abfrage.

Expression-Tree im Eigenbau

Das Programm funktioniert wieder mit demselben Ergebnis. Aber was bringt mir das jetzt ?

Das Schöne an Expression-Trees ist, dass man sie nicht nur aus Lambda-Ausdrücken erzeugen kann, sondern recht einfach selber zusammenbauen kann. Ich will folgendes versuchen: anstatt die Suchbedingungen hart zu kodieren, möchte ich eine eigene kleine Abfragesprache definieren, mit der ich mein Namens-Array abfragen kann. Die soll z. B. so aussehen:

e;r;!D

Damit sollen alle Namen ausgegeben werden, die ein “e” und ein “r” enthalten, aber kein “D”.

Zuerst definiere ich dazu eine Methode ParseQuery, die aus dem Query-String ein Expression-Tree in der für IQueryable benötigten Bauweise macht:

static Expression<Func<string, bool>> ParseQuery(string s)

Das Zusammenbauen der Expression besteht aus einer Schachtelung (daher auch Expression-Tree) von Sub-Expressions, die die notwendigen Bestandteile enthalten. Zuerst benötige ich eine Expression, die die Prüfvariable definiert, also den Teil, der von “aussen” in die Query mitgegeben wird und der dann auf die verschiedenen Bedingungen geprüft wird.

var paramEx = Expression.Parameter(typeof(string), "p");

Die Klasse Expression bietet zahlreiche solcher Factory-Methoden für Expressions an, wir werden jetzt noch weitere verwenden. Jetzt nehmen wir den Query-String auseinander:

foreach (var part in s.Split(‚;‘))

Für jeden part müssen wir eine Expression erzeugen und alle entstandenen Expressions mit logischem UND verknüpfen.

Für die Parts ohne NOT-Operator genügt folgende Expressions:

partEx = Expression.Call(
  paramEx,
  typeof (string).GetMethod("Contains"),
  Expression.Constant(part)
);

Wie ist das zu verstehen ? Diese Expression entspricht einem Aufruf (“Call”) einer Methode, die einen booleschen Wert zurückliefert. Diese Methode ist die Methode “Contains” der Klasse string und wird auf unserer Prüfvariablen aufgerufen (paramEx). Als Parameter bekommt die Contains-Methode unseren Part als konstanten Ausdruck. Auf gut Deutsch entspricht diese Expression etwa folgender C#-Zeile:

paramEx.Contains(part)

Für die Unterstützung des !-Operators müssen wir einfach nur diese Expression wieder in eine Expression, der Not-Expression, verpacken.

partEx = Expression.Not(
  Expression.Call(
    paramEx,
    typeof (string).GetMethod("Contains"),
    Expression.Constant(part.Substring(1))
  )
);

Dabei müssen wir vom Part das erste Zeichen ! abschneiden. In der Schleife erzeugen wir jetzt nacheinander diese Expressions und verknüpfen sie mit logischem UND. Das ist sehr einfach:

andEx = Expression.AndAlso(leftEx, rightEx);

Die fertige Methode sieht so aus:

static Expression<Func<string, bool>> ParseQuery(string s)
{
  var paramEx = Expression.Parameter(typeof (string), "p");

  Expression completeEx = null;

  foreach (var part in s.Split(‚;‘))
  {
    Expression partEx;

    if(part.StartsWith("!"))
    {
      partEx = Expression.Not(
        Expression.Call(
          paramEx,
          typeof (string).GetMethod("Contains"),
          Expression.Constant(part.Substring(1))
        )
      );
    }
    else
    {
      partEx = Expression.Call(
        paramEx,
        typeof (string).GetMethod("Contains"),
        Expression.Constant(part)
      );
    }

    completeEx = completeEx == null
          ?
partEx
          : Expression.AndAlso(completeEx, partEx);
  }

}

Halt, eins fehlt noch: das Return-Statement. Dazu muss die fertige boolesche Expression noch in eine Lambda-Expression verpackt werden:

return Expression.Lambda<Func<string, bool>>(completeEx, paramEx);

So, fertig. Mal schauen, was die Methode für verschiedene Query-Ausdrücke zurückliefert.

e;r: p => (p.Contains("e") AndAlso p.Contains("r"))
e;r;!D: p => ((p.Contains("e") AndAlso p.Contains("r")) AndAlso Not(p.Contains("D")))

Sieht gut aus, jetzt kann ich das Resultat als Where-Bedingung verwenden.

var somenames = names.AsQueryable().Where(ParseQuery("e;r;!D"));
=> Bernd

var somenames = names.AsQueryable().Where(ParseQuery("!F;r;i"));
=> Dieter, Erich

Das war’s. Wenn man das Prinzip verstanden hat, ist es eigentlich ganz einfach. Mit Expression-Trees bekommt der Entwickler ein mächtiges Werkzeug in die Hand, um die ganze Power der LINQ-Infrastruktur ausnutzen zu können.

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

Kategorien

%d Bloggern gefällt das: