Skip to main content

LINQ to Entities

Introduction

LINQ to Entities is a powerful implementation of LINQ (Language Integrated Query) specifically designed to work with Entity Framework, Microsoft's object-relational mapping (ORM) framework for .NET. It allows developers to write strongly-typed queries against an Entity Framework data model using familiar C# syntax, which then get translated into SQL commands that execute against the database.

Unlike direct SQL queries, LINQ to Entities offers several advantages:

  • Type safety: Compile-time error checking for your database queries
  • IntelliSense support: Code completion and suggestions as you write queries
  • Language integration: Write database queries using familiar C# syntax
  • Abstraction: Work with database objects as .NET objects
  • Testing: Easier to mock and test than raw SQL

In this tutorial, you'll learn how LINQ to Entities works, how to write basic and advanced queries, and how to use it in real-world scenarios.

Prerequisites

To follow along with this tutorial, you should have:

  • Basic knowledge of C# and .NET
  • Familiarity with basic LINQ concepts
  • Entity Framework installed in your project (Microsoft.EntityFrameworkCore for EF Core)
  • A database context set up in your application

Basic LINQ to Entities Queries

Let's start by looking at some basic queries. We'll use a simple bookstore database model with Book, Author, and Category entities.

Setting Up Your Context

First, you need an Entity Framework context class:

csharp
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;

public class BookstoreContext : DbContext
{
public BookstoreContext(DbContextOptions<BookstoreContext> options)
: base(options)
{ }

public DbSet<Book> Books { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<Category> Categories { get; set; }
}

public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public decimal Price { get; set; }
public int AuthorId { get; set; }
public int CategoryId { get; set; }
public int PageCount { get; set; }
public bool IsAvailable { get; set; }

public Author Author { get; set; }
public Category Category { get; set; }
}

public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Book> Books { get; set; }
}

public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Book> Books { get; set; }
}

Querying Data

The most basic LINQ to Entities query retrieves all records from a table:

csharp
// Get all books
public List<Book> GetAllBooks(BookstoreContext context)
{
var books = context.Books.ToList();
return books;
}

When this code executes, Entity Framework generates a SQL query like:

sql
SELECT [b].[Id], [b].[Title], [b].[Price], [b].[AuthorId], [b].[CategoryId], 
[b].[PageCount], [b].[IsAvailable]
FROM [Books] AS [b]

Filtering with Where

One of the most common operations is filtering data:

csharp
// Get all books with price less than $20
public List<Book> GetAffordableBooks(BookstoreContext context)
{
var affordableBooks = context.Books
.Where(book => book.Price < 20.0m)
.ToList();
return affordableBooks;
}

This generates SQL similar to:

sql
SELECT [b].[Id], [b].[Title], [b].[Price], [b].[AuthorId], [b].[CategoryId], 
[b].[PageCount], [b].[IsAvailable]
FROM [Books] AS [b]
WHERE [b].[Price] < 20.0

Ordering Results

You can order your query results:

csharp
// Get books ordered by price (lowest to highest)
public List<Book> GetBooksOrderedByPrice(BookstoreContext context)
{
var orderedBooks = context.Books
.OrderBy(book => book.Price)
.ToList();
return orderedBooks;
}

For descending order:

csharp
// Get books ordered by price (highest to lowest)
public List<Book> GetBooksOrderedByPriceDescending(BookstoreContext context)
{
var orderedBooks = context.Books
.OrderByDescending(book => book.Price)
.ToList();
return orderedBooks;
}

Selecting Specific Columns

You can select only the columns you need:

csharp
// Get only book titles and prices
public List<BookSummary> GetBookSummaries(BookstoreContext context)
{
var bookSummaries = context.Books
.Select(book => new BookSummary
{
Title = book.Title,
Price = book.Price
})
.ToList();
return bookSummaries;
}

public class BookSummary
{
public string Title { get; set; }
public decimal Price { get; set; }
}

LINQ to Entities makes it easy to work with related data through navigation properties.

Eager Loading with Include

To load related data in the same query:

