ASP.NET LINQ 教程:从入门到精通

目录

  1. 什么是 LINQ?
    • 核心思想
    • 为什么需要 LINQ?
    • LINQ 的两大组成部分
  2. LINQ 查询语法与方法语法
    • 查询语法 (Query Syntax) - 像SQL
    • 方法语法 (Method Syntax / Lambda Syntax) - 链式调用
    • 两种语法的对比与选择
  3. LINQ to Objects:操作内存中的集合
    • 基础查询操作:Where, Select, OrderBy
    • 聚合操作:Count, Sum, Average, Max, Min
    • 分组操作:GroupBy
    • 集合操作:Join, GroupJoin
    • 示例:在 ASP.NET 中处理数据列表
  4. LINQ to SQL:操作 SQL Server 数据库
    • 什么是 LINQ to SQL?
    • 步骤 1:创建数据模型 (dbml 文件)
    • 步骤 2:创建 DataContext
    • 步骤 3:编写查询
    • 增、删、改、查 操作
    • 重要提示:LINQ to SQL 的现状
  5. Entity Framework (EF) Core:现代数据库访问方式
    • 为什么 EF Core 是更好的选择?
    • 使用 EF Core 进行查询 (LINQ 是核心)
    • 示例:在 ASP.NET Core MVC 中使用 EF Core 和 LINQ
  6. ASP.NET 中的高级 LINQ 应用
    • 异步查询 (ToListAsync)
    • 投影:只查询需要的字段
    • 动态查询 (IQueryable vs IEnumerable)
  7. 最佳实践与性能优化
    • 延迟执行
    • 在服务层使用 IQueryable,在控制器/视图使用 IEnumerable
    • 避免 N+1 查询问题
  8. 总结与学习资源

什么是 LINQ?

核心思想

LINQ (Language Integrated Query),即“语言集成查询”,是微软在 .NET 3.5 中引入的一项革命性技术,它将强大的查询能力直接集成到了 C# (和 VB.NET) 语言中。

asp.net linq教程
(图片来源网络,侵删)

LINQ 允许你使用类似 SQL 的语法来查询任何类型的数据,无论是内存中的对象、数据库、XML 文件还是 Web 服务。

为什么需要 LINQ?

在 LINQ 出现之前,我们查询不同数据源的方式完全不同:

  • 查询内存集合 (List, Array):使用 for 循环或 foreach 循环,配合 if 条件进行筛选,代码冗长,且不直观。
  • 查询数据库:使用 ADO.NET,需要手写 SQL 语句,然后创建 Connection, Command, DataReader 等对象,将数据映射到自定义对象中,代码繁琐,容易出错。
  • 查询 XML:使用复杂的 XPath 或 XmlDocument API。

LINQ 的出现统一了这些查询方式,带来了以下好处:

  • 统一性:无论数据源是什么,查询语法都非常相似。
  • 强类型:编译器可以检查你的查询,在编译时就能发现错误,而不是等到运行时。
  • 可读性:查询语法(特别是查询语法)非常接近自然语言和 SQL,易于理解和维护。
  • 智能感知:在 Visual Studio 中,编写 LINQ 查询时可以获得完整的智能提示和代码补全。

LINQ 的两大组成部分

  1. 标准查询操作符:这是 LINQ 的核心,是一组在 System.Linq 命名空间中定义的方法,如 Where, Select, OrderBy, GroupBy 等,它们是扩展方法,可以作用于任何实现了 IEnumerable<T> 接口的集合。
  2. 查询语法:这是 C# 语言本身提供的一种语法糖,它最终会被编译器转换成对标准查询操作符的调用,它让查询代码看起来更像 SQL。

LINQ 查询语法与方法语法

理解这两种语法是掌握 LINQ 的第一步。

asp.net linq教程
(图片来源网络,侵删)

查询语法

使用类似 SQL 的 from, where, select 关键字。

