Förstå Expression Trees

När man arbetar med data som inte ligger i objekt i minnet (vanliga POCO-collections) som t.ex. SQL så har man tidigare behövt skriva en SQL-sats som gör det man vill, vilket kan vara att hämta data, att lägga till data, att ta bort data samt att uppdatera data i databasen. Med Linq to SQL och Linq to Entities, vilka kom i .NET 3.5 respektive .NET 3.5 SP 1 så har det blivit mycket enklare.

Med dessa så används ett nytt interface kallat IQueryable<T>. Detta implementerar IEnumerable, vilket vi har kunnat använda sedan länge. Skillnaden mellan IQueryable och IEnumerable är hur de behandlar datan. Anledningen till att Linq använder sig utav IQueryable är möjligheten till att använda en nyhet kallad Expression Trees, vilket jag kommer att gå igenom här.

För att göra det enklare att studera Expression Trees så kan jag rekommendera att du laddar ned en visualizer som följer med i CSharp Samples for Visual Studio 2008. Du kan ladda ned det här:

http://code.msdn.microsoft.com/csharpsamples

När exemplen är nedladdade och uppackade så öppnar projektet som finns under LinqSamples\ExpressionTreeVisualizer. Kompilera sedan ExpressionTreeVisualizer i release-läget och kopiera ExpressionTreeVisualizer.dll till Mina Dokument\Visual Studio 2008\Visualizers. När detta är gjort så skapar vi ett nytt projekt (jag använder ett Console-projekt) och skriver in detta:

Expression<Func<int, bool>> isTenTree = x => x == 10;

När vi nu kör programmet i debug-läge, så får vi möjlighet att se hur vårt Expression Tree ser ut:

1 - Visualizerknapp

När vi klickar på förstoringsglaset:

2 - Visualizer

Genom att sätta vår Func<> som en Expression<Func<>> så har vi gjort om den till ett Expression Tree. Som ni kan se i bilden ovan så är det en trädvy.

När vi skapar en Func<> så blir det en kompilerad funktion som vi kan avända oss utav direkt och köra, men när vi har en Expression<> så blir det en trädvy som måste kompileras.

Hade det här bara varit en Func<> så hade vi kunnat köra det här:

Func<int, bool> isTen = x => x == 10;
Console.WriteLine(isTen(10));

Och fått resultatet ”True” utskrivet på skärmen.

Kör vi istället med en Expression som inte är kompilerad så måste vi kompilera den för att kunna köra den. Vi kan alltså inte ta ett träd och och ställa frågor mot det direkt.

Expression<Func<int, bool>> isTenTree = x => x == 10;
Console.WriteLine(isTenTree.Compile()(10));

Det här ger samma resultat som koden innan. Genom att köra Compile()-metoden så kompileras vårt träd och vi kan köra det.

Varför ska man använda Expression Trees?

När man skall hämta data som inte finns i minnet på den aktuella servern så är det möjligt att vi måste skriva om koden. Det kan till exempel vara SQL som jag nämnde innan, men även till exempel Web Services (SOAP), SharePoint (CAML), JSON eller något annat. Genom att använda Expression Trees som inte är kompilerade så kan vi på ett enklare sätt läsa ut hur anropet skall se ut än om vi hade använt en kompilerad motsvarighet.

Skillnaden mellan Expression<Func<>> och Func<> är alltså att det förstnämnda är en datastruktur medan det andra är kompilerad kod.

Uppbyggnaden av Expression Trees

Expression<T> har fyra properties som vi kan använda oss utav för att kunna tolka hur datan skall hämtas.

Det är:

  • Body
  • Parameters
  • NodeType
  • Type

Om vi kikar på bilden ovan så kan vi se att vi i det exemplet har:

3 - VisualizerProperties

1. Body

Body är av typen ExpressionEqual, vilket beror på att vi har ”==” för jämförelsen. Hade vi istället haft ”>” så hade det varit GreaterThan.

2. Parameters

Vi skickar med en parameter kallad ”x” som är av typen Int32.

3. NodeType

