Publicerades 11 oktober 2019

Prestanda i EntityFramework – Expression och Func

Prestanda i EntityFramework - Expression och Func

Det är ofta ingen skillnad i syntax när ni kodar mot metoder som tar Expression<Func> eller Func som inparametrar. Lambdauttryck, som t.ex x => x.Value > 0, ser ofta identiska ut i båda fallen. Det här gör att ni enkelt kan förväxla den ena med den andra - något som drastiskt kan påverka prestandan negativt. Ofta ses detta när en OR-Mapper används, som t.ex. EntityFramework, där det via Repositoryklasser exponeras gränssnitt som accepterar lambdauttryck. Prestanda i EntityFramework är något vi ska se närmre på i denna blogg.

En jämförelse av Expression och Func

En Func<T> är vanligtvis en funktion som tar en inparameter och returnerar ett värde, den kan uttryckas som ett lambdauttryck men också som en funktionskropp (t.ex. x => { return x.Value > 0; }). Expression<Func<T>> å andra sidan är inte en funktion, det är metadata kring dess delegat – i vårat fall en Func<T>, detta är representerat som ett expression tree. Metadatat gör så att man kan inspektera och tolka betydelsen av lambdauttrycket snarare än att exekvera det. Jag skriver uttryckligen lambdauttryck eftersom en Expression<Func<T>> inte accepterar någon funktionskropp utan endast lambdas. Detta är fundamentalt i hur t.ex. EntityFramework översätter LINQ-frågor till SQL-frågor.

Exemplet nedan visar på varför ni bör arbeta med Expressions istället för rena Funcs i applikationer som nyttjar EntityFramework eller liknande.

Prestandajämförlse mellan Expression and Func

Tänk er en ASP.NET applikation som använder EntityFramework som använder följande modell.

Entity class

Låt oss säga att det finns 1 000 000 rader av denna entitet i en SQL-databas. Det finns också ett Repository som har två publika metoder – en som tar in en Func<T> och en som tar in en Expression<Func<T>>. Se exemplet nedan:

code example

När detta repository används så ser lambdauttrycket exakt likadant ut oavsett vilken metod som används, så ni kan se att det är lätt att förväxla de två.

repository.GetByExpression(x => x.Value > 0);
repository.GetByFunc(x => x.Value > 0);

Vad som faktiskt händer i funktionerna skiljer sig dock markant åt. Nedan följer de båda SQL-frågorna som genererats av EntityFramework för de båda metoderna.

Func<T>
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Value] AS [Value]
FROM [dbo].[Entities] AS [Extent1]

Exekveringstid: 10972 ms

Expression<Func<T>>
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Foo] AS [Foo],
[Extent1].[Value] AS [Value]
FROM [dbo].[Entities] AS [Extent1]
WHERE [Extent1].[Value] > 0

Exekveringstid: 2238 ms

Sammanfattning

Skillnaden i exekveringstid mellan de två funktionerna är markant, ungefär 11 sekunder för Func<T> mot ungefär 2,2 sekunder för Expression<T>. Detta beror, som sagt, på att EntityFramework kan dra nytta av metadatan från Expressions och på så sätt bygga upp en SQL-fråga med en korrekt WHERE-sats medans i fallet med Func<T> så kan en inspektion av filtret (lambdan) inte göras. EntityFramerwork har då inget annat val än att hämta alla rader och sedan exekvera filtret i minnet. Detta är alltså orsaken till den stora skillnaden i exekveringstid.

Som ni kan se så bör ni alltså föredra Expression<T> över Func<T> när ni arbetar med kod som drar nytta av metadata kring en funktion (t.ex. EntityFramework), annars kommer bl.a. prestandan bli lidande.

Andreas Hagsten, Infozone