// 假设我们有一个产品列表
var products = new List<Product>
{
    new Product { Id = 1, Name = "Laptop", Price = 1200, Category = "Electronics" },
    new Product { Id = 2, Name = "Mouse", Price = 25, Category = "Electronics" },
    new Product { Id = 3, Name = "Keyboard", Price = 75, Category = "Electronics" },
    new Product { Id = 4, Name = "Apple", Price = 1, Category = "Fruit" }
};
// 查询语法:查找所有电子产品,并按价格升序排序
var electronicProductsQuery = from p in products
                              where p.Category == "Electronics"
                              orderby p.Price ascending
                              select p;

方法语法

使用 Lambda 表达式和扩展方法进行链式调用。

// 方法语法:实现与上面完全相同的功能
var electronicProductsMethod = products
    .Where(p => p.Category == "Electronics")
    .OrderBy(p => p.Price)
    .Select(p => p); // Select(p => p) 可以省略,默认就是选择整个元素

Lambda 表达式p => p.Category == "Electronics" 是一个简化的匿名函数,读作 “对于每个元素 p,判断它的 Category 属性是否等于 'Electronics'”。

两种语法的对比与选择

特性 查询语法 方法语法
可读性 对于复杂的查询(尤其是多表连接),更接近SQL,可读性可能更高。 对于简单的、链式的操作,非常简洁流畅。
功能 功能有限,不是所有 LINQ 操作都能用查询语法表示(Count())。 功能最全,所有标准查询操作符和自定义方法都可以使用。
混合使用 查询语法最终会编译成方法语法,所以你可以在查询语法后继续链式调用方法。 这是 LINQ 的“底层”实现方式,更灵活。

你可以根据个人喜好和团队规范选择,在实际开发中,方法语法更为常用和通用,因为它更强大、更灵活,很多开发者最终只使用方法语法,但理解查询语法对于从 SQL 转过来的开发者非常有帮助。


LINQ to Objects:操作内存中的集合

这是最常用、最基础的 LINQ 应用,适用于任何实现了 IEnumerable<T> 的集合,如 List<T>, Array, Dictionary<T> 等。

假设我们有一个 Product 类:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
}

基础查询操作

var products = GetProducts(); // 假设这是一个获取产品列表的方法
// Where: 筛选
var expensiveProducts = products.Where(p => p.Price > 100);
// Select: 投影,选择特定的属性或创建新对象
var productNames = products.Select(p => p.Name);
var productDtos = products.Select(p => new { p.Name, p.Price }); // 创建匿名类型
// OrderBy: 排序
var productsSortedByName = products.OrderBy(p => p.Name);
var productsByPriceDesc = products.OrderByDescending(p => p.Price);

聚合操作

// Count: 计数
int productCount = products.Count();
// Sum: 求和
decimal totalPrice = products.Sum(p => p.Price);
// Average: 平均值
decimal averagePrice = products.Average(p => p.Price);
// Max/Min: 最大/最小值
var mostExpensiveProduct = products.Max(p => p.Price);
var cheapestProduct = products.Min(p => p.Price);

分组操作

// GroupBy: 按类别分组
var productsByCategory = products.GroupBy(p => p.Category);
// 遍历分组结果
foreach (var group in productsByCategory)
{
    Console.WriteLine($"Category: {group.Key}");
    foreach (var product in group)
    {
        Console.WriteLine($"  - {product.Name}");
    }
}

集合操作

var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 3, 4, 5 };
// Concat: 连接 (不去重)
var concatenated = list1.Concat(list2); // { 1, 2, 3, 3, 4, 5 }
// Union: 联合 (去重)
var united = list1.Union(list2); // { 1, 2, 3, 4, 5 }
// Intersect: 交集
var intersected = list1.Intersect(list2); // { 3 }
// Except: 差集
var excepted = list1.Except(list2); // { 1, 2 }

示例:在 ASP.NET 中处理数据列表

在 ASP.NET Core MVC 的控制器中,你经常需要从数据库获取数据,然后进行筛选或排序,再传递给视图。