csharp
// Get all books with their authors
public List<Book> GetBooksWithAuthors(BookstoreContext context)
{
var booksWithAuthors = context.Books
.Include(book => book.Author)
.ToList();
return booksWithAuthors;
}

This generates a SQL JOIN:

sql
SELECT [b].[Id], [b].[Title], [b].[Price], /* other book columns */,
[a].[Id], [a].[Name], /* other author columns */
FROM [Books] AS [b]
INNER JOIN [Authors] AS [a] ON [b].[AuthorId] = [a].[Id]

Multiple Levels of Include

You can include multiple related entities:

csharp
// Get books with both authors and categories
public List<Book> GetBooksWithAuthorsAndCategories(BookstoreContext context)
{
var books = context.Books
.Include(book => book.Author)
.Include(book => book.Category)
.ToList();
return books;
}

You can filter records based on related data:

csharp
// Get all books by author named "J.K. Rowling"
public List<Book> GetBooksByAuthor(BookstoreContext context, string authorName)
{
var books = context.Books
.Where(book => book.Author.Name == authorName)
.ToList();
return books;
}

Advanced LINQ to Entities Features

Aggregation Functions

LINQ to Entities provides several aggregation functions:

csharp
// Get the average book price
public decimal GetAverageBookPrice(BookstoreContext context)
{
return context.Books.Average(book => book.Price);
}

// Count books in a specific category
public int CountBooksInCategory(BookstoreContext context, int categoryId)
{
return context.Books.Count(book => book.CategoryId == categoryId);
}

// Get the most expensive book price
public decimal GetMaxBookPrice(BookstoreContext context)
{
return context.Books.Max(book => book.Price);
}

Grouping Results

You can group query results:

csharp
// Get count of books by category
public List<CategoryBookCount> GetBookCountByCategory(BookstoreContext context)
{
var bookCountByCategory = context.Books
.GroupBy(book => book.Category.Name)
.Select(group => new CategoryBookCount
{
CategoryName = group.Key,
BookCount = group.Count()
})
.ToList();

return bookCountByCategory;
}

public class CategoryBookCount
{
public string CategoryName { get; set; }
public int BookCount { get; set; }
}

Paging Results

For large datasets, you often want to retrieve data in pages:

csharp
// Get books with paging (page size = 10)
public List<Book> GetBooksPaged(BookstoreContext context, int pageNumber, int pageSize = 10)
{
return context.Books
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToList();
}

This generates SQL with OFFSET and FETCH:

sql
SELECT [b].[Id], [b].[Title], /* other columns */
FROM [Books] AS [b]
ORDER BY [b].[Id]
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

Real-World Examples

Let's explore some practical examples you might use in a real application.

Example 1: Building a Book Search Function

csharp
public List<Book> SearchBooks(BookstoreContext context, string searchTerm = null, 
decimal? maxPrice = null, int? categoryId = null)
{
var query = context.Books.AsQueryable();

// Apply filters conditionally
if (!string.IsNullOrEmpty(searchTerm))
{
query = query.Where(b => b.Title.Contains(searchTerm));
}

if (maxPrice.HasValue)
{
query = query.Where(b => b.Price <= maxPrice.Value);
}

if (categoryId.HasValue)
{
query = query.Where(b => b.CategoryId == categoryId.Value);
}

// Include related data
query = query.Include(b => b.Author).Include(b => b.Category);

// Order by relevance (if searching) or by title
if (!string.IsNullOrEmpty(searchTerm))
{
// Books with the term at the start of the title come first
query = query.OrderByDescending(b => b.Title.StartsWith(searchTerm))
.ThenBy(b => b.Title);
}
else
{
query = query.OrderBy(b => b.Title);
}

return query.ToList();
}

Example 2: Dashboard Statistics

csharp
public class BookstoreDashboard
{
public int TotalBooks { get; set; }
public int TotalAuthors { get; set; }
public decimal AverageBookPrice { get; set; }
public int BooksAddedThisMonth { get; set; }
public List<CategorySummary> TopCategories { get; set; }
}

public class CategorySummary
{
public string CategoryName { get; set; }
public int BookCount { get; set; }
}

