EF Core query translation: Why does some LINQ never become SQL?

We know that every Entity Framework Core (EF Core) LINQ query has a corresponding SQL query. That equivalent SQL is actually executed under the hood. Some LINQ expressions involve. NET-specific code, such as calling a method, using a reflection filter, or accessing files. You may have found several code blocks of your query that could not be translated. Why does it happen? In today's post, I will cover the reasons along with on-ground examples.

EF Core query translation: Why does some LINQ never become SQL?

LINQ Queries Translation

We will write different LINQ queries in a real project and examine what results they yield. I will use a Console application with a PostgreSQL database as an example.

Step 1: Create a project

dotnet new console -n EfTranslationDemo
cd EfTranslationDemo

Step 2: Install NuGet packages

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

Step 3: Model for the table

This will be a single-table application consisting of the Perfume model.

public class Perfume
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public string Brand { get; set; } = string.Empty;

    public decimal Price { get; set; }

    public int Rating { get; set; }

    public DateTime ReleaseDate { get; set; }
}

Step 4: Create a database

To speed up the process, I have already created the database and the Perfumes table.

CREATE DATABASE perfumedb;

CREATE TABLE public."Perfumes" (
    "Id" serial PRIMARY KEY,
    "Name" varchar(255) NOT NULL DEFAULT '',
    "Brand" varchar(255) NOT NULL DEFAULT '',
    "Price" decimal(10,2) NOT NULL,
    "Rating" integer NOT NULL,
    "ReleaseDate" timestamp NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC')
);

Step 5: Set up the DB context

using EfTranslationDemo.Models;
using Microsoft.EntityFrameworkCore;

namespace EfTranslationDemo.Data;

public class ApplicationDbContext: DbContext
{
    public DbSet<Perfume> Perfumes => Set<Perfume>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options
            .UseNpgsql("Your Connection string with db name perfumedb")
            .LogTo(Console.WriteLine);
    }
}

Step 6: Seed data

To begin with, I will seed some data programmatically.

using EfTranslationDemo.Data;
using EfTranslationDemo.Models;
using Microsoft.EntityFrameworkCore;

using var context = new ApplicationDbContext();

context.Database.EnsureCreated();

SeedData(context);

static void SeedData(ApplicationDbContext context)
{
    if (context.Perfumes.Any())
        return;

    context.Perfumes.AddRange(
        new Perfume { Name="Sauvage", Brand="Dior", Price=120, Rating=9, ReleaseDate=new DateTime(2018,1,1,0,0,0, DateTimeKind.Utc)},
        new Perfume { Name="Bleu De Chanel", Brand="Chanel", Price=150, Rating=10, ReleaseDate=new DateTime(2017,1,1,0,0,0, DateTimeKind.Utc)},
        new Perfume { Name="Aventus", Brand="Creed", Price=300, Rating=10, ReleaseDate=new DateTime(2015,1,1,0,0,0, DateTimeKind.Utc)},
        new Perfume { Name="F Black", Brand="Ferragamo", Price=60, Rating=7, ReleaseDate=new DateTime(2019,1,1,0,0,0, DateTimeKind.Utc)}
    );

    context.SaveChanges();
}

Fast forward, the data is seeded to the database

Perfumes table

Step 7: LINQ queries testing

1. Query with price filter

A simple query to fetch perfumes with a price of more than 100.

Console.WriteLine("Full translable");
var query1 = context
    .Perfumes
    .Where(p => p.Price > 100)
    .Select(p => new { p.Name, p.Price });

Console.WriteLine(query1.ToQueryString());
Query with price filter

2. Filter with Name

We will filter the data with the name starting with 'B'.

var query2 = context
    .Perfumes
    .Where(p => p.Name.StartsWith("B"));

Console.WriteLine(query2.ToQueryString());
Filter with Name

StartsWith() translates to SQL's LIKE . Similarly, EndsWith and Contains are also translated.

3. Client Side Execution

Another translatable but performance-heavy solution is

var perfumes = context
    .Perfumes
    .ToList()
    .Where(p => p.Price > 100);

Console.WriteLine(perfumes.Count());

It loads all the data in memory first, then applies a filter on the stored data. Such a solution can cause significant problems, especially when the data is large.

4. External list filter

We can write a translatable filter with an external list

var brands = new List<string> { "Dior", "Creed" };

var query = context
    .Perfumes
    .Where(p => brands.Contains(p.Brand));