public class ProductController : Controller
{
    private readonly IProductService _productService; // 假设通过依赖注入获取服务
    public ProductController(IProductService productService)
    {
        _productService = productService;
    }
    public IActionResult Index(decimal? minPrice, string category)
    {
        // 从服务获取所有产品 (通常来自数据库)
        var allProducts = _productService.GetAll();
        // 使用 LINQ to Objects 在内存中进行筛选
        // 这里的 allProducts 已经是一个 List<Product> 或 IEnumerable<Product>
        var filteredProducts = allProducts.AsQueryable(); // 使用 AsQueryable 以支持后续的数据库翻译
        if (minPrice.HasValue)
        {
            filteredProducts = filteredProducts.Where(p => p.Price >= minPrice.Value);
        }
        if (!string.IsNullOrEmpty(category))
        {
            filteredProducts = filteredProducts.Where(p => p.Category == category);
        }
        // 按价格降序排序
        var finalProducts = filteredProducts.OrderByDescending(p => p.Price).ToList();
        return View(finalProducts);
    }
}

注意:在这个例子中,_productService.GetAll() 返回的是 IQueryable<T> (EF Core 的默认做法),Where, OrderBy 等操作会被翻译成 SQL 并在数据库服务器上执行,而不是在内存中,这极大地提高了性能,如果返回的是 IEnumerable<T>,那么筛选和排序会在所有数据加载到内存之后进行,效率较低。


LINQ to SQL:操作 SQL Server 数据库

重要提示:LINQ to SQL 已经被微软标记为“过时”(deprecated),对于新项目,强烈推荐使用 Entity Framework (EF) Core,但了解它有助于理解 LINQ 如何与数据库交互。

什么是 LINQ to SQL?

它是一个 O/RM(对象关系映射)工具,允许你将 SQL Server 数据库中的表直接映射到 C# 的类中,然后使用 LINQ 查询这些类,LINQ to SQL 会自动将查询翻译成 SQL 语句并执行。

步骤 1:创建数据模型 (dbml 文件)

  1. 在 Visual Studio 中,右键项目 -> 添加 -> 新建项。
  2. 选择 "LINQ to SQL 类",命名为 MyDataClasses.dbml
  3. 服务器资源管理器中,将数据库中的表拖拽到设计器中,VS 会自动创建对应的 C# 类(实体)和 DataContext

步骤 2:创建 DataContext

DataContext 是 LINQ to SQL 的核心,它负责与数据库的连接和交互。

// MyDataClasses.designer.cs 中会自动生成
[Table(Name="dbo.Products")]
public partial class Product
{
    [Column(IsPrimaryKey=true)]
    public int Id { get; set; }
    [Column]
    public string Name { get; set; }
    // ... 其他属性
}
public partial class MyDataContext : DataContext
{
    public MyDataContext(string connection) : base(connection) { }
    public Table<Product> Products { get; set; }
}

步骤 3:编写查询

string connectionString = "Your_Connection_String";
using (var db = new MyDataContext(connectionString))
{
    // 查询语法:查询价格大于100的所有产品
    var expensiveProductsQuery = from p in db.Products
                                 where p.Price > 100
                                 select p;
    // 方法语法
    var expensiveProductsMethod = db.Products.Where(p => p.Price > 100);
    // 执行查询 (ToList() 会触发 SQL 执行)
    var results = expensiveProductsMethod.ToList();
}

增、删、改、查 操作

using (var db = new MyDataContext(connectionString))
{
    // Create (增加)
    var newProduct = new Product { Name = "New Monitor", Price = 300, Category = "Electronics" };
    db.Products.InsertOnSubmit(newProduct);
    db.SubmitChanges(); // 提交事务
    // Read (查询) - 如上所示
    // Update (修改)
    var productToUpdate = db.Products.FirstOrDefault(p => p.Name == "New Monitor");
    if (productToUpdate != null)
    {
        productToUpdate.Price = 280;
        db.SubmitChanges();
    }
    // Delete (删除)
    var productToDelete = db.Products.FirstOrDefault(p => p.Name == "New Monitor");
    if (productToDelete != null)
    {
        db.Products.DeleteOnSubmit(productToDelete);
        db.SubmitChanges();
    }
}

Entity Framework (EF) Core:现代数据库访问方式

