Av: Christer Nyberg

2021-02-12

Entity Framework & LINQ providers

Under mitt nuvarande uppdrag kom det upp en situation där jag hjälpte några av mina kollegor med ett problem de suttit med. Problemet manifesterade sig i ett märkligt felmeddelande från Entity Framework som de inte förstod. Ingen skam i det, då felmeddelandena från Entity Framework är ofta väldigt luddigt skrivna, speciellt så i det här fallet. I och med det här blogginlägget tänkte jag göra en lite djupare dykning i vad de stött på, och samtidigt beskriva grunderna i hur Entity Framework och andra LINQ providers fungerar.

Problemet

Koden som strulade såg ut så här:

var customers = new List<Customer>();
customers.Add(new Customer { Id = 1, Name = "Some Customer" });
var orderWithCustomerNames = databaseContext
.Orders
.Select(x => new {
x.OrderId,
CustomerName = customers.Where(y => y.Id == x.CustomerId).First().Name})
.ToList();

Order och kunder var egentligen något annat, men det är inte relevant för problemställningen. Den här koden kastar ett exception i runtime, och felmeddelandet från det är: "When called from 'VisitLambda', rewriting a node of type 'System.Linq.Expressions.ParameterExpression' must return a non-null value of the same type. Alternatively, override 'VisitLambda' and change it to not visit children of this type.". Det här meddelandet är kanske tekniskt korrekt, men otroligt intetsägande om man inte vet exakt hur Entity Framework är implementerat.

Så vad är det koden försöker göra? Vi försöker ställa en databasfråga mot tabellen Orders och vi vill att EF översätter det vi skrivit i argumentet till Select till SQL åt oss, så om vi försöker göra det själva så kan vi se vad felet är. Vi anropar Select() från Orders så vi vet att vår SQL ska se ut som:

SELECT ??? FROM Orders

Vi väljer ut OrderId:

SELECT OrderId, ??? FROM Orders

Nu blir det svårare, vi vill välja ut en kunds namn och kalla det för CustomerName:

SELECT OrderId, ??? AS CustomerName FROM Orders

Och det vi vill göra är att ta den första kunden som har ett matchande ID och plocka ut dess namn:

SELECT OrderId, (SELECT TOP 1 Name FROM ??? WHERE Id = Orders.CustomerId) FROM Orders

Men vad ska vi sätta istället för frågetecknen, “customers” är en lista på kunder vi har i minnet i applikationen och inget objekt/tabell i databasen som vi kan komma åt på databas-server-sidan, och därför får vi ett exception. EF vet inte vad den ska göra för att översätta hela uttrycket till SQL. För att förklara felmeddelandet får vi titta på hur Entity Framework, eller mer specifikt Entity Frameworks “LINQ provider” fungerar.

Hur fungerar en LINQ provider?

Vad är då egentligen LINQ? LINQ – Language Integrated Query – tillkom i .NET Framework 3.5 och var ett generaliserat sätt att skriva frågor (queries) mot en datakälla direkt i programmeringsspråket man använder. Datakällan kan vara allt från en lista man har i minnet till en XML-fil till en databas. LINQ implementeras med en kombination av ett antal “LINQ providers” och extension methods. Den enklaste providern är LINQ to Objects, som egentligen inte är någon provider utan implementeras enbart med de extension methods som finns på IEnumerable<T> (System.Linq.Enumerable). Dessa metoder låter oss göra filtrering och andra operationer på alla sorters IEnumerable vi har, exempelvis Where, Select och ToList.

Entity Frameworks LINQ provider erbjuder samma funktioner men översätter dem som nämnt ovan istället till SQL som den kör mot databasen, där LINQ to Objects istället bara kör koden rakt upp och ner i programmets process. Den kan göra det på grund av två nya funktioner i .NET 3.5: Lambda Expressions och Expression Trees. Lambda Expressions eller lambdauttryck är en syntax för att skriva delegater/funktioner utan att behöva deklarera dem som fullständiga metoder. Expression Trees är ett sätt för kompilatorn att ta ett lambdauttryck och omvandla det till en struktur (ett träd) som beskriver vad koden i uttrycket gör.

Jämför hur Where-funktionen är definierad för LINQ to Objects (IEnumerable<T>) och för alla andra LINQ providers (IQueryable<T>):

public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate)

public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);

För argumentet predicate, som alltså är själva filtret som ska användas för att filtrera datat från datakällan, så är det i IQueryables fall markerat som en Expression<T>. Koden för att anropa båda funktionerna ser likadan ut:

someEnumerable.Where(x => x.Id == 5);

someQueryable.Where(x => x.Id == 5);

Men är alltså egentligen helt olika saker. För Enumerable så körs koden som den står rakt upp och ner när det utvärderas, medan för Queryable så byggs ett Expression träd som utvärderas och översätts beroende på LINQ Providern som används. I Entity Frameworks fall så översätts trädet alltså till SQL. Denna översättningsprocess är implementerad med hjälp av ett Visitor-mönster, vilket förklarar varför felmeddelandet vi fick pratade om “VisitLambda”.

Sammanfattning

LINQ och LINQ Providers är ett oerhört komplext system som ger oss utvecklare möjlighet att snabbt skriva lättläst och koncis kod, med mycket större möjlighet för flexibilitet och refaktorisering än att skriva SQL uttryck som strängar. Baksidan av denna komplexitet är att det ibland blir väldigt svårt att felsöka när man stöter på implementationsdetaljer som vi gjorde här.

#EntityFramework #LINQproviders #Microsoft #Programmering #SQL

Relaterade inlägg

TypeScript vs JavaScript