Console.WriteLine(query.ToQueryString());
External list filter

EF Core converts the C# in-memory list into SQL constants.

5. DateTime filter

Any filter with parsing datetime will work the same as any other.

var query = context
    .Perfumes
    .Where(p => p.ReleaseDate.Year > 2016);

Console.WriteLine(query.ToQueryString());
DateTime filter

Common DateTime properties are supported.

6. Custom method filter

What you cannot do in a LINQ query is to filter by a method. LINQ can't find an SQL equivalent of it.

static bool IsLuxury(Perfume perfume)
{
    return perfume.Price > 200;
}

var query = context
    .Perfumes
    .Where(p => IsLuxury(p));

Console.WriteLine(query.ToQueryString());
Custom method filter

EF Core cannot inspect the body of arbitrary C# methods.

7. Regex matching

We can simply put a regex pattern in the filter as well.

var query = context
    .Perfumes
    .Where(p => Regex.IsMatch(p.Name, "^A"));

Console.WriteLine(query.ToQueryString());

var result = query.ToList();
Console.WriteLine(result.Count());
Regex matching

8. Non-Translatable Projection

string GetCategory(decimal price)
{
    return price > 150 ? "Luxury" : "Regular";
}

var query = context
    .Perfumes
    .Select(p => new
    {
        p.Name,
        Category = GetCategory(p.Price)
    });

Console.WriteLine(query.ToQueryString());
Non-Translatable Projection

The right way

var query = context
    .Perfumes
    .Select(p => new
    {
        p.Name,
        Category = p.Price > 150 ? "Luxury" : "Regular"
    });

Console.WriteLine(query.ToQueryString());
Non-Translatable Projection

9. Reflection

.NET does not have an equivalent in SQL.

var query = context
    .Perfumes
    .Where(p => p.GetType().GetProperty("Price") != null);

Console.WriteLine(query.ToQueryString());
Reflection

10. File System Access

var query = context
    .Perfumes
    .Where(p => File.Exists($"brands/{p.Brand}.txt"));

Console.WriteLine(query.ToQueryString());
File System Access

11. Random number generation

var query = context
    .Perfumes
    .Where(p => Random.Shared.Next(0, 10) > 5);

Console.WriteLine(query.ToQueryString());
Random number generation

A filter using random number generation did not translate.

12. string.Compare method

One of the cool methods that we often use for string comparison is string.Comparison. Unfortunately, you cannot use it in a LINQ query.

var query = context
    .Perfumes
    .Where(p => 
        string.Compare(p.Name, "Sauvage", 
            StringComparison.OrdinalIgnoreCase) == 0
        );
var result  = await query.ToListAsync();
Console.WriteLine(query.ToQueryString());
string.Compare method

Why does EF Core have limitations in translating LINQ queries?

One question arises that even after years of improvement and enhancement, EF Core does not translate some common C# expressions in LINQ. Actually, LINQ does not merely read and execute any query. It first creates an expression tree of the query. Next, these trees are converted to SQL. SQL queries are a declarative language that describes what data you want, not how to compute it step by step. So, methods' bodies are not parsed, as in our case with the IsLuxury method, where expressions do not analyze arbitrary method bodies for translation. Also, C# is a full programming language, whereas SQL lacks many of the features of a programming language. Neither can it read its logic, as SQL servers cannot execute .NET runtime code.

EF Core is responsible for translating an expression tree to database-specific SQL. We know that every database has its own SQL dialect. For example, DATE_PART('year', column) in a PostgreSQL expression, while the SQL Server equivalent is YEAR(column). So EF must maintain different translators for each database. It does not rely on a single translator, so there are limitations in translating each and every query across diverse database providers. Finally, some .NET runtime features are specific to the program, such as random number generation, ordinal string comparison, and reflection, which are impossible to translate.

Conclusion

EF Core is a popular ORM and the default choice for many developers for database operations. EF Core uses LINQ for querying and enables you to use databases as if they were in C#. However, the underlying database cannot fully replicate a complete programming language like C#. Many programming expressions and filtering logic cannot be translated into a database query. Calling a method in a LINQ query, using reflection, and ordinal string comparison are examples of limitations in EF Core. SQL is specifically designed to fetch data and is not a programming language. It does not have the .NET runtime, so it cannot execute .NET features. In this post, I have gone through many examples that can be translated into SQL and some common queries that cannot.

Code: https://github.com/elmahio-blog/EfTranslationDemo.git

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website