EF Core 是微软目前主推的 O/RM 框架,它支持多种数据库(SQL Server, PostgreSQL, SQLite, MySQL 等),并且性能更好,功能更强大。LINQ 是与 EF Core 交互的核心方式

为什么 EF Core 是更好的选择?

  • 跨平台:可以在 Windows, Linux, macOS 上运行。
  • 开源:社区活跃,持续更新。
  • 性能优化:包括高效的变更跟踪、批量操作等。
  • 灵活的模型配置:通过 Fluent API 或 Data Annotations 轻松配置模型关系。
  • 数据库迁移:可以方便地根据 C# 模型的变化来更新数据库架构。

使用 EF Core 进行查询

假设你已经通过 EF Core 设置好了 DbContextDbSet

// 在你的 DbContext 中
public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    // ... 其他 DbSet
}

查询语法与方法语法与 LINQ to SQL 几乎完全一样,但底层机制不同。

// 通过依赖注入获取 DbContext
public class ProductController : Controller
{
    private readonly AppDbContext _context;
    public ProductController(AppDbContext context)
    {
        _context = context;
    }
    public async Task<IActionResult> Index()
    {
        // EF Core 的 LINQ 查询返回的是 IQueryable<T>
        // 这意味着查询尚未执行,它只是一个描述
        var query = _context.Products
            .Where(p => p.Category == "Electronics")
            .OrderBy(p => p.Price);
        // 当你调用 ToList(), FirstOrDefault(), Count() 等方法时,
        // EF Core 才会将 LINQ 表达式翻译成 SQL 并发送到数据库执行
        var products = await query.ToListAsync();
        return View(products);
    }
}

示例:在 ASP.NET Core MVC 中使用 EF Core 和 LINQ

  1. 安装 NuGet 包Microsoft.EntityFrameworkCore.SqlServer Microsoft.EntityFrameworkCore.Tools Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation (可选,方便开发)

  2. 创建 DbContext 和实体

    // Product.cs
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
    // AppDbContext.cs
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
        public DbSet<Product> Products { get; set; }
    }
  3. 配置服务 (Program.cs)

    var builder = WebApplication.CreateBuilder(args);
    // 1. 添加 DbContext
    builder.Services.AddDbContext<AppDbContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
    // ... 其他服务
  4. 在控制器中使用: 如上面的 ProductController 示例。

  5. 创建数据库迁移: 在包管理器控制台中运行: Add-Migration InitialCreate Update-Database


ASP.NET 中的高级 LINQ 应用

异步查询

为了不阻塞 Web 服务器线程,所有与数据库的交互都应该是异步的。

// 推荐使用 async/await 模式
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }
    // FirstOrDefaultAsync 是异步版本
    var product = await _context.Products
                                .FirstOrDefaultAsync(m => m.Id == id);
    if (product == null)
    {
        return NotFound();
    }
    return View(product);
}

投影:只查询需要的字段

这是一个重要的性能优化技巧,如果你只需要显示产品的名称和价格,就不应该查询整个 Product 对象。

// 查询整个对象
var fullProducts = await _context.Products.ToListAsync();
// 投影:只查询 Name 和 Price,并创建一个匿名类型或 DTO
var productNamesAndPrices = await _context.Products
    .Select(p => new { p.Name, p.Price }) // 创建匿名类型
    .ToListAsync();
// 或者创建一个 DTO (Data Transfer Object)
public class ProductDto
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}
var productDtos = await _context.Products
    .Select(p => new ProductDto { Name = p.Name, Price = p.Price })
    .ToListAsync();

这样做的好处是,生成的 SQL 语句只选择了 NamePrice 两列,减少了数据传输量,提高了性能。

动态查询 (IQueryable vs IEnumerable)

这是理解 LINQ 在 EF Core 中如何工作的关键。

  • IQueryable<T>:表示一个可查询的数据源,当你对 IQueryable 调用 Where, OrderBy 时,它不会立即执行查询,而是将操作添加到一个表达式树中,只有当你调用 ToList(), FirstOrDefault() 等执行方法时,EF Core 才会遍历整个表达式树,生成最终的 SQL。
  • IEnumerable<T>:表示一个已加载到内存中的集合,当你对 IEnumerable 调用 Where, OrderBy 时,LINQ to Objects 会在内存中对已经存在的所有数据进行筛选和排序。

