Document

深入理解TCP、UDP协议及两者的区别

GitHubhttps://github.com/JDawnF

一、TCP协议:

位于传输层, 提供可靠的字节流服务。所谓的字节流服务(Byte Stream Service) 是指, 为了方便传输, 将大块数据分割成以报文段(segment) 为单位的数据包进行管理。 而可靠的传输服务是指, 能够把数据准确可靠地传给对方。 即TCP 协议为了更容易传送大数据才把数据分割, 而且 TCP 协议能够确认数据最终是否送达到对方。所以,TCP连接相当于两根管道(一个用于服务器到客户端,一个用于客户端到服务器),管道里面数据传输是通过字节码传输,传输是有序的,每个字节都是一个一个来传输。

(1)、三次握手:握手过程中使用了 TCP 的标志(flag) —— SYN(synchronize) 和ACK(acknowledgement) 。

  • 第一次握手:建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。
  • 第二次握手:服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。
  • 第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,完成三次握手。

若在握手过程中某个阶段莫名中断, TCP 协议会再次以相同的顺序发送相同的数据包。 (2)、四次挥手:由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。先进行关闭的一方将执行主动关闭,而另一方被动关闭。

  • 客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。
  • 服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。
  • 服务器B关闭与客户端A的连接,发送一个FIN给客户端A。
  • 客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。

三次握手和四次挥手:在TCP连接中,服务器端的SYN和ACK向客户端发送是一次性发送的,而在断开连接的过程中, B端向A 端发送的ACK和FIN是分两次发送的。因为在B端接收到A端的FIN后, B端可能还有数据要传输,所以先发送ACK,等B端处理完自己的事情后就可以发送FIN断开连接了。

(3)、深入理解TCP连接:

由于TCP是全双工的,因此在每一个方向都必须单独关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这个方向上没有数据流动,一个TCP连接在接收到一个FIN后仍能发送数据。 首先进行关 闭的一方将执行主动关闭,而另一方执行被动关闭。 TCP协议的连接是全双工连接,一个TCP连接存在双向的读写通道。简单来说,是“先关读,再关写” ,总共需要4个阶段。以客户机发起关闭连接为例:1.服务器读通道关闭;2.客户端写通道关闭;3.客户端读通道关闭;4.服务器写通道关闭。 关闭行为是在发起方数据发送完毕之后,给对方发出一个FIN(finish)数据段,直到接收到对方发送的FIN,且对方收到了接收确认的ACK之后,双方的数据通信完全结束,过程中每次都需要返回确认数据段ACK。

(4)、TCP使用滑动窗口机制来进行流量控制。 建立连接时,各端分配一个缓冲区用来存储接收的数据,并将缓冲区的尺寸发送给另一端。接收方发送的确认消息中包含了自己剩余的缓冲区尺寸。剩余缓冲区空间的数量叫做窗口。其实就是建立连接的双虎互相知道彼此剩余的缓冲区大小。

(5)、拥塞控制

拥塞控制:防止过多的数据注入到网路中,这样可以使网络中的路由器或链路不至于阻塞。拥塞控制是一个全局性的过程,和流量控制不同,流量控制是点对点的控制。

1、慢开始:发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态的变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接收方的接收能力,发送窗口可能小于拥塞窗口。思路就是:不要一开始就发送大量的数据,先试探一下网络的拥塞程度,也就是说由小到大增加拥塞窗口的大小。

为了防止cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。 ssthresh的方法如下: 当cwnd < ssthresh时,开始使用慢开始算法;当cwnd > ssthresh, 改用拥塞避免算法;当cwnd = ssthresh时,慢开始与拥塞算法任意。 2.拥塞避免:

拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍,这样拥塞窗口按照线性规律缓慢增长。无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为⽆法判定,所以都当作拥塞处理),就把慢开始门限设置为出现拥塞时的发送窗口的一半,然后把拥塞窗口设置为1,执行慢开始算法:

此外,还有快速重传和快速恢复,停止-等待协议,回退N帧协议,选择重传协议等。

二、UDP协议:

无连接协议,也称透明协议,也位于传输层。

两者区别:

1) TCP提供面向连接的传输,通信前要先建立连接(三次握手机制); UDP提供无连接的传输,通信前不需要建立连接。 2) TCP提供可靠的传输(有序,无差错,不丢失,不重复); UDP提供不可靠的传输。 3) TCP面向字节流的传输,因此它能将信息分割成组,并在接收端将其重组; UDP是面向数据报的传输,没有分组开销。 4) TCP提供拥塞控制和流量控制机制; UDP不提供拥塞控制和流量控制机制。

三、长连接和短连接

HTTP的长连接和短连接本质上是TCP长连接和短连接。HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。 IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠地传递数据包,使得网络上接收端收到发送端所发出的所有包,并且顺序与发送顺序一致。TCP协议是可靠的、面向连接的。

在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。

而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
 
Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。

HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。

https://www.cnblogs.com/gotodsp/p/6366163.html

超越 Entity Framework:构建自适应、可编程的数据引擎,EF Core 源码分析

 even鹏 even鹏
 
 2025年09月13日 10:01 广东

01

 EF Core 源码分析



引言:

在 .NET Core + Entity Framework Core(EF Core) 的实际开发中,随着数据量增长和业务复杂度提升,仅靠基础的 LINQ 查询已无法满足性能需求。因此,必须深入理解并应用一系列高级优化策略,包括:

  • 避免客户端评估
  • 合理使用跟踪与非跟踪查询
  • 预加载、延迟加载与显式加载的权衡
  • 调用数据库函数(Scalar Functions)
  • 使用存储过程(Stored Procedures)
  • 原始 SQL 查询优化
  • 查询拆分与批处理
  • 索引优化与执行计划分析
  • 缓存机制集成
  • 异步查询与并发控制

本文将在前文基础上,对 EF Core 中的性能调优进行系统性、实战性的详细扩展,重点聚焦于 函数调用、存储过程、SQL 调优、执行计划分析、缓存等高级主题,力求提供可落地的最佳实践。





一、避免客户端估(Client Evaluation)—— 首要性能红线

1.1 什么是客户端评估?

当 EF Core 无法将某个 C# 表达式翻译为 SQL 时,它会将整个结果集拉取到内存中,在 .NET 运行时执行该逻辑,这称为“客户端评估”(Client Evaluation)。

示例:危险的客户端评估

var expensiveProducts = context.Products    .Where(p => p.Name.ToUpper().Contains("PHONE"))    .ToList();

上述代码中,p.Name.ToUpper() 在 EF Core 3.0+ 默认不会被翻译成 SQL,导致:

  • 先从数据库 SELECT * FROM Products
  • 再在内存中遍历每条记录执行 ToUpper().Contains

这是典型的性能灾难。





1.2 如何识别客户端评估?

启用 EF Core 日志:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){    optionsBuilder.UseSqlServer(connectionString)                  .LogTo(Console.WriteLine, LogLevel.Information)                  .EnableSensitiveDataLogging();}

日志中会出现类似警告:

The LINQ expression 'where __ef_filter__ ...' could not be translated and will be evaluated locally.




1.3 解决方案

✅ 改写为可翻译表达式

// 正确:使用 SQL 函数或忽略大小写比较var products = context.Products    .Where(p => EF.Functions.Like(p.Name"%phone%"))    .ToList();// 或使用数据库内置函数(SQL Server)var products = context.Products    .Where(p => p.Name.Contains("phone"StringComparison.OrdinalIgnoreCase))    .ToList(); // 注意:需数据库支持不区分大小写的排序规则

✅ 显式使用 AsEnumerable() 表示意图

如果你确实需要在内存中处理,应显式调用 AsEnumerable(),让开发者明确知道性能影响:

var result = context.Products    .Select(p => new { p.Id, p.Name })    .AsEnumerable()  // 从此开始在内存中执行    .Where(p => IsComplexBusinessRule(p.Name)) // 自定义方法    .ToList();

🔥 最佳实践

  • EF Core 6+ 默认禁止客户端评估(抛出异常),建议保持此设置。
  • 使用 .ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)) 强制报错。




二、非跟踪查询(No-Tracking Queries)—— 提升只读性能

2.1 什么是变更跟踪?

EF Core 默认会对查询结果进行变更跟踪(Change Tracking),以便后续调用 SaveChanges() 时检测对象是否被修改。

但这带来了额外开销:内存占用、性能损耗。





2.2 使用 AsNoTracking()

对于只读查询(如列表展示、报表),应禁用跟踪:

var products = context.Products    .AsNoTracking()    .Where(p => p.Price > 100)    .OrderBy(p => p.Name)    .ToList();




2.3 性能对比(示例)

场景跟踪查询(Tracking)非跟踪查询(NoTracking)
查询 10,000 条记录 ~800ms ~400ms
内存占用 高(含 Entry 管理)

 





2.4 全局设置(谨慎使用)

可在 OnModelCreating 中为特定实体默认关闭跟踪:

modelBuilder.Entity<Product>().HasQueryFilter(null); // 不推荐

更推荐:按需使用 AsNoTracking()