public BookstoreDashboard GetDashboardStats(BookstoreContext context)
{
var now = DateTime.UtcNow;
var startOfMonth = new DateTime(now.Year, now.Month, 1);

return new BookstoreDashboard
{
TotalBooks = context.Books.Count(),
TotalAuthors = context.Authors.Count(),
AverageBookPrice = context.Books.Average(b => b.Price),
BooksAddedThisMonth = context.Books
.Count(b => b.CreatedAt >= startOfMonth),
TopCategories = context.Categories
.OrderByDescending(c => c.Books.Count)
.Take(5)
.Select(c => new CategorySummary
{
CategoryName = c.Name,
BookCount = c.Books.Count
})
.ToList()
};
}

Best Practices and Optimization

1. Defer Execution Until Needed

LINQ to Entities queries use deferred execution, meaning they don't run until you iterate over the results or call methods like ToList(), First(), etc. Use this to build complex queries:

csharp
// The query isn't executed yet
var query = context.Books.Where(b => b.Price < 20);

// Apply more conditions if needed
if (onlyAvailable)
{
query = query.Where(b => b.IsAvailable);
}

// Query executes here when ToList() is called
var results = query.ToList();

2. Use IQueryable for Building Complex Queries

Keep your query as IQueryable<T> until you're ready to execute it:

csharp
// Good: Builds query before execution
public IQueryable<Book> GetBookQuery(BookstoreContext context)
{
return context.Books.Where(b => b.IsAvailable);
}

// Usage:
var affordableBooks = GetBookQuery(context)
.Where(b => b.Price < 20)
.ToList();

3. Avoid Loading Unnecessary Data

Only select the columns you need:

csharp
// Bad: Loads full objects
var titles = context.Books.ToList().Select(b => b.Title);

// Good: Only retrieves titles from database
var titles = context.Books.Select(b => b.Title).ToList();

4. Use Async Methods for Better Performance

For web applications, use async versions of LINQ methods:

csharp
public async Task<List<Book>> GetBooksAsync(BookstoreContext context)
{
return await context.Books
.Where(b => b.IsAvailable)
.ToListAsync();
}

Common Pitfalls

1. N+1 Query Problem

The N+1 query problem occurs when you load a collection and then individually access related entities for each item:

csharp
// Bad: Causes N+1 queries
var authors = context.Authors.ToList();
foreach (var author in authors)
{
// This causes a separate query for each author!
Console.WriteLine($"{author.Name} has {author.Books.Count()} books");
}

// Good: Use Include to load related data in one query
var authors = context.Authors.Include(a => a.Books).ToList();
foreach (var author in authors)
{
// No additional queries
Console.WriteLine($"{author.Name} has {author.Books.Count()} books");
}

2. Client vs Server Evaluation

Not all C# operations can be translated to SQL. When EF Core can't translate an operation, it retrieves data from the database and performs the operation in memory:

csharp
// Won't work well - custom method can't be translated to SQL
public bool IsPopularBook(Book book) => book.PageCount > 100 && book.Price < 15;

// This causes entire table to be loaded before filtering
var popularBooks = context.Books.Where(b => IsPopularBook(b)).ToList();

// Better - use expressions that EF can translate
var popularBooks = context.Books
.Where(b => b.PageCount > 100 && b.Price < 15)
.ToList();

Summary

LINQ to Entities through Entity Framework provides a powerful and type-safe way to query your databases using C# syntax. In this tutorial, we've covered:

  • Basic queries for retrieving, filtering, and ordering data
  • Working with related data using Include
  • Advanced features like aggregation and grouping
  • Building real-world examples
  • Best practices and optimization techniques

By mastering LINQ to Entities, you can write clean, maintainable data access code that bridges the gap between your database and your C# application logic.

Additional Resources

Practice Exercises

  1. Create a LINQ to Entities query that finds all books published in the last year, ordered by most recent first.
  2. Write a query to find authors who have written more than 3 books.
  3. Create a book recommendation system that finds similar books based on category and price range.
  4. Build a query that calculates the average price of books by category and shows which categories have an average price above the overall average.
  5. Create a paged list of books that includes author information, ordered by author name and then by book title.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)