Appearance
LINQ 查询基础
EF Core 中 LINQ 查询的完整指南,从基础到高级技巧
📖 目录
LINQ 查询概述
什么是 LINQ?
LINQ (Language Integrated Query) 是 .NET 的语言集成查询功能,允许使用 C# 语法查询各种数据源。
csharp
// LINQ to Objects (内存集合)
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
// LINQ to Entities (数据库查询)
var products = await context.Products
.Where(p => p.Price > 100)
.ToListAsync();EF Core 中的 LINQ
EF Core 将 LINQ 表达式翻译为 SQL 查询:
csharp
// C# LINQ
var products = await context.Products
.Where(p => p.Price > 100)
.OrderByDescending(p => p.Price)
.Take(10)
.ToListAsync();
// 翻译为 SQL
// SELECT TOP 10 * FROM Products
// WHERE Price > 100
// ORDER BY Price DESC两种语法
1. 方法语法(Method Syntax)- 推荐
csharp
var products = await context.Products
.Where(p => p.IsActive)
.OrderByDescending(p => p.CreatedAt)
.Select(p => new { p.Id, p.Name })
.ToListAsync();优点:
- ✅ 更灵活
- ✅ 支持所有 LINQ 操作符
- ✅ 更易组合
2. 查询语法(Query Syntax)
csharp
var products = await (from p in context.Products
where p.IsActive
orderby p.CreatedAt descending
select new { p.Id, p.Name })
.ToListAsync();优点:
- ✅ 类似 SQL,易读
- ⚠️ 部分操作符不支持
基础查询操作
1. 查询所有记录
csharp
// 查询所有产品
var allProducts = await context.Products.ToListAsync();
// 生成的 SQL
// SELECT * FROM Products⚠️ 警告: 避免在生产环境使用,会加载所有数据!
2. 根据主键查询
csharp
// FindAsync - 先检查缓存,再查询数据库
var product = await context.Products.FindAsync(1);
// FirstOrDefaultAsync - 始终查询数据库
var product = await context.Products
.FirstOrDefaultAsync(p => p.Id == 1);
// SingleOrDefaultAsync - 期望唯一结果
var product = await context.Products
.SingleOrDefaultAsync(p => p.Sku == "LAPTOP-001");对比:
| 方法 | 缓存检查 | 异常处理 | 适用场景 |
|---|---|---|---|
FindAsync | ✅ | 返回 null | 主键查询 |
FirstOrDefaultAsync | ❌ | 返回第一个或 null | 条件查询 |
SingleOrDefaultAsync | ❌ | 多个结果抛异常 | 唯一约束 |
FirstAsync | ❌ | 无结果抛异常 | 确保有结果 |
3. 条件查询
csharp
// 单个条件
var expensiveProducts = await context.Products
.Where(p => p.Price > 100)
.ToListAsync();
// 多个条件 (AND)
var result = await context.Products
.Where(p => p.Price > 100 && p.IsActive)
.ToListAsync();
// 多个条件 (OR)
var result = await context.Products
.Where(p => p.Price > 100 || p.CategoryId == 1)
.ToListAsync();
// 复杂条件
var result = await context.Products
.Where(p => p.Price > 100 &&
p.IsActive &&
(p.CategoryId == 1 || p.CategoryId == 2))
.ToListAsync();过滤和排序
1. Where 过滤
csharp
// 字符串匹配
var products = await context.Products
.Where(p => p.Name.StartsWith("Laptop"))
.ToListAsync();
var products = await context.Products
.Where(p => p.Name.Contains("Pro"))
.ToListAsync();
var products = await context.Products
.Where(p => p.Name.EndsWith("X"))
.ToListAsync();
// 数值范围
var products = await context.Products
.Where(p => p.Price >= 50 && p.Price <= 200)
.ToListAsync();
// 日期范围
var startDate = DateTime.UtcNow.AddDays(-30);
var products = await context.Products
.Where(p => p.CreatedAt >= startDate)
.ToListAsync();
// IN 查询
var categoryIds = new[] { 1, 2, 3 };
var products = await context.Products
.Where(p => categoryIds.Contains(p.CategoryId))
.ToListAsync();
// NOT IN 查询
var products = await context.Products
.Where(p => !categoryIds.Contains(p.CategoryId))
.ToListAsync();
// NULL 检查
var products = await context.Products
.Where(p => p.Description != null)
.ToListAsync();
var products = await context.Products
.Where(p => string.IsNullOrEmpty(p.Description))
.ToListAsync();2. OrderBy 排序
csharp
// 单列排序
var products = await context.Products
.OrderBy(p => p.Price) // 升序
.ToListAsync();
var products = await context.Products
.OrderByDescending(p => p.Price) // 降序
.ToListAsync();
// 多列排序
var products = await context.Products
.OrderByDescending(p => p.CategoryId) // 第一排序
.ThenByDescending(p => p.Price) // 第二排序
.ThenBy(p => p.Name) // 第三排序
.ToListAsync();
// 动态排序
string sortBy = "Price";
bool descending = true;
IQueryable<Product> query = context.Products;
query = sortBy.ToLower() switch
{
"price" => descending
? query.OrderByDescending(p => p.Price)
: query.OrderBy(p => p.Price),
"name" => descending
? query.OrderByDescending(p => p.Name)
: query.OrderBy(p => p.Name),
_ => query.OrderBy(p => p.Id)
};
var products = await query.ToListAsync();3. Distinct 去重
csharp
// 获取所有唯一的分类 ID
var categoryIds = await context.Products
.Select(p => p.CategoryId)
.Distinct()
.ToListAsync();
// 注意: Distinct 只能用于简单类型
// 对于复杂类型,需要使用 GroupBy投影和转换
1. Select 投影
csharp
// 匿名类型投影
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
p.Price
})
.ToListAsync();
// DTO 投影
var products = await context.Products
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name // 导航属性
})
.ToListAsync();
// 计算字段
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
OriginalPrice = p.Price,
DiscountedPrice = p.Price * 0.9m,
TaxAmount = p.Price * 0.13m
})
.ToListAsync();2. 条件投影
csharp
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
Status = p.IsActive ? "Active" : "Inactive",
PriceLevel = p.Price > 100 ? "Expensive" :
p.Price > 50 ? "Medium" : "Cheap"
})
.ToListAsync();3. Skip 和 Take (分页)
csharp
int pageNumber = 1;
int pageSize = 20;
var products = await context.Products
.OrderByDescending(p => p.CreatedAt)
.Skip((pageNumber - 1) * pageSize) // 跳过前面的记录
.Take(pageSize) // 取指定数量的记录
.ToListAsync();
// 生成的 SQL (SQL Server)
// SELECT * FROM Products
// ORDER BY CreatedAt DESC
// OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY完整的分页查询:
csharp
public async Task<PagedResult<Product>> GetProductsAsync(
int pageNumber,
int pageSize,
string searchTerm = null)
{
IQueryable<Product> query = context.Products;
// 搜索过滤
if (!string.IsNullOrEmpty(searchTerm))
{
query = query.Where(p => p.Name.Contains(searchTerm));
}
// 总数
var totalCount = await query.CountAsync();
// 分页数据
var items = await query
.OrderByDescending(p => p.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<Product>
{
Items = items,
TotalCount = totalCount,
PageNumber = pageNumber,
PageSize = pageSize,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
};
}聚合函数
1. 计数
csharp
// 总数量
var count = await context.Products.CountAsync();
// 条件计数
var activeCount = await context.Products
.CountAsync(p => p.IsActive);
// 长整型计数(大数据集)
var count = await context.Products.LongCountAsync();2. 求和、平均、最大、最小
csharp
// 求和
var totalValue = await context.Products
.SumAsync(p => p.Price);
// 平均值
var avgPrice = await context.Products
.AverageAsync(p => p.Price);
// 最大值
var maxPrice = await context.Products
.MaxAsync(p => p.Price);
// 最小值
var minPrice = await context.Products
.MinAsync(p => p.Price);
// 带条件的聚合
var avgActivePrice = await context.Products
.Where(p => p.IsActive)
.AverageAsync(p => p.Price);3. 判断存在性
csharp
// Any - 是否存在满足条件的记录
bool hasExpensiveProducts = await context.Products
.AnyAsync(p => p.Price > 1000);
// All - 是否所有记录都满足条件
bool allActive = await context.Products
.AllAsync(p => p.IsActive);
// Contains - 是否包含特定值
var productIds = new[] { 1, 2, 3 };
bool exists = await context.Products
.AnyAsync(p => productIds.Contains(p.Id));性能提示:
- ✅
AnyAsync()比CountAsync() > 0更快 - ✅
AnyAsync()找到第一条就停止 - ❌
CountAsync()需要遍历所有记录
分组查询
1. 基础分组
csharp
// 按分类分组
var groupedProducts = await context.Products
.GroupBy(p => p.CategoryId)
.Select(g => new
{
CategoryId = g.Key,
Count = g.Count(),
TotalValue = g.Sum(p => p.Price),
AvgPrice = g.Average(p => p.Price),
MaxPrice = g.Max(p => p.Price),
MinPrice = g.Min(p => p.Price)
})
.ToListAsync();
foreach (var group in groupedProducts)
{
Console.WriteLine($"Category {group.CategoryId}: " +
$"{group.Count} products, " +
$"Avg: ${group.AvgPrice:F2}");
}生成的 SQL:
sql
SELECT
[CategoryId],
COUNT(*) AS [Count],
SUM([Price]) AS [TotalValue],
AVG([Price]) AS [AvgPrice],
MAX([Price]) AS [MaxPrice],
MIN([Price]) AS [MinPrice]
FROM [Products]
GROUP BY [CategoryId]2. 多列分组
csharp
var stats = await context.Products
.GroupBy(p => new { p.CategoryId, p.IsActive })
.Select(g => new
{
g.Key.CategoryId,
g.Key.IsActive,
Count = g.Count(),
AvgPrice = g.Average(p => p.Price)
})
.ToListAsync();3. 分组后过滤
csharp
// HAVING 子句
var categories = await context.Products
.GroupBy(p => p.CategoryId)
.Where(g => g.Count() > 10) // 产品数量大于 10 的分类
.Select(g => new
{
CategoryId = g.Key,
Count = g.Count(),
AvgPrice = g.Average(p => p.Price)
})
.ToListAsync();联接查询
1. Inner Join
csharp
// 方法语法
var result = await context.Products
.Join(context.Categories,
product => product.CategoryId,
category => category.Id,
(product, category) => new
{
ProductName = product.Name,
CategoryName = category.Name,
Price = product.Price
})
.ToListAsync();
// 查询语法(更清晰)
var result = await (from p in context.Products
join c in context.Categories
on p.CategoryId equals c.Id
select new
{
ProductName = p.Name,
CategoryName = c.Name,
Price = p.Price
})
.ToListAsync();2. Left Join
csharp
// 左外连接
var result = await (from p in context.Products
join c in context.Categories
on p.CategoryId equals c.Id into gj
from c in gj.DefaultIfEmpty()
select new
{
ProductName = p.Name,
CategoryName = c != null ? c.Name : "No Category",
Price = p.Price
})
.ToListAsync();
// 或使用导航属性(推荐)
var result = await context.Products
.Select(p => new
{
ProductName = p.Name,
CategoryName = p.Category != null ? p.Category.Name : "No Category",
Price = p.Price
})
.ToListAsync();3. 多表联接
csharp
var result = await (from o in context.Orders
join c in context.Customers on o.CustomerId equals c.Id
join oi in context.OrderItems on o.Id equals oi.OrderId
join p in context.Products on oi.ProductId equals p.Id
where o.OrderDate >= DateTime.Today.AddDays(-30)
select new
{
OrderId = o.Id,
CustomerName = c.Name,
ProductName = p.Name,
Quantity = oi.Quantity,
TotalAmount = oi.Quantity * p.Price
})
.ToListAsync();异步查询
1. 异步方法列表
csharp
// 返回列表
await context.Products.ToListAsync();
// 返回单个
await context.Products.FirstOrDefaultAsync(p => p.Id == 1);
await context.Products.SingleOrDefaultAsync(p => p.Id == 1);
await context.Products.FindAsync(1);
// 聚合
await context.Products.CountAsync();
await context.Products.SumAsync(p => p.Price);
await context.Products.AverageAsync(p => p.Price);
await context.Products.MaxAsync(p => p.Price);
await context.Products.MinAsync(p => p.Price);
// 判断
await context.Products.AnyAsync(p => p.Price > 100);
await context.Products.AllAsync(p => p.IsActive);
// 分页
await context.Products.Skip(10).Take(20).ToListAsync();2. 异步最佳实践
csharp
// ✅ 正确: 全程异步
public async Task<List<ProductDto>> GetProductsAsync()
{
var products = await context.Products
.Where(p => p.IsActive)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name
})
.ToListAsync();
return products;
}
// ❌ 错误: 混用同步和异步
public List<Product> GetProducts()
{
var products = context.Products.ToList(); // 同步阻塞
return products;
}查询执行时机
延迟执行(Deferred Execution)
csharp
// 此时查询并未执行
var query = context.Products.Where(p => p.Price > 100);
// 添加更多条件
query = query.OrderBy(p => p.Name);
// 真正执行查询
var products = await query.ToListAsync();立即执行的操作
以下操作会立即执行查询:
csharp
await query.ToListAsync();
await query.FirstOrDefaultAsync();
await query.CountAsync();
await query.AnyAsync();
await query.SumAsync(p => p.Price);
// 同步版本(避免使用)
query.ToList();
query.FirstOrDefault();查询组合
csharp
// 基础查询
IQueryable<Product> query = context.Products;
// 动态添加条件
if (searchTerm != null)
{
query = query.Where(p => p.Name.Contains(searchTerm));
}
if (minPrice.HasValue)
{
query = query.Where(p => p.Price >= minPrice.Value);
}
if (categoryId.HasValue)
{
query = query.Where(p => p.CategoryId == categoryId.Value);
}
// 最后执行
var products = await query.ToListAsync();常见陷阱
1. 客户端评估限制
csharp
// ❌ EF Core 3.0+ 不再支持客户端评估
var products = await context.Products
.Where(p => IsExpensive(p.Price)) // 运行时异常!
.ToListAsync();
bool IsExpensive(decimal price)
{
return price > 100;
}
// ✅ 正确: 使用服务器端可翻译的表达式
var products = await context.Products
.Where(p => p.Price > 100)
.ToListAsync();2. N+1 查询问题
csharp
// ❌ 错误: N+1 查询
var products = await context.Products.ToListAsync();
foreach (var product in products)
{
var category = await context.Categories
.FirstOrDefaultAsync(c => c.Id == product.CategoryId); // N 次查询
}
// ✅ 正确: 使用 Include
var products = await context.Products
.Include(p => p.Category)
.ToListAsync();
// ✅ 更好: 投影查询
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
CategoryName = p.Category.Name
})
.ToListAsync();3. 忘记异步
csharp
// ❌ 错误: 同步阻塞
var products = context.Products.ToList();
// ✅ 正确: 异步非阻塞
var products = await context.Products.ToListAsync();4. 过度使用 ToList
csharp
// ❌ 错误: 过早执行查询
var products = await context.Products.ToListAsync();
var filtered = products.Where(p => p.Price > 100).ToList();
// ✅ 正确: 在数据库中过滤
var products = await context.Products
.Where(p => p.Price > 100)
.ToListAsync();💡 最佳实践
1. 始终使用异步方法
csharp
// ✅ 推荐
await context.Products.ToListAsync();
// ❌ 避免
context.Products.ToList();2. 投影查询减少数据传输
csharp
// ✅ 推荐
var products = await context.Products
.Select(p => new ProductDto { Id = p.Id, Name = p.Name })
.ToListAsync();
// ❌ 查询所有字段
var products = await context.Products.ToListAsync();3. 避免在循环中查询
csharp
// ❌ 错误
foreach (var productId in productIds)
{
var product = await context.Products.FindAsync(productId);
}
// ✅ 正确
var products = await context.Products
.Where(p => productIds.Contains(p.Id))
.ToListAsync();4. 使用索引优化查询
csharp
// 为常用查询字段添加索引
modelBuilder.Entity<Product>(entity =>
{
entity.HasIndex(e => e.CategoryId);
entity.HasIndex(e => e.Name);
entity.HasIndex(e => new { e.CategoryId, e.Price });
});📚 延伸阅读
💡 小结
核心要点:
- ✅ 优先使用方法语法
- ✅ 始终使用异步方法
- ✅ 投影查询减少数据传输
- ✅ 避免 N+1 查询问题
- ✅ 理解延迟执行机制
- ✅ 合理使用索引优化性能
下一步:
- 学习 预加载 Include
- 掌握 投影查询
- 理解 复杂查询