Det vi har använt är en Lambda Expression.

4. Type

Typen är en Func<Int32, Boolean>, vilket stämmer överens med det vi skapade.

Vi kan alltså med den här informationen se att det vi ska ha är data där den inskickade parametern x har ett värde som är lika med 10, och om så är fallet så returnerar vi true.

Vi kan även använda mer avancerade exempel med fler parametrar.

Expression<Func<int, int, int>> isTenTree2 = (x, y) => x + y;
Console.WriteLine(isTenTree2.Compile()(10, 20));

I det här exemplet så tar vi emot två parametrar av typen Int32 och returnerar sedan summan av dem som en Int32.

Vårt Expression Tree ser nu ut på detta sätt:

4 - TwoParameters

Om vi ska tolka detta så kan vi se att:

1. Body

Body är av typen ExpressionAdd då vi summerar värdena i det.

2. Parameters

Vi har två parametrar, båda av typen Int32.

3. NodeType

NodeType är en Lambda Expression.

4. Type

Typen är en Func<Int32, Int32, Int32>, då vi skickar in två Int32 och sedan returnerar en tredje.

Skapa expressions programmatiskt

Istället för att direkt ange hur vår expression skall vara uppbyggd så kan vi istället skapa den från grunden själva.

För att skapa den senaste frågan ((x, y) => x + y) så kan vi skriva detta:

ParameterExpression paramX = Expression.Parameter(typeof(Int32), "x");
ParameterExpression paramY = Expression.Parameter(typeof(Int32), "y");
BinaryExpression body = Expression.Add(paramX, paramY);
 
ParameterExpression[] parameters = new ParameterExpression[] { paramX, paramY };
 
Expression<Func<Int32, Int32, Int32>> result = Expression.Lambda<Func<Int32, Int32, Int32>>(body, parameters);

Det vi gör här är att vi först skapar de två parametrarna, x och y. Sedan skapar vi en BinaryExpression med dessa två parametrar. För att de skall skickas in som parametrar i vårt Expression Tree så behöver vi även ange dem när vi till sist skapar själva frågan.

Om vi kompilerar och debuggar så kan vi se att vi har fått exakt samma Expression Tree som tidigare.

Vi kan även läsa av dessa propertys ur det skapade Expression Tree vi har. För enkelhetens skull så har jag skapat en metod för att skriva ut innehållet:

static void WriteExpression(Expression<Func<Int32, Int32, Int32>> exp)
{
    Console.Clear();
 
    Console.WriteLine("Body\n-----");
    Console.WriteLine("Body.NodeType: {0}", exp.Body.NodeType.ToString());
    Console.WriteLine("Body.NodeType: {0}", exp.Body.Type.ToString());
 
    Console.WriteLine();
 
    Console.WriteLine("NodeType\n-----");
    Console.WriteLine("NodeType: {0}", exp.NodeType.ToString());
 
    Console.WriteLine();
 
    Console.WriteLine("Parameters\n-----");
    foreach (ParameterExpression p in exp.Parameters)
    {
        Console.WriteLine("{0}", p.Name);
        Console.WriteLine("\tNodeType: {0}", p.NodeType.ToString());
        Console.WriteLine("\tType: {0}", p.Type.ToString());
    }
 
    Console.WriteLine();
 
    Console.WriteLine("Type\n-----");
    Console.WriteLine("Type: {0}", exp.Type.ToString());
}

Vi tar här emot en Expression<Func<Int32, Int32, Int32>> och skriver ut innehållet ur den.

När vi sedan anropar metoden med ”result” som parameter så får vi:

5 - ExpressionTreeProperties

Här har vi läst av alla parametrar och kan se att det är just det vi nyss skapade.

För att kunna exekvera vårt Expression Tree så måste det kompileras innan det kan köras. Då det är en Expression<Func<Int32, Int32, Int32>> när det är okompilerat så blir det en Func<Int32, Int32, Int32> när det är kompilerat.

Vi kan till exempel köra det så här:

Func<Int32, Int32, Int32> summera = result.Compile();
Console.WriteLine(summera(1, 2));

