Skip to content

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 查询问题
  • ✅ 理解延迟执行机制
  • ✅ 合理使用索引优化性能

下一步:

  1. 学习 预加载 Include
  2. 掌握 投影查询
  3. 理解 复杂查询

基于 MIT 许可发布