三、高效加载关联数据:Include、ThenInclude 与 Split Queries

3.1 N+1 查询问题

var products = context.Products.ToList();foreach (var p in products){    Console.WriteLine(p.Category.Name); // 每次触发一次数据库查询!}

这就是著名的 N+1 查询问题





3.2 使用 Include 预加载

var products = context.Products    .Include(p => p.Category)    .ThenInclude(c => c.ParentCategory)    .Include(p => p.Reviews)    .ToList();

生成一条包含多个 JOIN 的 SQL。





3.3 分裂查询(Split Queries)—— EF Core 5+

当关联数据较多时,单条 SQL 可能产生笛卡尔积,导致数据膨胀。

解决方案:分裂查询

var products = context.Products    .Include(p => p.Category)    .Include(p => p.Reviews)    .AsSplitQuery()  // 拆分为多条 SQL    .ToList();
  • 优点:避免数据重复,内存更小。
  • 缺点:多次数据库往返。

✅ 建议:

  • 少量关联 → 单查询(Single Query)
  • 多对多或深层嵌套 → 分裂查询(Split Query)




四、调用数据库函数(Database Functions)

EF Core 支持调用数据库内置函数,避免客户端计算。

4.1 使用 EF.Functions

字符串函数

// LIKEvar products = context.Products    .Where(p => EF.Functions.Like(p.Name"Apple%"))    .ToList();// 正则表达式(SQL Server)var matches = context.Products    .Where(p => EF.Functions.PatIndex("%[0-9]%", p.Name) > 0)    .ToList();

日期函数

// DATEADD、DATEDIFFvar recent = context.Orders    .Where(o => o.OrderDate >= EF.Functions.DateAdd("day", -7DateTime.UtcNow))    .ToList();

JSON 函数(SQL Server / PostgreSQL)

// 查询 JSON 字段var users = context.Users    .Where(u => EF.Functions.JsonValue(u.Profile"$.age") == "30")    .ToList();




4.2 映射自定义标量函数(Scalar Functions)

假设数据库有一个函数:

CREATE FUNCTION dbo.CalculateDiscount(@price DECIMAL(18,2), @rate DECIMAL(3,2))RETURNS DECIMAL(18,2)AS BEGIN    RETURN @price * (1 - @rate)END

在 EF Core 中映射:

public class AppDbContext : DbContext{    [DbFunction("CalculateDiscount", Schema = "dbo")]    public static decimal CalculateDiscount(decimal price, decimal rate)    {        throw new Exception("This method is only for use in LINQ queries");    }    protected override void OnModelCreating(ModelBuilder modelBuilder)    {        modelBuilder.HasDbFunction(typeof(AppDbContext).GetMethod(nameof(CalculateDiscount)));    }}

使用:

var products = context.Products    .Select(p => new    {        p.Name,        DiscountedPrice = AppDbContext.CalculateDiscount(p.Price0.1m)    })    .ToList();

生成 SQL:

SELECT Name, dbo.CalculateDiscount(Price, 0.1) AS DiscountedPriceFROM Products




五、使用存储过程(Stored Procedures)

5.1 何时使用存储过程?

  • 复杂事务逻辑
  • 批量操作(插入/更新/删除)
  • 高性能报表
  • 已有遗留系统集成




5.2 映射存储过程

示例:执行返回结果集的存储过程

CREATE PROCEDURE GetTopSellingProducts    @Year INT,    @Count INTASBEGIN    SELECT TOP (@Count        p.Name,         SUM(od.Quantity) AS TotalQuantity    FROM Products p    JOIN OrderDetails od ON p.Id = od.ProductId    JOIN Orders o ON od.OrderId = o.Id    WHERE YEAR(o.OrderDate) = @Year    GROUP BY p.Id, p.Name    ORDER BY TotalQuantity DESCEND

在 EF Core 中调用:

var topProducts = context.Set<TopProductDto>()    .FromSqlRaw("EXEC GetTopSellingProducts @Year={0}, @Count={1}"202310)    .ToList();

⚠️ 注意:FromSqlRaw 必须返回 DbSet 类型,且 TopProductDto 应为实体或映射类型。





5.3 使用 ExecuteSqlRaw 执行非查询

var rowsAffected = context.Database.ExecuteSqlRaw(    "UPDATE Products SET Price = Price * 1.1 WHERE CategoryId = {0}", categoryId);




5.4 异步支持

await context.Database.ExecuteSqlRawAsync("...");




六、原始 SQL 查询优化技巧

6.1 参数化查询(防止 SQL 注入)

✅ 正确:

.FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", minPrice)

❌ 错误(危险):

.FromSqlRaw($"SELECT * FROM Products WHERE Price > {minPrice}"// SQL Injection!




6.2 使用 FromSqlInterpolated(推荐)

var minPrice = 100;var products = context.Products    .FromSqlInterpolated($"SELECT * FROM Products WHERE Price > {minPrice}")    .ToList();

自动参数化,安全且易读。





6.3 动态 SQL 构建

使用 StringBuilder 或 Dapper 风格构建动态查询:

var sql = new StringBuilder("SELECT * FROM Products WHERE 1=1");var parameters = new List<object>();if (minPrice.HasValue){    sql.Append(" AND Price >= @minPrice");    parameters.Add(new SqlParameter("@minPrice", minPrice.Value));}if (!string.IsNullOrEmpty(category)){    sql.Append(" AND Category = @category");    parameters.Add(new SqlParameter("@category", category));}var products = context.Products.FromSqlRaw(sql.ToString(), parameters.ToArray()).ToList();




七、索引优化与执行计划分析

7.1 数据库索引设计

EF Core 无法替代 DBA 的工作。常见索引策略:

protected override void OnModelCreating(ModelBuilder modelBuilder){    // 单列索引    modelBuilder.Entity<Product>()        .HasIndex(p => p.Price);    // 复合索引(重要!)    modelBuilder.Entity<Order>()        .HasIndex(o => new { o.CustomerId, o.OrderDate })        .IncludeProperties(o => new { o.Total, o.Status });    // 唯一索引    modelBuilder.Entity<User>()        .HasIndex(u => u.Email)        .IsUnique();}




7.2 查看执行计划

在 SQL Server Management Studio 中:

  • 开启“显示实际执行计划”
  • 执行 EF Core 生成的 SQL
  • 分析是否存在:
    • Table Scan(应为 Index Seek)
    • Key Lookup(可考虑覆盖索引)
    • Sort / Hash Match(大数据集慢)




7.3 覆盖索引(Covering Index)

让索引包含所有查询字段,避免回表:

CREATE NONCLUSTERED INDEX IX_Orders_CustomerDateON Orders (CustomerId, OrderDate)INCLUDE (Total, Status);




八、缓存策略集成

8.1 输出缓存(HTTP 层)

适用于 API 接口:

[HttpGet][ResponseCache(Duration = 60)]public IActionResult GetProducts() { ... }




8.2 内存缓存(IMemoryCache)

public class ProductService{    private readonly IMemoryCache _cache;    private readonly AppDbContext _context;    public Product GetTopProducts(int count)    {        return _cache.GetOrCreate($"top_products_{count}", entry =>        {            entry.SlidingExpiration = TimeSpan.FromMinutes(10);            return _context.Products                .OrderByDescending(p => p.Sales)                .Take(count)                .AsNoTracking()                .ToList();        });    }}




8.3 分布式缓存(Redis)

适用于集群环境,使用 IDistributedCache





九、异步查询与并发控制

9.1 使用异步 API

var products = await context.Products    .AsNoTracking()    .Where(p => p.IsActive)    .ToListAsync();var count = await context.Products.CountAsync();
避免阻塞线程,提升吞吐量。



9.2 并发控制(乐观锁)

使用 RowVersion

public class Product{    public int Id { getset; }    public string Name { getset; }    [Timestamp]    public byte[] RowVersion { getset; }}
更新时自动检查版本,避免脏写。



十、综合调优 Checklist

项目是否已优化说明
❏ 客户端评估 ✅ 否决 使用 LogTo 检查
❏ 跟踪查询 ✅ AsNoTracking() 只读场景必用
❏ N+1 查询 ✅ Include 或 SplitQuery 避免循环查库
❏ 分页性能 ✅ 键集分页 避免 Skip(10000)
❏ 索引缺失 ✅ 执行计划分析 添加复合索引
❏ 大字段传输 ✅ 投影(Select) 避免 SELECT *
❏ 存储过程 ✅ 复杂逻辑封装 提高性能
❏ 缓存 ✅ Redis / MemoryCache 减少数据库压力
❏ 异步 ✅ ToListAsync 提升响应能力

 





在 EF Core 中实现高性能数据访问,不能仅依赖 LINQ 的便利性,而必须深入理解其底层机制,并结合数据库层面的优化手段。关键策略包括:

  • 杜绝客户端评估 —— 首要原则
  • 善用 AsNoTracking —— 提升只读性能
  • 合理加载关联数据 —— Include vs SplitQuery
  • 调用数据库函数与存储过程 —— 复杂逻辑下推
  • 使用原始 SQL 与参数化查询 —— 灵活高效
  • 索引与执行计划优化 —— 数据库层调优
  • 引入缓存机制 —— 减少数据库负载
  • 异步编程模型 —— 提升系统吞吐

通过以上综合手段,可以在保证开发效率的同时,构建出高性能、高可用的 .NET Core 数据访问层。

💡 最后建议
对于关键路径的查询,建议使用 MiniProfiler 或 EF Core Logging + SQL Profiler 进行全程监控





十一、键集分页(Keyset Pagination)—— 替代 OFFSET/FETCH 的高性能分页方案

11.1 传统分页的问题:Skip + Take

var page = context.Products    .OrderBy(p => p.Id)    .Skip((page - 1) * pageSize)    .Take(pageSize)    .ToList();

当 Skip(10000) 时,数据库仍需扫描前 10,000 条记录,性能急剧下降 —— 这称为“深度分页问题”。





11.2 键集分页原理

利用上一页最后一条记录的排序键作为下一页的起点,避免跳过大量数据。

示例:按主键分页

public List<Product> GetNextPage(int? lastId = nullint pageSize = 20){    var query = context.Products.AsNoTracking();    if (lastId.HasValue)    {        query = query.Where(p => p.Id > lastId.Value); // 关键:基于上一页末尾 ID 继续    }    return query        .OrderBy(p => p.Id)        .Take(pageSize)        .ToList();}

生成的 SQL:

SELECT TOP 20 * FROM Products WHERE Id > 1000 ORDER BY Id

✅ 优势

  • 时间复杂度 O(1),不受偏移量影响
  • 适用于无限滚动、消息流等场景

⚠️ 限制

  • 排序字段必须唯一且不可变(推荐主键或时间戳)
  • 不支持“跳转到第 N 页”
  • 数据插入可能造成“漏读”或“重复”,需业务权衡




11.3 时间戳分页(适用于日志、订单)

var lastTimestamp = new DateTime(20259101200);var nextBatch = context.Orders    .Where(o => o.CreatedAt > lastTimestamp)    .OrderBy(o => o.CreatedAt)    .ThenBy(o => o.Id)  // 防止时间重复    .Take(100)    .ToList();

🔥 建议:为 CreatedAt 字段建立复合索引 (CreatedAt, Id)





十二、批量操作优化:高效处理大量数据

EF Core 默认逐条提交,对大批量操作性能极差。

12.1 批量插入性能问题

❌ 低效方式:

foreach (var item in largeList){    context.Products.Add(item);}await context.SaveChangesAsync(); // 逐条 INSERT

每条 INSERT 都是一次网络往返,速度慢。





12.2 解决方案

✅ 方案一:使用第三方库 —— EFCore.BulkExtensions

dotnet add package EFCore.BulkExtensions
await context.BulkInsertAsync(products);await context.BulkUpdateAsync(products);await context.BulkDeleteAsync(products);await context.BulkMergeAsync(products); // Upsert

支持:

  • 批量插入/更新/删除/合并
  • 自定义批大小、事务控制
  • 支持多种数据库(SQL Server、PostgreSQL、MySQL 等)

✅ 方案二:使用原生 SQL 批量插入

using var transaction = context.Database.BeginTransaction();var dataTable = new DataTable();dataTable.Columns.Add("Name"typeof(string));dataTable.Columns.Add("Price"typeof(decimal));foreach (var p in products){    dataTable.Rows.Add(p.Name, p.Price);}using var command = context.Database.GetDbConnection().CreateCommand();command.Transaction = transaction.GetDbTransaction();command.CommandText = @"    INSERT INTO Products (Name, Price)    SELECT Name, Price FROM @TVP";// SQL Server 表值参数(TVP)var parameter = command.CreateParameter();parameter.ParameterName = "@TVP";parameter.SqlDbType = SqlDbType.Structured;parameter.TypeName = "dbo.ProductTableType"// 用户定义表类型parameter.Value = dataTable;command.Parameters.Add(parameter);await command.ExecuteNonQueryAsync();await transaction.CommitAsync();

⚠️ 注意:需提前创建 User-Defined Table Type

✅ 方案三:Dapper + 表值参数(高性能选择)

对于极致性能要求,可混合使用 Dapper:

using (var connection = new SqlConnection(connectionString)){    await connection.OpenAsync();    await connection.ExecuteAsync("BulkInsertProducts"new { Items = products },        commandType: CommandType.StoredProcedure);}




十三、查询过滤器(Query Filters)与软删除

13.1 全局查询过滤器

常用于实现软删除、多租户隔离。

protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.Entity<Product>()        .HasQueryFilter(p => !p.IsDeleted); // 自动过滤已删除记录    modelBuilder.Entity<Order>()        .HasQueryFilter(o => o.TenantId == _tenantId); // 多租户}




13.2 如何绕过过滤器?

// 绕过软删除过滤器var allProducts = context.Products    .IgnoreQueryFilters()    .Where(p => p.IsDeleted)    .ToList();




13.3 性能影响

  • 过滤器会自动添加到所有查询中,增加 WHERE 条件。
  • 必须为过滤字段建立索引,否则全表扫描。

✅ 建议:

modelBuilder.Entity<Product>()    .HasIndex(p => p.IsDeleted); // 软删除索引




十四、自定义函数映射进阶:CLR 函数与数据库函数同步

除了标量函数,还可映射表值函数(TVF)

14.1 创建表值函数(SQL Server)

CREATE FUNCTION dbo.GetProductsByCategory(@CategoryId INT)RETURNS TABLEASRETURN (    SELECT Id, Name, Price     FROM Products     WHERE CategoryId = @CategoryId);




14.2 在 EF Core 中映射

[DbFunction("GetProductsByCategory", Schema = "dbo")]public IQueryable<Product> GetProductsByCategory(int categoryId){    var categoryParam = new SqlParameter("@CategoryId", categoryId);    var result = FromExpression(() => GetProductsByCategory(categoryId));    ((IInfrastructure<DbSqlQuery>)result).Instance.SetParameterValues(new[] { categoryParam });    return result;}

或使用更现代的方式(EF Core 6+)配合 FromSqlRaw

调用:

var products = context.GetProductsByCategory(1).ToList();




十五、执行上下文优化:DbContext 池化与作用域管理

15.1 DbContext 池化(EF Core 2.0+)

减少 DbContext 创建开销:

// Program.cs / Startup.csservices.AddPooledDbContextFactory<AppDbContext>(options =>    options.UseSqlServer(connectionString));

然后通过工厂获取实例:

private readonly IDbContextFactory<AppDbContext> _contextFactory;public ProductService(IDbContextFactory<AppDbContext> contextFactory){    _contextFactory = contextFactory;}public async Task<List<Product>> GetProducts(){    await using var context = _contextFactory.CreateDbContext();    return await context.Products.ToListAsync();}

✅ 适用场景:高并发 Web API。 





十六、监控与诊断工具集成

16.1 使用 MiniProfiler 可视化查询

// 安装包dotnet add package MiniProfiler.AspNetCore.Mvcdotnet add package MiniProfiler.EntityFrameworkCore// 添加服务services.AddMiniProfiler(options => options.RouteBasePath = "/profiler");// 中间件app.UseMiniProfiler();

在页面中查看每个请求的 SQL 执行时间、次数、参数。 





16.2 使用 Application Insights 监控生产环境

services.AddApplicationInsightsTelemetry();

可捕获:

  • 查询延迟
  • 异常次数
  • 数据库依赖调用




十七、代码生成与编译时优化

17.1 使用源生成器(Source Generators)优化 DTO 映射

避免运行时反射,提升 Select 投影性能。

例如使用 AutoMapper Source Generator

[AutoMap(typeof(Product))]public partial class ProductDto{    public string Name { getset; }    public decimal Price { getset; }}

编译时生成映射代码,零运行时代价。 





十八、数据库连接层优化

18.1 连接池配置(SQL Server)

默认已启用,可通过连接字符串调整:

Server=.;Database=AppDb;Trusted_Connection=true;Pooling=true;Max Pool Size=200;Min Pool Size=10;




18.2 超时设置

optionsBuilder.UseSqlServer(connectionString, sqlOptions =>{    sqlOptions.CommandTimeout(30); // 命令超时(秒)    sqlOptions.EnableRetryOnFailure(); // 启用重试});




十九、常见反模式(Anti-Patterns)总结

反模式正确做法
ToList().Where(...) Where(...).ToList()
Count() > 0 Any()
循环中查数据库 预加载或批量查询
大对象图 SaveChanges 分批次保存
忽略异步 使用 ToListAsync 等
在 LINQ 中调用 C# 方法 提取到内存或改写为 SQL

 





二十、终极调优 checklist(生产级)

类别检查项是否完成
查询 所有查询是否使用 AsNoTracking()(只读)?
  是否避免 Skip/Take 深度分页?
  是否使用 Any() 而非 Count() > 0
  是否投影(Select)仅需字段?
加载 是否解决 N+1 问题?
  多表 JOIN 是否使用 AsSplitQuery
写入 批量操作是否使用 BulkInsert
  是否启用事务控制?
数据库 关键字段是否有索引?
  执行计划是否高效(无 Scan)?
  是否使用覆盖索引?
架构 是否使用缓存(Redis/Memory)?
  是否启用连接池与重试?
  是否监控 SQL 执行性能?

 





结语:构建高性能 EF Core 应用的五大支柱

  • 查询优化:杜绝客户端评估,合理使用 AnyWhereSelect
  • 加载策略:掌握 IncludeSplitQuery、键集分页。
  • 写入效率:借助 BulkExtensions 实现批量操作。
  • 数据库协同:索引、存储过程、函数、执行计划分析。
  • 系统可观测性:日志、监控、缓存、诊断工具集成。

EF Core 不是“银弹”,它提供了强大的抽象能力,但也要求开发者具备一定的数据库和性能调优知识。只有将 C# 逻辑 与 SQL 底层 深度结合,才能真正发挥其潜力。

📌 最终建议

  • 对关键接口进行压测(如 JMeter、k6)
  • 在生产环境开启慢查询日志
  • 定期审查执行计划
  • 建立“数据库健康检查”流程

通过持续优化,你的 .NET Core + EF Core 应用将不仅开发高效,更能稳定支撑高并发、大数据量的生产场景。





在前面两部分中,我们已系统讲解了 EF Core 的基础查询、性能优化、存储过程、批量操作、分页策略等核心内容。

现在,我们将进入更深层次的架构级优化与高级特性应用,涵盖以下前沿主题





二十一、EF Core 与 CQRS 模式集成 —— 读写分离的终极实践

21.1 什么是 CQRS?

CQRS(Command Query Responsibility Segregation)即“命令查询职责分离”,其核心思想是:

  • Command(命令):负责数据修改(增删改),走主库(写库)
  • Query(查询):负责数据读取,走从库(只读副本)

这与 EF Core 默认的“统一 DbContext”模式形成对比。 





21.2 为什么需要 CQRS?

问题CQRS 解法
主库压力大 查询分流到只读副本
复杂查询影响事务性能 独立查询模型,可自由 JOIN
实时性要求不同 命令强一致,查询最终一致

 





21.3 实现方案:双 DbContext 架构

// 写模型(Command)public class WriteDbContext : DbContext{    public WriteDbContext(DbContextOptions<WriteDbContext> options) : base(options) { }
    public DbSet<ProductProducts => Set<Product>();    // 只用于 SaveChanges}// 读模型(Query)public class ReadDbContext : DbContext{    public ReadDbContext(DbContextOptions<ReadDbContext> options) : base(options) { }    public IQueryable<ProductSummaryProductSummaries =>        from p in Products        join c in Categories on p.CategoryId equals c.Id        select new ProductSummary        {            Id = p.Id,            Name = p.Name,            CategoryName = c.Name,            StockValue = p.Price * p.Stock        };}




21.4 在服务中使用

public class ProductService{    private readonly WriteDbContext _writeContext;    private readonly ReadDbContext _readContext;    public async Task<Guid> CreateProduct(CreateProductCommand command)    {        var product = new Product { /* ... */ };        _writeContext.Products.Add(product);        await _writeContext.SaveChangesAsync();        return product.Id;    }    public async Task<ProductSummary> GetProductSummary(Guid id)    {        return await _readContext.ProductSummaries            .AsNoTracking()            .FirstOrDefaultAsync(p => p.Id == id);    }}




21.5 高级:结合事件溯源(Event Sourcing)

命令执行后发布领域事件,由后台服务更新读模型(如 ElasticSearch、物化视图),实现最终一致性

✅ 适用场景:高并发电商、金融系统、报表平台。





二十二、EF Core 与数据库迁移(Migrations)最佳实践

22.1 迁移反模式

  • 直接在生产环境运行 Update-Database
  • 忽略迁移脚本审查
  • 自动生成冗余迁移




22.2 安全迁移流程

✅ 步骤 1:生成迁移脚本

dotnet ef migrations add AddProductStatus --project Data/

✅ 步骤 2:审查 SQL 脚本

dotnet ef migrations script --from PreviousMigration --to AddProductStatus -o migration.sql

检查:

  • 是否有 LOCK 表?
  • 大表 ALTER 是否在线执行?
  • 索引创建是否带 ONLINE = ON(SQL Server)?

✅ 步骤 3:手动执行或 CI/CD 流水线部署

避免自动应用迁移,尤其是在生产环境。





22.3 数据迁移(Data Migration)

有时需在结构变更后填充数据:

migrationBuilder.Sql(@"    UPDATE Products     SET Status = 'Active'     WHERE Status IS NULL");

或使用 C# 代码迁移:

protected override void Up(MigrationBuilder migrationBuilder){    migrationBuilder.Sql("...");    // 或调用 SeedData.Seed(context);}
⚠️ 注意:代码迁移需确保能在无网络环境下运行(如离线安装包)。



二十三、EF Core 与多租户架构设计

23.1 多租户数据隔离模式

模式描述适用场景
独立数据库 每租户一个 DB 安全要求极高,成本高
共享数据库,独立 Schema 同 DB,不同 Schema 中大型 SaaS
共享数据库,共享表 + TenantId 列 所有租户共用表 轻量级 SaaS(推荐)

 





23.2 实现方式:全局查询过滤器 + 动态连接字符串

方案一:行级隔离(TenantId)

protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.Entity<Product>()        .HasQueryFilter(p => p.TenantId == _currentTenantId);}

_currentTenantId 从 IHttpContextAccessor 或 ClaimsPrincipal 获取。

方案二:动态连接字符串(按租户切换数据库)

public class TenantDbContext : DbContext{    private readonly string _tenantConnectionString;    public TenantDbContext(string tenantConnectionString)    {        _tenantConnectionString = tenantConnectionString;    }    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)    {        optionsBuilder.UseSqlServer(_tenantConnectionString);    }}
结合依赖注入工厂模式使用。



二十四、EF Core 与 NoSQL 混合架构

虽然 EF Core 主要面向关系型数据库,但现代应用常需结合 NoSQL。

24.1 典型混合架构

数据类型存储引擎原因
用户资料、订单 SQL(PostgreSQL) 强一致性、事务
日志、事件流 Kafka / MongoDB 高吞吐、写密集
全文搜索 Elasticsearch 模糊匹配、聚合
缓存会话 Redis 低延迟、临时数据

 





24.2 EF Core + Elasticsearch 示例

使用 NEST 客户端同步数据:

// 订单创建后,异步写入 ESpublic async Task<Order> CreateOrder(CreateOrderCommand command){    var order = MapToEntity(command);    _context.Orders.Add(order);    await _context.SaveChangesAsync();    // 发布事件或直接索引    await _elasticClient.IndexDocumentAsync(order);    return order;}

查询时:

  • 结构化查询 → EF Core
  • 模糊搜索 → Elasticsearch




二十五、EF Core 与微服务中的数据访问

25.1 微服务数据访问原则

  • 每个服务拥有独立数据库
  • 禁止跨服务直接访问数据库
  • 通过 API 或事件通信




25.2 EF Core 在微服务中的角色

场景使用方式
本地数据持久化 EF Core + 本地数据库
查询其他服务数据 HTTP Client / gRPC
数据同步 消息队列(RabbitMQ/Kafka)+ 事件驱动

 





25.3 避免分布式事务

不要使用 TransactionScope 跨服务。

✅ 正确做法:Saga 模式 + 补偿事务。





二十六、EF Core 高级配置技巧

26.1 自定义 Value Converters

将枚举存储为字符串:

modelBuilder.Entity<Product>()    .Property(p => p.Status)    .HasConversion(        v => v.ToString(),        v => (ProductStatus)Enum.Parse(typeof(ProductStatus), v));




26.2 使用 Owned Types(拥有的实体)

适用于值对象(Value Object):

[Owned]public class Address{    public string Street { getset; }    public string City { getset; }}public class Customer{    public int Id { getset; }    public Address BillingAddress { getset; }    public Address ShippingAddress { getset; }}
生成单表,字段命名为 BillingAddress_StreetShippingAddress_City 等。



二十七、EF Core 与测试策略

27.1 单元测试:使用 In-Memory Database

var options = new DbContextOptionsBuilder<AppDbContext>()    .UseInMemoryDatabase("TestDb")    .Options;var context = new AppDbContext(options);context.Products.Add(new Product { Name = "Test" });context.SaveChanges();var service = new ProductService(context);var result = service.GetActiveProducts();Assert.Equal(1, result.Count);

⚠️ 注意:内存数据库不支持:

  • 复杂函数(如 DateAdd
  • 存储过程
  • 分页行为可能与真实数据库不同




27.2 集成测试:使用 Testcontainers

启动真实数据库容器进行测试:

private readonly IContainer _postgres = new ContainerBuilder()    .Image("postgres:15")    .Build();[Fact]public async Task Can_Insert_Product(){    // 连接到真实 PostgreSQL 实例    using var context = new AppDbContext(_connectionString);    // 执行真实 CRUD}
保证测试环境与生产一致。



二十八、EF Core 性能基准测试(Benchmarking)

使用 BenchmarkDotNet 对比不同查询方式:

[MemoryDiagnoser]public class QueryBenchmarks{    private AppDbContext _context;    [GlobalSetup]    public void Setup() => _context = CreateContext();    [Benchmark]    public List<Product> ToList_Where() =>         _context.Products.Where(p => p.Price > 100).ToList();    [Benchmark]    public bool Any_Check() =>         _context.Products.Any(p => p.Price > 100);    [GlobalCleanup]    public void Cleanup() => _context?.Dispose();}

运行结果示例:

Method       | Mean      | Gen0   | Allocated |-------------|-----------|--------|-----------|ToList_Where | 8.12 ms   | 1.23   | 4.5 MB    |Any_Check    | 0.12 ms   | 0.01   | 0.1 KB    |
直观展示性能差异。



二十九、EF Core 社区生态与替代方案

工具特点适用场景
Dapper 轻量级 ORM,手写 SQL 高性能查询、报表
SqlKata SQL 构建器 动态查询生成
LINQ to DB 高性能 LINQ ORM 替代 EF Core
Entity Framework Plus 扩展批处理、缓存 增强 EF Core
AutoMapper DTO 映射 减少样板代码

✅ 建议:

  • 核心业务 → EF Core
  • 高频查询 → Dapper + 缓存
  • 动态 SQL → SqlKata




三十、未来展望:EF Core 8+ 新特性前瞻

30.1 EF Core 8 亮点

  • JSON 列映射增强:支持 PostgreSQL/MySQL JSON 字段直接映射为 C# 对象
  • Bulk Operations 内置支持:原生 ExecuteUpdate / ExecuteDelete
  • Filter by Owner:更灵活的拥有者实体过滤
  • Improved Many-to-Many:无需显式中间实体




30.2 示例:EF Core 8 批量更新

await context.Products    .Where(p => p.CategoryId == 1)    .ExecuteUpdateAsync(setters => setters        .SetProperty(p => p.Pricep => p.Price * 1.1m));
无需加载到内存,直接生成 UPDATE 语句。



终极总结:构建企业级数据访问层的五大层级

层级关键技术目标
L1:基础查询 LINQ、Any、Where、Select 正确性
L2:性能优化 NoTracking、SplitQuery、Keyset Pagination 响应快
L3:架构设计 CQRS、多租户、微服务 可扩展
L4:系统韧性 批量操作、重试、监控、缓存 高可用
L5:持续演进 基准测试、自动化迁移、CI/CD 可维护

 





现在,我们将进入更深层次的实战专题,聚焦于 高并发场景下的数据一致性、分布式锁、审计日志、软删除陷阱、连接复用优化、EF Core 内部机制剖析 等企业级难题,并结合真实生产案例进行讲解。





三十一、高并发下的数据竞争与乐观锁(Optimistic Concurrency)

31.1 问题:并发更新导致数据覆盖

多个用户同时读取同一条记录,修改后保存,后提交者会覆盖前者的更改。

示例场景:

  • 用户 A 读取商品库存 = 100
  • 用户 B 读取商品库存 = 100
  • A 下单 10 件 → 库存设为 90
  • B 下单 5 件 → 库存设为 95(但实际应为 85)

这就是典型的“丢失更新”问题。





31.2 解决方案:乐观并发控制

✅ 方式一:使用 RowVersion(推荐)

public class Product{    public int Id { getset; }    public string Name { getset; }    public int Stock { getset; }    [Timestamp]    public byte[] RowVersion { getset; } // 自动管理}

数据库生成 rowversion 列(SQL Server)或 timestamptz(PostgreSQL)。

更新时自动检查版本:

var product = await context.Products.FindAsync(id);product.Stock -= 10;try{    await context.SaveChangesAsync(); // 如果版本不匹配,抛出 DbUpdateConcurrencyException}catch (DbUpdateConcurrencyException){    // 处理冲突:重试 or 提示用户}

生成的 SQL:

UPDATE Products SET Stock = @p0, RowVersion = @p1 WHERE Id = @p2 AND RowVersion = @p3;

✅ 方式二:自定义并发令牌(如 LastModifiedAt)

modelBuilder.Entity<Product>()    .Property(p => p.LastModifiedAt)    .IsConcurrencyToken();
⚠️ 注意:时间戳精度可能不足,不推荐用于高频更新场景。



三十二、分布式锁与 EF Core 的协同使用

32.1 何时需要分布式锁?

当多个实例(如 Kubernetes Pod)同时运行,需确保某项操作全局唯一执行

  • 定时任务去重
  • 库存扣减防超卖
  • 文件导出避免重复触发




32.2 基于数据库的分布式锁(轻量级)

public async Task<boolTryAcquireLock(string lockKey, TimeSpan expiry){    var now = DateTime.UtcNow;    var expiresAt = now + expiry;    var existing = await context.DistributedLocks        .Where(l => l.Key == lockKey && l.ExpiresAt > now)        .FirstOrDefaultAsync();    if (existing != nullreturn false;    var newLock = new DistributedLock    {        Key = lockKey,        AcquiredAt = now,        ExpiresAt = expiresAt,        InstanceId = _instanceId    };    context.DistributedLocks.Add(newLock);    try    {        await context.SaveChangesAsync();        return true;    }    catch (DbUpdateException)    {        // 并发插入失败(唯一键冲突)        return false;    }}

表结构:

CREATE TABLE DistributedLocks (    Id INT IDENTITY(1,1PRIMARY KEY,    [Key] NVARCHAR(100UNIQUE NOT NULL,    AcquiredAt DATETIME2,    ExpiresAt DATETIME2);
✅ 优点:无需 Redis,依赖现有数据库
❌ 缺点:性能低于 Redis,适合低频场景



三十三、审计日志(Audit Logging)自动化实现

33.1 使用 SaveChanges 拦截变更

public override int SaveChanges(){    LogAuditEntries();    return base.SaveChanges();}private void LogAuditEntries(){    var entries = ChangeTracker.Entries()        .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted);    foreach (var entry in entries)    {        var auditEntry = new AuditLog        {            EntityName = entry.Entity.GetType().Name,            EntityId = entry.PrimaryKey(),            Action = entry.State.ToString(),            ChangedBy = _currentUserService.UserId,            Timestamp = DateTime.UtcNow,            OldValues = entry.State == EntityState.Modified || entry.State == EntityState.Deleted                ? JsonConvert.SerializeObject(entry.GetDatabaseValues())                : null,            NewValues = entry.State == EntityState.Added || entry.State == EntityState.Modified                ? JsonConvert.SerializeObject(entry.CurrentValues)                : null        };        AuditLogs.Add(auditEntry);    }}

🔥 建议:

  • 使用 IEntityTypeConfiguration 配置 AuditLog 实体
  • 异步写入日志表或发送到消息队列,避免阻塞主事务




三十四、软删除的深层陷阱与解决方案

34.1 陷阱一:外键引用已删除记录

// Order 引用了一个被软删除的 Customervar order = context.Orders.Include(o => o.Customer).First();// Customer 可能为 null,业务逻辑崩溃

解法:查询时也过滤关联实体

modelBuilder.Entity<Order>()    .HasOne(o => o.Customer)    .WithMany()    .HasForeignKey(o => o.CustomerId)    .OnDelete(DeleteBehavior.SetNull); // 或 Restrict// 并在 Customer 上启用查询过滤器modelBuilder.Entity<Customer>().HasQueryFilter(c => !c.IsDeleted);
这样即使 Order 存在,也无法加载已删除的 Customer。



34.2 陷阱二:唯一索引冲突

ALTER TABLE Users ADD CONSTRAINT UQ_Email UNIQUE (Email);-- 用户删除后再注册相同邮箱,违反唯一性

解法:包含 IsDeleted 的复合唯一索引

modelBuilder.Entity<User>()    .HasIndex(u => new { u.Email, u.IsDeleted })    .HasFilter("[IsDeleted] = 0"// SQL Server 过滤索引    .IsUnique();

或使用:

CREATE UNIQUE INDEX UQ_ActiveEmail ON Users (Email) WHERE IsDeleted = 0;




三十五、DbContext 连接生命周期深度控制

35.1 默认行为:依赖注入自动管理

ASP.NET Core 默认将 DbContext 注册为 Scoped,即每个请求一个实例。





35.2 手动控制连接(高级场景)

场景:跨多个 DbContext 共享事务

using var transaction = context1.Database.BeginTransaction();try{    await context1.Products.AddAsync(new Product());    await context1.SaveChangesAsync();    // 共享同一数据库连接    context2.Database.UseTransaction(transaction.GetDbTransaction());    await context2.AuditLogs.AddAsync(new AuditLog());    await context2.SaveChangesAsync();    await transaction.CommitAsync();}catch{    await transaction.RollbackAsync();    throw;}
⚠️ 要求:两个上下文必须连接到同一个数据库实例



三十六、EF Core 查询管道内部机制剖析

理解 EF Core 如何将 LINQ 转换为 SQL,有助于写出更高效的查询。

36.1 查询执行流程

  1. LINQ Expression Tree 构建

.Where(p => p.Price > 100)
  1. Expression Tree 翻译

    • 由 RelationalQueryTranslationProvider 处理
    • 转换为 SelectExpression
  2. SQL 生成

    • SqlServerSqlGenerationHelper 生成 T-SQL
    • 参数化处理
  3. 执行与结果映射

    • Shaper 将 DataReader 映射为实体




36.2 关键类图(简化)

IQueryable<T>    ↓Expression Tree (LINQ)    ↓Query Translation (EF Core)    ↓SelectExpression (Internal)    ↓SQL String + Parameters    ↓DbCommand.Execute()    ↓Object Materialization (Shaper)    ↓List<T>




36.3 如何查看生成的 SQL?

var sql = context.Products    .Where(p => p.Price > 100)    .ToQueryString(); // EF Core 5+Console.WriteLine(sql);
⚠️ 注意:ToQueryString() 不执行查询,仅生成 SQL 字符串。



三十七、EF Core 与函数式编程思想融合

虽然 C# 是面向对象语言,但可借鉴函数式思想提升数据访问代码质量。

37.1 不可变性(Immutability)

public record ProductDto(string Name, decimal Price);// 投影返回不可变对象var dtos = context.Products    .Where(p => p.IsActive)    .Select(p => new ProductDto(p.Name, p.Price))    .ToList();




37.2 函数组合(Function Composition)

Expression<Func<Product, bool>> ByCategory(int? categoryId) =>    categoryId.HasValue ? (p => p.CategoryId == categoryId) : (_ => true);Expression<Func<Product, bool>> ByPriceRange(decimal? min, decimal? max) =>    (p => (!min.HasValue || p.Price >= min) && (!max.HasValue || p.Price <= max));// 组合查询var query = context.Products.AsQueryable();if (filter.CategoryId.HasValue)    query = query.Where(ByCategory(filter.CategoryId));if (filter.MinPrice.HasValue || filter.MaxPrice.HasValue)    query = query.Where(ByPriceRange(filter.MinPrice, filter.MaxPrice));




三十八、EF Core 在 Serverless 架构中的挑战与优化

38.1 问题:冷启动与连接延迟

Serverless(如 AWS Lambda、Azure Functions)存在冷启动,首次连接数据库慢。





38.2 优化策略

策略说明
连接池保持 使用 RDS Proxy 或 Azure DB 连接池
预热函数 定期调用函数防止冷启动
DbContext 复用 在函数实例生命周期内重用(谨慎)
精简上下文 减少 OnModelCreating 复杂度

 





38.3 示例:Azure Function + EF Core

public class ProductFunction{    private static AppDbContext _context;    [FunctionName("GetProducts")]    public async Task<IActionResult> Run(        [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req)    {        if (_context == null)        {            var options = new DbContextOptionsBuilder<AppDbContext>()                .UseSqlServer(Environment.GetEnvironmentVariable("DB_CONN"))                .Options;            _context = new AppDbContext(options);        }        return new OkObjectResult(await _context.Products.ToListAsync());    }}
⚠️ 注意线程安全与内存泄漏风险。



三十九、EF Core 安全最佳实践

39.1 防止 SQL 注入

  • 始终使用参数化查询
  • 避免字符串拼接
  • 使用 FromSqlInterpolated 而非 FromSqlRaw 直接拼接




39.2 数据脱敏

// 敏感字段不应随意暴露public class User{    public string Email { getset; }
    [NotMapped]    public string SensitiveData => null// 强制隐藏}// DTO 中显式控制输出public class UserSummaryDto{    public string Name { getset; }    // 不包含 Email}




39.3 权限过滤

结合 ClaimsPrincipal 实现行级安全:

modelBuilder.Entity<Document>()    .HasQueryFilter(d => d.OwnerId == _httpContextAccessor.HttpContext.User.FindFirst("sub")?.Value);




四十、终极建议:构建可持续演进的数据访问层

原则说明
分层清晰 Domain → Application → Infrastructure
接口抽象 定义 IProductRepository,而非直接暴露 DbContext
可测试性 依赖注入 + 接口隔离
可观测性 日志、指标、追踪(OpenTelemetry)
自动化 迁移脚本审查、CI/CD 验证

 





结语:从“会用”到“精通”的跨越

我们已经走过了漫长的旅程:

  • 从最基础的 .Any() 查询,
  • 到复杂的存储过程调用与性能调优,
  • 再到 CQRS、多租户、分布式锁等架构设计,
  • 最后深入 EF Core 内部机制与函数式思想。

真正的“精通”不是记住所有 API,而是:

在正确的时间,选择正确的工具,以最小的代价,解决最本质的问题。

EF Core 是一把强大的瑞士军刀,但只有理解其边界与局限,才能在复杂系统中游刃有余。





四十一、超大规模数据集处理 —— 百万级表的生存指南

41.1 问题:单表亿级记录,传统查询失效

当 Products 表达到 1 亿行时:

  • 即使有索引,ORDER BY Id OFFSET 1000000 ROWS 仍极慢
  • 全表扫描风险剧增
  • 统计信息滞后导致执行计划错误




41.2 解决方案组合拳

✅ 方案一:分库分表(Sharding)

使用中间件如 Vitess(MySQL)、Citus(PostgreSQL)或自研路由层。

// 根据 TenantId 或 ProductId 路由到不同数据库var shardKey = product.Id % 4// 简单哈希var context = _shardFactory.CreateContext(shardKey);

✅ 方案二:读写分离 + 只读副本

// 查询走从库services.AddDbContext<ReadDbContext>(options =>    options.UseSqlServer(Configuration["SlaveConnectionString"]));// 写入走主库services.AddDbContext<WriteDbContext>(options =>    options.UseSqlServer(Configuration["MasterConnectionString"]));

✅ 方案三:物化视图(Materialized View)

预计算高频聚合:

-- PostgreSQL 示例CREATE MATERIALIZED VIEW product_sales_summary ASSELECT     p.CategoryId,    SUM(s.Quantity) as TotalSales,    AVG(s.Price) as AvgPriceFROM Products pJOIN Sales s ON p.Id = s.ProductIdGROUP BY p.CategoryId;

EF Core 中映射为只读实体:

[Keyless]public class ProductSalesSummary{    public int CategoryId { getset; }    public long TotalSales { getset; }    public decimal AvgPrice { getset; }}

⚠️ 注意:需定时刷新视图(REFRESH MATERIALIZED VIEW

✅ 方案四:冷热数据分离

  • 热数据:最近 3 个月,存 SSD 高速库
  • 冷数据:历史数据,归档至列式存储(如 ClickHouse)

查询时优先查热库,必要时合并结果。





四十二、实时流式查询与变更捕获(Change Tracking Streaming)

42.1 场景:实时监控订单状态变化

传统轮询效率低下。我们希望“数据库一变,应用立刻知道”。





42.2 技术选型

技术数据库原理
SQL Server CDC SQL Server 读取事务日志
Debezium MySQL/PostgreSQL Kafka Connect 源连接器
PostgreSQL Logical Replication PG 复制槽(Replication Slot)
EF Core + SignalR Any 应用层广播

 





42.3 示例:SQL Server CDC + Background Service

public class OrderChangeProcessor : BackgroundService{    private readonly IServiceScopeFactory _scopeFactory;    protected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        while (!stoppingToken.IsCancellationRequested)        {            using var scope = _scopeFactory.CreateScope();            var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();            // 查询 CDC 表(__$start_lsn, __$operation)            var changes = await context.Database                .ExecuteSqlRawInterpolatedAsync($@"                    SELECT * FROM cdc.fn_cdc_get_all_changes_dbo_Orders(                        @from_lsn, @to_lsn, 'all')");
            // 处理变更并通知客户端(SignalR)            foreach (var change in ParseChanges(changes))            {                await _hub.Clients.All.SendAsync("OrderUpdated", change);            }            await Task.Delay(1000, stoppingToken); // 轮询间隔        }    }}
🔥 推荐:结合 Kafka 构建事件驱动架构,解耦数据源与消费者。



四十三、EF Core 与 AI 集成:智能查询生成

43.1 愿景:用自然语言生成 LINQ 查询

用户输入:“找出上个月销量最高的电子产品”

→ 自动生成:

var topProduct = await context.Products    .Where(p => p.Category.Name == "Electronics" &&                p.Sales.Any(s => s.SaleDate >= DateTime.Now.AddMonths(-1)))    .OrderByDescending(p => p.Sales.Sum(s => s.Quantity))    .FirstOrDefaultAsync();




43.2 实现路径(PoC 级)

步骤 1:定义 DSL 描述语言

{  "entity": "Product",  "filters": [    { "field": "Category.Name", "op": "=", "value": "Electronics" },    { "field": "Sales.SaleDate", "op": ">=", "value": "last_month" }  ],  "sort": { "field": "Sales.Quantity.Sum", "dir": "desc" },  "limit": 1}

步骤 2:LLM 解析自然语言 → DSL

使用 GPT-4 或本地模型(如 Llama 3)进行语义理解:

var prompt = @"将以下自然语言转换为 JSON 查询描述:'找出上个月销量最高的电子产品'输出格式:{ entity, filters[], sort, limit }";var dslJson = await _llm.CompleteAsync(prompt);

步骤 3:DSL → Expression Tree → IQueryable

public class DslQueryEngine{    public IQueryable<TBuildQuery<T>(DslQuery dsl, IQueryable<T> source)    {        if (dsl.Filters != null)        {            foreach (var filter in dsl.Filters)            {                source = ApplyFilter(source, filter);            }        }        if (dsl.Sort != null)        {            source = ApplySort(source, dsl.Sort);        }        return dsl.Limit.HasValue ? source.Take(dsl.Limit.Value) : source;    }}
🌟 这是“自然语言 BI”的核心,正在被 Microsoft Semantic Kernel、LangChain 等框架推动。



四十四、基于 Roslyn 的 EF Core 查询分析器(代码即文档)

44.1 目标:自动检测低效 LINQ 并告警

创建一个 Roslyn Analyzer,在编译时提示:

  • ❌ .ToList().Where(...)
  • ❌ Count() > 0
  • ❌ 未使用 AsNoTracking() 的只读查询




44.2 实现步骤

  • 创建 Roslyn 分析器项目
  • 监听 InvocationExpressionSyntax
  • 匹配特定方法调用链
// 伪代码if (node.ToString().Contains("ToList().Where")){    context.ReportDiagnostic(Diagnostic.Create(        Descriptors.EfCore_UseWhereBeforeToList,        node.GetLocation()));}




44.3 成果

开发者编写:

context.Products.ToList().Where(p => p.Price > 100);

立即收到警告:

⚠️ [EF001] 应先 Where 再 ToList,避免客户端过滤。





四十五、深度定制 EF Core:编写自定义 Query Translation Plugin

45.1 场景:支持数据库特有函数

如 PostgreSQL 的 tsvector 全文搜索:

WHERE to_tsvector('english', Name) @@ to_tsquery('search & term')




45.2 步骤

1. 定义 C# 方法

public static class FullTextSearchExtensions{    [DbFunction("to_tsvector", Schema = "")]    public static string ToTsVector(string language, string text) => throw new Exception();    [DbFunction("to_tsquery", Schema = "")]    public static string ToTsQuery(string query) => throw new Exception();}

2. 注册函数映射

protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.HasDbFunction(typeof(FullTextSearchExtensions).GetMethod(nameof(ToTsVector)));    modelBuilder.HasDbFunction(typeof(FullTextSearchExtensions).GetMethod(nameof(ToTsQuery)));}

3. 使用

var results = context.Products    .Where(p => EF.Functions.ToTsVector("english", p.Name)           .IsMatch(EF.Functions.ToTsQuery("laptop & gaming")))    .ToList();




四十六、EF Core 与领域驱动设计(DDD)深度整合

46.1 聚合根(Aggregate Root)的持久化边界

// Order 是聚合根public class Order : IAggregateRoot{    public Guid Id { getprivate set; }    private readonly List<OrderItem> _items = new();    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();    public void AddItem(Product product, int quantity)    {        // 业务规则校验        if (quantity <= 0throw new DomainException("数量必须大于0");        _items.Add(new OrderItem(Id, product.Id, product.Price, quantity));    }}




46.2 EF Core 配置聚合

builder.OwnsMany(o => o.Itemsib =>{    ib.WithOwner().HasForeignKey("OrderId");    ib.Property<int>("Id").ValueGeneratedOnAdd();    ib.HasKey("Id");});




46.3 领域事件持久化

public override async Task<intSaveChangesAsync(CancellationToken cancellationToken = default){    var events = ChangeTracker.Entries<IHaveDomainEvents>()        .Select(e => e.Entity.ExtractDomainEvents())        .SelectMany(x => x)        .ToList();    var result = await base.SaveChangesAsync(cancellationToken);    // 发布事件(可持久化到 EventStore)    await _domainEventPublisher.Publish(events, cancellationToken);    return result;}




四十七、EF Core 性能调优终极 checklist(高级版)

类别检查项工具
查询 是否所有只读查询都用了 AsNoTracking() MiniProfiler
  是否避免了 N+1 EF Core Logging
  是否使用 Any() 替代 Count() > 0 Roslyn Analyzer
加载 复杂查询是否启用 AsSplitQuery() SQL Profiler
  是否为 Include 字段建立覆盖索引? SQL Execution Plan
写入 批量操作是否使用 BulkInsert BenchmarkDotNet
  是否启用乐观锁防并发冲突? DbUpdateConcurrencyException 日志
架构 是否实现读写分离? 架构图审查
  是否对大表做了分库分表? 数据量统计
  是否引入缓存层(Redis)? Redis Insights
可观测性 是否记录慢查询日志? Application Insights
  是否监控数据库连接池? Prometheus + Grafana

 





四十八、写给架构师的建议:技术选型决策树

当你面对一个新的数据访问需求时,按此流程决策:





四十九、结语:ORM 的未来不是消亡,而是进化

有人认为 “ORM 已死”,但事实是:

ORM 正在从“对象关系映射”进化为“智能数据代理”

未来的 EF Core 可能会:

  • 自动选择最佳查询策略(Pull vs Push)
  • 动态生成索引建议
  • 结合 AI 优化执行计划
  • 支持多模数据库统一访问(SQL + NoSQL)

而你,作为开发者,不应只是工具的使用者,更应成为数据生态的设计者





五十、最后的行动号召

  • 立即审查你的项目:找出至少一个 .ToList().Where() 反模式并修复。
  • 为关键接口添加性能基准测试(BenchmarkDotNet)。
  • 建立“数据库健康月度会议”,审查索引、慢查询、容量。
  • 尝试用自然语言描述一个查询,看能否用 LLM 生成 LINQ。
  • 贡献开源:为 EF Core 提交一个 Issue 或 Docs PR。

至此,我们已完成从 基础语法 → 性能调优 → 架构设计 → AI 融合 → 未来展望 的完整闭环。

这不仅是一份 EF Core 指南,更是一套现代数据访问的思维体系

知识已交付,行动在你手中。





现在,我们将进入一个前所未有的领域EF Core 的“元编程”时代 —— 通过 代码生成、编译时优化、动态模型构建、与底层 ADO.NET 的深度融合,将数据访问的控制力提升到极致。

这是一条通往 “自适应数据引擎” 的道路。





五十一、编译时 LINQ 优化:超越运行时表达式树

51.1 问题:LINQ 的运行时开销

每次 .Where(p => p.Price > 100) 都要:

  • 构建 Expression<Func<Product, bool>>
  • EF Core 翻译为 SQL
  • 缓存查询计划

虽然有缓存,但首次执行仍慢,且内存占用高。





51.2 解法:Source Generators 预生成 SQL

使用 C# Source Generator 在编译时将 LINQ 转为原生 SQL。

示例:定义一个“查询模板”

[GenerateSql]public static IQueryable<Product> GetActiveProductsWithCategory(ApplicationDbContext db){    return from p in db.Products           where p.IsActive           join c in db.Categories on p.CategoryId equals c.Id           select new Product {               Id = p.Id,               Name = p.Name,               CategoryName = c.Name           };}

Source Generator 输出:

// 自动生成private const string Sql_GetActiveProducts = @"SELECT p.Id, p.Name, c.Name as CategoryNameFROM Products pJOIN Categories c ON p.CategoryId = c.IdWHERE p.IsActive = 1";public static async Task<List<Product>> GetActiveProductsWithCategory_Raw(    ApplicationDbContext db,     CancellationToken ct = default){    using var cmd = db.Database.GetDbConnection().CreateCommand();    cmd.CommandText = Sql_GetActiveProducts;    // 参数绑定...    using var reader = await cmd.ExecuteReaderAsync(ct);    var result = new List<Product>();    while (await reader.ReadAsync(ct))    {        result.Add(new Product {            Id = reader.GetInt32("Id"),            Name = reader.GetString("Name"),            CategoryName = reader.GetString("CategoryName")        });    }    return result;}

✅ 优势:

  • 零运行时表达式树解析
  • SQL 预编译,启动更快
  • 可静态分析 SQL 安全性




五十二、动态模型构建(Dynamic Model Building)—— 无实体映射的数据访问

52.1 场景:处理未知结构的数据表

如 CMS 系统中用户自定义表单,字段在运行时才确定。





52.2 传统做法:使用 DataTable 或 ExpandoObject

var dataTable = new DataTable();// 填充数据...
但无法享受 EF Core 的 LINQ 优势。



52.3 终极解法:运行时构建 ModelBuilder

public DbContext CreateDynamicDbContext(string tableName, Dictionary<stringType> columns){    var options = new DbContextOptionsBuilder()        .UseSqlServer(_connectionString)        .Options;    return new DynamicDbContext(options, modelBuilder =>    {        var entityTypeBuilder = modelBuilder.Entity(tableName);        foreach (var (name, typein columns)        {            entityTypeBuilder.Property(type, name);        }        entityTypeBuilder.HasNoKey(); // 或动态设置主键        entityTypeBuilder.ToView(tableName); // 或 ToTable    });}public class DynamicDbContext : DbContext{    private readonly Action<ModelBuilder> _modelBuilderAction;    public DynamicDbContext(DbContextOptions options, Action<ModelBuilder> modelBuilderAction)         : base(options)    {        _modelBuilderAction = modelBuilderAction;    }    protected override void OnModelCreating(ModelBuilder modelBuilder)    {        _modelBuilderAction(modelBuilder);    }}

使用:

var columns = new Dictionary<string, Type>{    { "Id"typeof(int) },    { "Name"typeof(string) },    { "Score"typeof(decimal) }};using var context = CreateDynamicDbContext("CustomTable", columns);var data = context.Set<Dictionary<stringobject>>()    .FromSqlRaw("SELECT * FROM CustomTable")    .ToList();
🔥 这是“低代码平台”的核心能力之一。



五十三、EF Core 与 ADO.NET 的共生关系:何时绕过 ORM?

53.1 EF Core 底层仍是 ADO.NET

var connection = context.Database.GetDbConnection();using var command = connection.CreateCommand();command.CommandText = "EXEC UpdateProductStock @ProductId, @Delta";command.Parameters.Add(new SqlParameter("@ProductId", id));command.Parameters.Add(new SqlParameter("@Delta"-1));await connection.OpenAsync();await command.ExecuteNonQueryAsync();




53.2 混合编程模式(Hybrid Persistence)

public async Task<OrderResultPlaceOrder(OrderCommand command){    using var transaction = context.Database.BeginTransaction();    // 1. 使用 EF Core 创建订单(需主键返回)    var order = new Order { /* ... */ };    context.Orders.Add(order);    await context.SaveChangesAsync();    // 2. 使用 ADO.NET 批量插入订单项(高性能)    await using var cmd = context.Database.GetDbConnection().CreateCommand();    cmd.Transaction = transaction.GetDbTransaction();    cmd.CommandText = "INSERT INTO OrderItems (...) VALUES (...)";
    foreach (var item in command.Items)    {        // 添加参数并执行        await cmd.ExecuteNonQueryAsync();    }    // 3. 使用存储过程计算积分(复杂逻辑)    var points = await context.Database        .ExecuteSqlInterpolatedAsync($"            EXEC CalculateLoyaltyPoints {order.CustomerId}, {order.Total}");    await transaction.CommitAsync();    return new OrderResult(order.Id, points);}
✅ 原则用最合适的工具解决特定问题



五十四、EF Core 的“暗物质”:未文档化的高级 API

54.1 IStateManager —— 变更跟踪器的底层接口

var stateManager = context.GetInfrastructure<IStateManager>();foreach (var entry in stateManager.Entries){    Console.WriteLine($"{entry.Entity.GetType()} is {entry.EntityState}");}

可用于实现:

  • 自定义变更分析
  • 实体状态快照
  • 调试工具




54.2 IDiagnosticsLogger —— 深度监控 EF Core 内部事件

public class CustomDbLogger : IDiagnosticsLogger<DbLoggerCategory.Database.Command>{    public void Log(        EventDefinition eventDef,        LogLevel logLevel,        Func<string> messageFunc,        Exception exception,        Func<object, Exception, string> formatter)    {        if (eventDef.EventId == RelationalEventId.CommandExecuting)        {            var command = (DbCommand)eventDef.Parameters[0];            LogSlowQuery(command);        }    }}

注册:

services.AddSingleton<IDiagnosticsLogger<...>, CustomDbLogger>();




五十五、构建“自适应查询引擎” —— 根据负载动态切换策略

55.1 目标:系统自动选择最佳查询方式

public class AdaptiveQueryService{    private readonly IServiceProvider _serviceProvider;    private readonly IMetricsCollector _metrics;    public async Task<List<Product>> GetProducts(QueryCriteria criteria)    {        var load = _metrics.GetDatabaseLoad(); // CPU, Latency, QPS        if (load.IsHigh)        {            // 高负载:走缓存 + 简化查询            return await GetFromCacheOrFallback(criteria);        }        else if (criteria.IsComplex)        {            // 复杂查询:使用 SplitQuery            using var scope = _serviceProvider.CreateScope();            var ctx = scope.ServiceProvider.GetRequiredService<AppDbContext>();            return await ctx.Products                .AsSplitQuery()                .Include(p => p.Category)                .ThenInclude(c => c.Parent)                .Where(BuildFilter(criteria))                .ToListAsync();        }        else        {            // 简单查询:使用 EF Core 默认            return await _context.Products                .Where(p => p.IsActive)                .AsNoTracking()                .ToListAsync();        }    }}
🌟 这是“智能数据库代理”的雏形。



五十六、EF Core 与 WebAssembly 的奇妙组合

56.1 场景:在浏览器中运行 EF Core?

是的!使用 EntityFrameworkCore.Sqlite.Core + WASM

// Blazor WebAssemblyvar options = new DbContextOptionsBuilder<AppDbContext>()    .UseSqlite("Data Source=local.db")    .Options;using var db = new AppDbContext(options);db.Database.EnsureCreated();var products = db.Products.Where(p => p.Price < 100).ToList();




56.2 用途

  • 离线应用(PWA)
  • 客户端数据缓存
  • 教学演示(无需服务器)

⚠️ 注意:仅适用于轻量级场景,数据安全需谨慎。





五十七、EF Core 的“哲学”反思:我们真的需要对象映射吗?

57.1 DDD 的启示:聚合优于实体

我们不应过度关注“每个表对应一个类”,而应关注:

  • 行为在哪里?
  • 不变性如何保证?
  • 一致性边界在哪?




57.2 回归数据本质:DTO + 行为 + 规则

// 不再是贫血模型public record PlaceOrderCommand(    Guid CustomerId,     List<OrderItemDto> Items);public class OrderService{    public async Task<OrderResult> PlaceOrder(PlaceOrderCommand command)    {        // 1. 验证        // 2. 加载聚合        // 3. 执行领域逻辑        // 4. 持久化        // 5. 发布事件    }}

EF Core 只是持久化实现细节,而非设计核心。





五十八、给初学者的终极建议:学习路径图

✅ 关键:先学 SQL,再学 EF Core
不懂 SQL 的人,永远无法真正掌握 EF Core。



五十九、写给未来的你

当你多年后回看这段学习旅程,你会意识到:

技术的本质,不是工具的堆砌,而是对“不确定性”的管理。

  • 数据库连接可能失败 → 你设计了重试机制
  • 查询可能变慢 → 你引入了缓存与索引
  • 需求可能变化 → 你构建了可扩展的架构

你对抗的从来不是“技术难题”,而是系统的熵增

而 EF Core,只是你手中的一把剑。





六十、最后的彩蛋:用 EF Core 实现一个“迷你 ORM”

public class MiniEf<Twhere T : class{    private readonly string _connectionString;    public MiniEf(string connectionString) => _connectionString = connectionString;    public async Task<List<T>> Where(Expression<Func<T, bool>> predicate)    {        var sql = PredicateToSql(predicate); // 简化:仅支持 p => p.Name == "xxx"        using var conn = new SqlConnection(_connectionString);        using var cmd = new SqlCommand(sql, conn);        await conn.OpenAsync();        using var reader = await cmd.ExecuteReaderAsync();        return MapToEntities(reader).ToList();    }    private string PredicateToSql(Expression<Func<T, bool>> predicate)    {        // 简化实现:仅处理二元表达式        if (predicate.Body is BinaryExpression bin)        {            var left = (bin.Left as MemberExpression)?.Member.Name;            var right = (bin.Right as ConstantExpression)?.Value;            return $"SELECT * FROM {typeof(T).Name}s WHERE {left} = '{right}'";        }        throw new NotSupportedException();    }    private IEnumerable<T> MapToEntities(SqlDataReader reader)    {        var type = typeof(T);        while (reader.Read())        {            var obj = Activator.CreateInstance<T>();            foreach (var prop in type.GetProperties())            {                if (!reader.IsDBNull(prop.Name))                {                    prop.SetValue(obj, reader[prop.Name]);                }            }            yield return obj;        }    }}

使用:

var products = await new MiniEf<Product>(connStr)    .Where(p => p.Name == "Laptop");
这,就是 ORM 的起点。



终章:旅程永不结束

我们从 .Any() 开始,到“构建迷你 ORM”结束,完成了一个认知的闭环

但真正的学习,才刚刚开始。

因为:

每一个生产环境的崩溃,都是宇宙给你发的一封情书
它在说:“来吧,再深一点,再远一点,我还有更多秘密等你发现。”






posted @ 2025-09-16 09:48  从未被超越  阅读(265)  评论(0)    收藏  举报