Resultatet blir här ”3”.

De två parametrarna vi har skickat in (1 och 2) är vad som är Left och Right i ett Expression Tree.

Om vi kör den här koden så får vi lätt fram den vänstra och högra sidan av ett Expression Tree:

BinaryExpression exbody = (BinaryExpression)result.Body;
ParameterExpression exleft = (ParameterExpression)body.Left;
ParameterExpression exright = (ParameterExpression)body.Right;
 
Console.WriteLine("Left: {0}", exleft.Name);
Console.WriteLine("Right: {0}", exright.Name);

Det här skrivet ut ”Left: x” och ”Right: y”, då det är de två parametrarna som används. Vi kan även få ut detta i Expression Tree Viewer på ett snyggt sätt:

6 - LeftRightParameters

Vi har alltså full insyn på hur vårt Expression Tree är uppbyggt.

Expression Trees != Lambda Expressions

Jag har visat upp lite exempel på Expression Trees och hur de används med Lambda Expressions, men de är inte samma sak. Vi kan använda Lambda Expressions för att skapa Expression Trees, eller så kan vi skapa dem manuellt som i det föregående exemplet. Alla Lambda Expressions kan dock ej förvandlas till Expression Trees.

Ett exempel på det är:

Action<string> PrintParameter = s => { Console.WriteLine("Parameter: {0}", s); };
Expression<Action<string>> PrintParameterTree = s => { Console.WriteLine("Parameter: {0}", s); };

Den förstnämnda kan vi skapa utan problem, men när vi försöker skapa ett Expression Tree av den så får vi det här felmeddelandet:

A lambda expression with a statement body cannot be converted to an expression tree

Det är alltså två olika funktioner, men som i vissa fall kan användas tillsammans.

Linq to Entities

I det här exemplet så har jag skapat upp en enkel databas med en tabell (Customers) som har tre fält (id, FirstName, LastName). Jag har sedan skapat upp en Entity-modell med den här tabellen.

Jag använder den här koden för att hämta ut alla personer där efternamnet slutar med ”son”:

Database1Entities ent = new Database1Entities();
var customers = ent.Customers.Where(c => c.LastName.EndsWith("son"));
 
foreach (var customer in customers)
{
    Console.WriteLine(customer.FirstName);
}

När vi nu debuggar så får vi fram:

7 - EntitiesDebug

Då Linq to Entities är uppbyggt med Expression Trees så kan vi få fram hur det är uppbyggt när frågan ställs. Klicka på förstoringsglaset för att få fram visualizern:

8 - EntitiesDebugVisualizer

Vi kan se här att det precis som tidigare är ett vanligt Expression Tree, vilket vi skulle kunna bygga dynamiskt om vi vill. Vi behöver alltså inte använda Linq-frågor, eller Lambda Expressions, utan vi kan själva generera det här trädet och på så vis ställa egna frågor.

Om vi ser vilken Type som används (längst ned i bilden) så ser vi att det är en IQueryable<Customers>, vilket gör det möjligt att skapa upp detta Expression Tree. Hade vi kört frågan mot vanliga POCO-objekt så hade vi istället använt en IEnumerable<Customers> då frågorna inte behöver skrivas om.

Då det är ett Expression Tree som lätt kan tolkas så gör det att Entity Framework lätt kan använda SQL Server-providern för att skriva om detta till en lämplig SQL-sats. För att gå ett steg längre i felsökningen så kan vi ladda ned en visualizer för det. Ni hittar en här:

http://visualstudiogallery.msdn.microsoft.com/en-us/99468ece-689b-481c-868c-19e00e0a4e69

Ladda ned dll-filen och lägg i Visualizers-mappen precis som tidigare och starta om Visual Studio. När vi sedan debuggar koden igen så får vi fram det här:

9 - EntityVisualizer

Och när vi klickar där:

10 - EntityVisualizerView

Här kan vi enkelt se att vårt Expression Tree har översatts till:

