深入理解TCP、UDP协议及两者的区别
GitHub:https://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协议,会在响应头加入这行代码:
Connection:keep-alive
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
https://www.cnblogs.com/gotodsp/p/6366163.html
超越 Entity Framework:构建自适应、可编程的数据引擎,EF Core 源码分析
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", -7, DateTime.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 BEGINRETURN @price * (1 - @rate)END
在 EF Core 中映射:
public class AppDbContext : DbContext{[]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.Price, 0.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 INTASBEGINSELECT TOP (@Count)p.Name,SUM(od.Quantity) AS TotalQuantityFROM Products pJOIN OrderDetails od ON p.Id = od.ProductIdJOIN Orders o ON od.OrderId = o.IdWHERE YEAR(o.OrderDate) = @YearGROUP BY p.Id, p.NameORDER BY TotalQuantity DESCEND
在 EF Core 中调用:
var topProducts = context.Set<TopProductDto>().FromSqlRaw("EXEC GetTopSellingProducts @Year={0}, @Count={1}", 2023, 10).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 接口:
[][]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 { get; set; }public string Name { get; set; }[]public byte[] RowVersion { get; set; }}
十、综合调优 Checklist
| 项目 | 是否已优化 | 说明 |
|---|---|---|
| ❏ 客户端评估 | ✅ 否决 | 使用 LogTo 检查 |
| ❏ 跟踪查询 | ✅ AsNoTracking() |
只读场景必用 |
| ❏ N+1 查询 | ✅ Include 或 SplitQuery |
避免循环查库 |
| ❏ 分页性能 | ✅ 键集分页 | 避免 Skip(10000) |
| ❏ 索引缺失 | ✅ 执行计划分析 | 添加复合索引 |
| ❏ 大字段传输 | ✅ 投影(Select) | 避免 SELECT * |
| ❏ 存储过程 | ✅ 复杂逻辑封装 | 提高性能 |
| ❏ 缓存 | ✅ Redis / MemoryCache | 减少数据库压力 |
| ❏ 异步 | ✅ ToListAsync |
提升响应能力 |
结论
在 EF Core 中实现高性能数据访问,不能仅依赖 LINQ 的便利性,而必须深入理解其底层机制,并结合数据库层面的优化手段。关键策略包括:
- 杜绝客户端评估 —— 首要原则
- 善用
AsNoTracking—— 提升只读性能 - 合理加载关联数据 ——
IncludevsSplitQuery - 调用数据库函数与存储过程 —— 复杂逻辑下推
- 使用原始 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 = null, int 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 ProductsWHERE Id > 1000ORDER BY Id
✅ 优势:
- 时间复杂度 O(1),不受偏移量影响
- 适用于无限滚动、消息流等场景
⚠️ 限制:
- 排序字段必须唯一且不可变(推荐主键或时间戳)
- 不支持“跳转到第 N 页”
- 数据插入可能造成“漏读”或“重复”,需业务权衡
11.3 时间戳分页(适用于日志、订单)
var lastTimestamp = new DateTime(2025, 9, 10, 12, 0, 0);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, PriceFROM ProductsWHERE CategoryId = @CategoryId);
14.2 在 EF Core 中映射
[]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:
[]public partial class ProductDto{public string Name { get; set; }public decimal Price { get; set; }}
编译时生成映射代码,零运行时代价。
十八、数据库连接层优化
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 应用的五大支柱
- 查询优化:杜绝客户端评估,合理使用
Any、Where、Select。 - 加载策略:掌握
Include、SplitQuery、键集分页。 - 写入效率:借助
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<Product> Products => Set<Product>();// 只用于 SaveChanges}// 读模型(Query)public class ReadDbContext : DbContext{public ReadDbContext(DbContextOptions<ReadDbContext> options) : base(options) { }public IQueryable<ProductSummary> ProductSummaries =>from p in Productsjoin c in Categories on p.CategoryId equals c.Idselect 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 ProductsSET 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):
生成单表,字段命名为[]public class Address{public string Street { get; set; }public string City { get; set; }}public class Customer{public int Id { get; set; }public Address BillingAddress { get; set; }public Address ShippingAddress { get; set; }}
BillingAddress_Street、ShippingAddress_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();[]public async Task Can_Insert_Product(){// 连接到真实 PostgreSQL 实例using var context = new AppDbContext(_connectionString);// 执行真实 CRUD}
二十八、EF Core 性能基准测试(Benchmarking)
使用 BenchmarkDotNet 对比不同查询方式:
[]public class QueryBenchmarks{private AppDbContext _context;[]public void Setup() => _context = CreateContext();[]public List<Product> ToList_Where() =>_context.Products.Where(p => p.Price > 100).ToList();[]public bool Any_Check() =>_context.Products.Any(p => p.Price > 100);[]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.Price, p => 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 { get; set; }public string Name { get; set; }public int Stock { get; set; }[]public byte[] RowVersion { get; set; } // 自动管理}
数据库生成 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 ProductsSET Stock = , RowVersion =WHERE Id = AND RowVersion = ;
✅ 方式二:自定义并发令牌(如 LastModifiedAt)
⚠️ 注意:时间戳精度可能不足,不推荐用于高频更新场景。modelBuilder.Entity<Product>().Property(p => p.LastModifiedAt).IsConcurrencyToken();
三十二、分布式锁与 EF Core 的协同使用
32.1 何时需要分布式锁?
当多个实例(如 Kubernetes Pod)同时运行,需确保某项操作全局唯一执行:
- 定时任务去重
- 库存扣减防超卖
- 文件导出避免重复触发
32.2 基于数据库的分布式锁(轻量级)
public async Task<bool> TryAcquireLock(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 != null) return 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;}}
表结构:
✅ 优点:无需 Redis,依赖现有数据库CREATE TABLE DistributedLocks (Id INT IDENTITY(1,1) PRIMARY KEY,[Key] NVARCHAR(100) UNIQUE NOT NULL,AcquiredAt DATETIME2,ExpiresAt DATETIME2);
❌ 缺点:性能低于 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,业务逻辑崩溃
解法:查询时也过滤关联实体
这样即使 Order 存在,也无法加载已删除的 Customer。modelBuilder.Entity<Order>().HasOne(o => o.Customer).WithMany().HasForeignKey(o => o.CustomerId).OnDelete(DeleteBehavior.SetNull); // 或 Restrict// 并在 Customer 上启用查询过滤器modelBuilder.Entity<Customer>().HasQueryFilter(c => !c.IsDeleted);
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 查询执行流程
-
LINQ Expression Tree 构建
.Where(p => p.Price > 100)
-
Expression Tree 翻译
- 由
RelationalQueryTranslationProvider处理 - 转换为
SelectExpression -
SQL 生成
SqlServerSqlGenerationHelper生成 T-SQL- 参数化处理
-
执行与结果映射
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;[]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 { get; set; }[]public string SensitiveData => null; // 强制隐藏}// DTO 中显式控制输出public class UserSummaryDto{public string Name { get; set; }// 不包含 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 ASSELECTp.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 中映射为只读实体:
[]public class ProductSalesSummary{public int CategoryId { get; set; }public long TotalSales { get; set; }public decimal AvgPrice { get; set; }}
⚠️ 注意:需定时刷新视图(
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
🔥 推荐:结合 Kafka 构建事件驱动架构,解耦数据源与消费者。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); // 轮询间隔}}}
四十三、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
🌟 这是“自然语言 BI”的核心,正在被 Microsoft Semantic Kernel、LangChain 等框架推动。public class DslQueryEngine{public IQueryable<T> BuildQuery<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;}}
四十四、基于 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{[]public static string ToTsVector(string language, string text) => throw new Exception();[]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 { get; private set; }private readonly List<OrderItem> _items = new();public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();public void AddItem(Product product, int quantity){// 业务规则校验if (quantity <= 0) throw new DomainException("数量必须大于0");_items.Add(new OrderItem(Id, product.Id, product.Price, quantity));}}
46.2 EF Core 配置聚合
builder.OwnsMany(o => o.Items, ib =>{ib.WithOwner().HasForeignKey("OrderId");ib.Property<int>("Id").ValueGeneratedOnAdd();ib.HasKey("Id");});
46.3 领域事件持久化
public override async Task<int> SaveChangesAsync(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。
示例:定义一个“查询模板”
[]public static IQueryable<Product> GetActiveProductsWithCategory(ApplicationDbContext db){return from p in db.Productswhere p.IsActivejoin c in db.Categories on p.CategoryId equals c.Idselect 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
但无法享受 EF Core 的 LINQ 优势。var dataTable = new DataTable();// 填充数据...
52.3 终极解法:运行时构建 ModelBuilder
public DbContext CreateDynamicDbContext(string tableName, Dictionary<string, Type> columns){var options = new DbContextOptionsBuilder().UseSqlServer(_connectionString).Options;return new DynamicDbContext(options, modelBuilder =>{var entityTypeBuilder = modelBuilder.Entity(tableName);foreach (var (name, type) in 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<string, object>>().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<OrderResult> PlaceOrder(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, QPSif (load.IsHigh){// 高负载:走缓存 + 简化查询return await GetFromCacheOrFallback(criteria);}else if (criteria.IsComplex){// 复杂查询:使用 SplitQueryusing 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。
五十九、写给未来的你
当你多年后回看这段学习旅程,你会意识到:
技术的本质,不是工具的堆砌,而是对“不确定性”的管理。
- 数据库连接可能失败 → 你设计了重试机制
- 查询可能变慢 → 你引入了缓存与索引
- 需求可能变化 → 你构建了可扩展的架构
你对抗的从来不是“技术难题”,而是系统的熵增。
而 EF Core,只是你手中的一把剑。
六十、最后的彩蛋:用 EF Core 实现一个“迷你 ORM”
public class MiniEf<T> where 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;}}}
使用:
这,就是 ORM 的起点。var products = await new MiniEf<Product>(connStr).Where(p => p.Name == "Laptop");
终章:旅程永不结束
我们从 .Any() 开始,到“构建迷你 ORM”结束,完成了一个认知的闭环。
但真正的学习,才刚刚开始。
因为:
每一个生产环境的崩溃,都是宇宙给你发的一封情书。
它在说:“来吧,再深一点,再远一点,我还有更多秘密等你发现。”
完

浙公网安备 33010602011771号