Querying Data in EF Core
Entity Framework Core translates LINQ (Language Integrated Query) expressions into SQL queries that run against your database. Understanding how this translation works is essential for writing efficient data access code.
IQueryable vs IEnumerable
The distinction between IQueryable<T> and IEnumerable<T> is fundamental to how EF Core processes queries.
IQueryable
csharp1// IQueryable - builds an expression tree, no SQL executed yet2IQueryable<Product> query = context.Products3 .Where(p => p.Price > 10)4 .OrderBy(p => p.Name);56// SQL executes HERE when results are materialized7List<Product> results = await query.ToListAsync();
IEnumerableIEnumerable, any further operations happen in C# memory, not in SQL.
csharp1// Dangerous: loads ALL products into memory, then filters in C#2IEnumerable<Product> allProducts = await context.Products.ToListAsync();3var expensive = allProducts.Where(p => p.Price > 100);45// Better: filters in SQL, only matching rows are loaded6var expensive = await context.Products7 .Where(p => p.Price > 100)8 .ToListAsync();
Deferred Execution
LINQ queries use deferred execution -- the query does not run until you request the results. This lets you compose queries step by step.
csharp1var query = context.Products.AsQueryable();23// No SQL yet - just building the expression tree4if (minPrice.HasValue)5 query = query.Where(p => p.Price >= minPrice.Value);67if (!string.IsNullOrEmpty(category))8 query = query.Where(p => p.Category == category);910query = query.OrderBy(p => p.Name);1112// SQL executes now with all filters combined into one query13var products = await query.ToListAsync();
The query is only sent to the database when you call a materializing operator:
| Operator | Behavior |
|---|---|
ToListAsync() | Executes query, returns all results as a list |
ToArrayAsync() | Executes query, returns results as an array |
FirstOrDefaultAsync() | Executes query, returns first result or null |
SingleOrDefaultAsync() | Executes query, returns single result or null (throws if multiple) |
CountAsync() | Executes query, returns the count |
AnyAsync() | Executes query, returns true if any results exist |
Client vs Server Evaluation
EF Core attempts to translate your entire LINQ expression into SQL. When it encounters C# code that has no SQL equivalent, it may evaluate that part on the client side.
Server-evaluated (translated to SQL -- preferred):
csharp1// String methods that EF Core can translate to SQL2var results = await context.Products3 .Where(p => p.Name.Contains("Widget")) // LIKE '%Widget%'4 .Where(p => p.Name.StartsWith("Pro")) // LIKE 'Pro%'5 .Where(p => p.Price > 10 && p.Stock > 0) // AND in SQL6 .ToListAsync();
Client-evaluated (runs in C# after loading data -- avoid if possible):
csharp1// Custom C# method - EF Core cannot translate this to SQL2var results = await context.Products3 .AsEnumerable() // Forces switch to client evaluation4 .Where(p => CalculateDiscount(p.Price) > 5)5 .ToList();
Starting with EF Core 3.0, client evaluation of the final Select expression is allowed, but client evaluation in Where or OrderBy throws an exception by default. This prevents accidentally loading entire tables into memory.
How LINQ Translates to SQL
Each LINQ method maps to a SQL clause:
| LINQ Method | SQL Equivalent |
|---|---|
Where() | WHERE |
OrderBy() / OrderByDescending() | ORDER BY / ORDER BY ... DESC |
Select() | SELECT (column list) |
Skip() / Take() | OFFSET / FETCH (or LIMIT) |
GroupBy() | GROUP BY |
Include() | JOIN or separate query |
Count() / Sum() / Average() | Aggregate functions |
Any() | EXISTS |
First() / Single() | TOP 1 / LIMIT 1 |
Logging Generated SQL
You can inspect the SQL that EF Core generates by enabling logging:
csharp1// In Program.cs or DbContext configuration2options.UseSqlite("Data Source=app.db")3 .LogTo(Console.WriteLine, LogLevel.Information)4 .EnableSensitiveDataLogging(); // Shows parameter values (dev only)
Reviewing the generated SQL helps you catch performance issues early, such as missing filters that cause full table scans or N+1 query problems.
Key Takeaways
- IQueryable builds expression trees that translate to SQL; IEnumerable operates in memory
- Queries use deferred execution and only run when materialized (e.g.,
ToListAsync()) - Keep filters on
IQueryableso they execute as SQL, not in C# memory - EF Core throws exceptions for untranslatable expressions in
WhereandOrderBy - Always review the generated SQL during development to verify efficiency