SELECT 
[Extent1].[id] AS [id], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName]
FROM [dbo].[Customers] AS [Extent1]
WHERE (RIGHT([Extent1].[LastName], CAST(LEN(N'son') AS int))) = N'son'

Det här är den stora fördelen med Expression Trees, det är alltså möjligt att enkelt skriva om dessa till något som kan tolkas av systemet det kopplas mot.

Nyhet i .NET 4.0 – Skapa dynamiska metoder

En nyhet som kommer i .NET 4.0 är att vi på ett smidigt (allt är relativt..) sätt kan skapa dynamiska metoder. Tidigare så har vi varit tvungna att använda System.Reflection.Emit för att göra detta, vilket gör att vi hamnar i ”OpCode hell”. Med .NET 4.0 så går det att lösa mycket smidigare.

För att köra det här så behöver du Visual Studio 2010 och .NET 4.0.

Till skillnad från .NET 3.5 där vi har Expression Trees och därmed kan skapa Expressions med kod, så har vi i .NET 4.0 även stöd för Statement Trees. Med dessa kan vi dynamiskt skapa metoder utan att behöva blanda in IL.

Metoden vi kommer att skapa nu motsvarar det här:

static int DynamicMethod(int parameter)
{
    int value = 0;
 
    do
    {
        if (value < 10)
            value += parameter;
        else
            break;
 
    } while (true);
 
    return value;
}

Vi kommer att skicka in en parameter med värdet ”3” till den här metoden. Sedan körs en loop till dess att if-satsen inte är giltig längre. Om vi kör den här koden kommer resultatet att bli ”12”.

Motsvarande med Statement Expressions är detta:

// Skapa en ParameterExpression som tillhandahåller det inskickade värdet
ParameterExpression parameter = Expression.Parameter(typeof(int), "parameter");
 
// Det lokala värdet som finns i bodyn i vår metod
ParameterExpression localvalue = Expression.Parameter(typeof(int), "localvalue");
 
// En label för att kunna gå ut ur loopen
LabelTarget target = Expression.Label(typeof(int));
 
// Den dynamiska metodens body
BlockExpression block = Expression.Block(
 
    // Skapar en lokal variabel i scopet
    new[] { localvalue },
 
    // Sätter värdet på localvalue till 0
    Expression.Assign(localvalue, Expression.Constant(0)),
 
    Expression.Loop(
        Expression.IfThenElse(
    // Testar om value < 10
            Expression.LessThan(localvalue, Expression.Constant(10, typeof(Int32))),
 
            // Om sant så lägg till det inskickade värdet
            Expression.AddAssign(localvalue, parameter),
 
            // Om falskt så gå ut ur loopen
            Expression.Break(target, localvalue)
        ),
    // Gå ut ur loopen genom att gå till labeln
        target
    )
);
 
// Spara ned ett expression tree
Expression<Func<int, int>> dynamicmethod = Expression.Lambda<Func<int, int>>(block, parameter);

Parametern som skapas i början är den som skickas in i metoden. Sedan skapar vi en lokal variabel som är den som vi i det tidigare exemplet kallar för ”value”. Efter det skapar vi en label, vilket motsvarar break;. Vi får använda denna label för att berätta att vi ska ut ur loopen när villkoret inte är giltigt längre, annars får vi en evighetsloop.

Efter det kommer något som är nytt i C# 4.0, nämligen en BlockExpression, vilket gör det möjligt att skapa kodblock med egen logik i. I det här exemplet så har vi i den själva loopen med en if-sats i.

Vi kan alltså med hjälp av Statement Expressions generera kod i C# som vi sedan kan kompilera med dynamicmethod.Compile() och efter det anropa den precis som tidigare.

Sammanfattning

Expressions är otroligt kraftfulla verktyg för att kunna generera kod, antingen för att kunna kompilera till något annat språk som SQL, eller till C# eller ett dynamiskt språk (DLR använder Statement Expressions väldigt mycket för att generera kod).

I C# 5.0 är det även sagt att det ska bli mer Compiler as a Service, så vi lär definitivt se mer av detta framöver.

8 Comments

Comments have been disabled for this content.