最佳实践

  • 在你的服务层或仓储层,方法应该返回 IQueryable<T>Task<List<T>> (在 ToListAsync() 之后),这允许调用者(通常是控制器)可以继续构建查询,将复杂的筛选逻辑放在控制器或 API 端点中。
  • 在你的控制器或 API 端点,在所有查询逻辑都构建完毕后,调用 ToListAsync()ToList()执行查询。
// 在仓储/服务层
public IQueryable<Product> GetProducts()
{
    return _context.Products; // 返回 IQueryable,延迟执行
}
// 在控制器
public async Task<IActionResult> GetFilteredProducts(decimal minPrice)
{
    // 构建查询
    var query = _productService.GetProducts().Where(p => p.Price > minPrice);
    // 执行查询
    var results = await query.ToListAsync();
    return Ok(results);
}

最佳实践与性能优化

  1. 理解延迟执行:LINQ 查询只有在需要结果时(如 foreach, ToList(), Count())才会执行,这既是优点(可以构建复杂查询),也是陷阱(可能导致意外的多次数据库查询)。

  2. 在服务层使用 IQueryable,在控制器/视图使用 IEnumerable

    • 服务层:返回 IQueryable<T>,让调用者决定如何查询。
    • 控制器:接收 IQueryable<T>,添加筛选条件,然后调用 ToListAsync() 执行查询。
    • 视图:接收 IEnumerable<T>List<T>,用于展示,不应再进行复杂的查询。
  3. 避免 N+1 查询问题: 这是一个经典的性能问题,假设你有一个 Blog 和多个 Post

    // 错误示例:N+1 查询
    var blogs = _context.Blogs.ToList();
    foreach (var blog in blogs)
    {
        // 这行代码会在 foreach 循环的每一次都执行一次数据库查询!
        var postCount = _context.Posts.Count(p => p.BlogId == blog.Id); 
        Console.WriteLine($"Blog {blog.Name} has {postCount} posts.");
    }
    // 如果有10个博客,就会执行 1 (获取所有blogs) + 10 (获取每个blog的post count) = 11 次查询。

    解决方案:使用 IncludeGroupJoin 进行预加载

    // 正确示例:使用 Include 预加载
    var blogsWithPosts = _context.Blogs
        .Include(b => b.Posts) // 一次性加载所有相关的 Posts
        .ToList();
    foreach (var blog in blogsWithPosts)
    {
        // 现在直接在内存中计算,无需再次查询数据库
        var postCount = blog.Posts.Count;
        Console.WriteLine($"Blog {blog.Name} has {postCount} posts.");
    }
    // 只执行了 2 次查询:1次获取blogs,1次获取所有posts。

总结与学习资源

  • LINQ 是一种统一的查询模式,让 C# 开发者可以用一致的语法查询各种数据源。
  • 掌握 方法语法Lambda 表达式 是现代 .NET 开发的必备技能。
  • 在 ASP.NET 应用中,LINQ 主要通过 Entity Framework Core 与数据库交互。
  • 始终优先考虑 异步操作 (async/await)。
  • 理解 IQueryableIEnumerable 的区别 是编写高性能数据访问层的关键。
  • 使用 投影预加载 (Include) 来优化性能,避免 N+1 查询问题。

学习资源

  • 微软官方文档LINQ - C# Guide - 最权威、最全面的资料。
  • Entity Framework Core 文档EF Core Docs - 学习如何在 ASP.NET 中使用 LINQ 查询数据库。
  • LINQPad:一个极好的学习工具,你可以用它即时运行和测试 LINQ 查询,并查看生成的 SQL。
  • C# in Depth (by Jon Skeet):这本书对 LINQ 的讲解非常深入透彻。
  • 视频教程:在 Pluralsight, Udemy, YouTube 等平台搜索 "C# LINQ" 或 "ASP.NET Core EF Core" 可以找到大量高质量的视频教程。