Essential patterns and techniques for writing efficient LINQ queries in .NET 9
// ✅ GOOD - 13-20% faster for read-only
var products = await _context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
AsNoTracking() disables change tracking, reducing memory and improving performance for queries that don't need to update entities.
// ✅ GOOD - Non-blocking
var products = await _context.Products
.Where(p => p.Price > 100)
.ToListAsync();
// ❌ BAD - Blocks thread
var products = _context.Products
.Where(p => p.Price > 100)
.ToList();
// ✅ GOOD - Fetch only needed columns
var summaries = await _context.Products
.Where(p => p.IsActive)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
// ✅ GOOD - Filter first
var products = await _context.Products
.Where(p => p.IsActive && p.Price > 100)
.OrderBy(p => p.Name)
.ToListAsync();
// ✅ GOOD - Paginate large results
var products = await _context.Products
.Where(p => p.IsActive)
.OrderBy(p => p.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// ✅ GOOD - Eager loading (1 query)
var products = await _context.Products
.Include(p => p.Category)
.Include(p => p.Reviews)
.Where(p => p.IsActive)
.ToListAsync();
-- ✅ GOOD - Index frequently queried columns
CREATE INDEX IX_Products_Price
ON Products(Price);
CREATE INDEX IX_Products_Category_IsActive
ON Products(Category, IsActive);
// ✅ GOOD - Build query incrementally
IQueryable<Product> query = _context.Products;
if (!string.IsNullOrEmpty(category))
query = query.Where(p => p.Category == category);
if (minPrice.HasValue)
query = query.Where(p => p.Price >= minPrice);
var results = await query.ToListAsync();
// ✅ GOOD - Stops at first match
if (await _context.Products.AnyAsync(p => p.Price > 1000))
{
// ...
}
// ❌ BAD - Counts all matching records
if (await _context.Products.CountAsync(p => p.Price > 1000) > 0)
{
// ...
}
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
if (builder.Environment.IsDevelopment())
{
options.EnableSensitiveDataLogging();
options.LogTo(Console.WriteLine, LogLevel.Information);
}
});
// ❌ BAD - Loads everything into memory first
var allProducts = await _context.Products.ToListAsync();
var filtered = allProducts
.Where(p => p.Price > 100)
.ToList();
// ✅ GOOD - Filter in database
var filtered = await _context.Products
.Where(p => p.Price > 100)
.ToListAsync();
// ❌ BAD - N+1 queries
var products = await _context.Products.ToListAsync();
foreach (var product in products)
{
// Additional query per product!
var category = await _context.Categories
.FirstAsync(c => c.Id == product.CategoryId);
}
// ✅ GOOD - Single query with Include
var products = await _context.Products
.Include(p => p.Category)
.ToListAsync();
// ❌ BAD - Executes immediately, then filters in memory
var products = await _context.Products.ToListAsync();
var filtered = products
.Where(p => p.Price > 100)
.OrderBy(p => p.Name)
.Take(10);
// ✅ GOOD - All operations in database
var filtered = await _context.Products
.Where(p => p.Price > 100)
.OrderBy(p => p.Name)
.Take(10)
.ToListAsync();
// ❌ BAD - Fetches all columns
var products = await _context.Products
.Where(p => p.IsActive)
.ToListAsync();
var names = products.Select(p => p.Name).ToList();
// ✅ GOOD - Fetch only name
var names = await _context.Products
.Where(p => p.IsActive)
.Select(p => p.Name)
.ToListAsync();
// ❌ BAD - Method executed client-side
var products = await _context.Products
.Where(p => IsExpensive(p.Price)) // Can't translate to SQL
.ToListAsync();
// ✅ GOOD - Direct comparison in SQL
var products = await _context.Products
.Where(p => p.Price > 500)
.ToListAsync();
// ❌ BAD - Not using 'using' statement
var context = new AppDbContext();
var products = context.Products.ToList();
// Context not disposed!
// ✅ GOOD - Automatic disposal
using (var context = new AppDbContext())
{
var products = context.Products.ToList();
} // Context disposed here
// ✅ EVEN BETTER - Dependency Injection
// Let DI container manage lifetime
// ❌ BAD - Multiple queries
var categories = await _context.Products
.Select(p => p.Category)
.Distinct()
.ToListAsync();
foreach (var cat in categories)
{
var count = await _context.Products
.CountAsync(p => p.Category == cat);
}
// ✅ GOOD - Single GroupBy query
var stats = await _context.Products
.GroupBy(p => p.Category)
.Select(g => new {
Category = g.Key,
Count = g.Count()
})
.ToListAsync();
// ❌ BAD - Ignoring warnings
#pragma warning disable EF1001
var products = await _context.Products
.Where(p => SomeClientMethod(p))
.ToListAsync();
// ✅ GOOD - Fix the warning
var products = await _context.Products
.Where(p => p.Price > threshold)
.ToListAsync();
| Scenario | Use |
|---|---|
| Read-only queries | .AsNoTracking() |
| Existence check | .AnyAsync() |
| Related data | .Include() |
| Partial data | .Select() |
| Large results | .Skip().Take() |
| Aggregations | .GroupBy() |
| Single result | .FirstOrDefaultAsync() |
| All results | .ToListAsync() |