Entity Framework Core 迁移与优化
Entity Framework Core迁移与性能优化详解:从Code First建模到跨数据库Provider的完整指南。
Entity-Framework-Core迁移与优化
前置知识
- 反射与特性应用:建议先完成前一篇的学习
学习目标
- 掌握「1. 历史动机与发展脉络」的核心机制、典型用法与常见陷阱
- 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
- 掌握「3. 理论推导与原理解析」的核心机制、典型用法与常见陷阱
- 掌握「4. 代码示例」的核心机制、典型用法与常见陷阱
- 掌握「5. 对比分析」的核心机制、典型用法与常见陷阱
“对象关系映射(ORM)是开发者的舒适区,但舒适区往往是性能黑洞。” —— Jimmy Bogard, Architecting with Entity Framework Core
1. 历史动机与发展脉络
1.1 Entity Framework 1.0(2008):ADO.NET Entity Framework
EF1 诞生于 .NET Framework 3.5 SP1,是对 ADO.NET 数据集(DataSet)的升级。设计目标是将关系数据抽象为概念模型(Conceptual Model),通过 EDM(Entity Data Model)描述:
<!-- EDM 的 CSDL(Conceptual Schema Definition Language) -->
<Schema xmlns="http://schemas.microsoft.com/ado/2006/04/edm">
<EntityContainer Name="BlogModel">
<EntitySet Name="Blogs" EntityType="BlogModel.Blog" />
</EntityContainer>
<EntityType Name="Blog">
<Key>
<PropertyRef Name="BlogId" />
</Key>
<Property Name="BlogId" Type="Int32" Nullable="false" />
<Property Name="Url" Type="String" Nullable="false" />
</EntityType>
</Schema>
EF1 的局限:
- XML 配置冗长:EDMX 文件数千行,难以维护。
- 性能较差:Entity SQL 翻译、对象物化开销大。
- API 设计不一致:
ObjectSet<T>、ObjectQuery<T>与 LINQ 混用。
1.2 Entity Framework 4.0(2010):Code First 与 DbContext
EF4 引入 Code First 风格,Poco 实体 + Fluent API 配置,并简化 DbContext API:
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class BlogContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithRequired(p => p.Blog)
.HasForeignKey(p => p.BlogId);
}
}
virtual 属性启用懒加载代理(lazy loading proxy),EF4 动态生成代理类,首次访问导航属性时查询数据库。
1.3 Entity Framework 6(2013):成熟期
EF6 是 .NET Framework 上的最后版本,引入:
- Async/Await 支持:
ToListAsync、SaveChangesAsync。 - Code First Migrations:自动生成迁移类。
- Interceptors:拦截 SQL 执行(
IDbCommandInterceptor)。 - Stored Procedure 映射:实体到存储过程的映射。
- 枚举与空间类型支持。
EF6 性能瓶颈:
- 代理类反射开销:每次实体物化涉及反射。
- ChangeTracker 全表扫描:大量实体时检测变更缓慢。
- SQL 翻译效率低:复杂 LINQ 翻译为低效 SQL。
1.4 Entity Framework Core 1.0(2016):全新重写
.NET Core 推出之际,EF 团队选择完全重写,发布 EF Core 1.0:
- 跨平台:基于 .NET Core,支持 Linux、macOS。
- 轻量与模块化:核心包 + Provider 包,可按需引入。
- DI 友好:
DbContext通过IServiceCollection.AddDbContext注册。 - 性能优化:编译期模型缓存、
IModelCacheKeyFactory。 - LINQ 重写:基于 Roslyn 的全新查询翻译。
EF Core 1.0 缺失特性(后续补齐):
- 懒加载(1.0 不支持,2.1 才引入)。
- 复杂类型(Owned Entity 在 2.0 引入)。
- GroupBy 翻译(2.1 部分支持,3.0 完全支持)。
1.5 EF Core 2.0(2017):Owned Entity 与懒加载
EF Core 2.0 引入:
- Owned Entity:值对象映射(如
Person.Address)。 - Table Spliting:多个实体共享同一张表。
- Query Types:只读查询(EF Core 3.0 改为
KeylessEntityType)。
EF Core 2.1 引入懒加载:
services.AddDbContext<BlogContext>(options =>
options.UseSqlServer(connectionString)
.UseLazyLoadingProxies());
public class Blog
{
public int BlogId { get; set; }
public virtual ICollection<Post> Posts { get; set; } // virtual 启用懒加载
}
1.6 EF Core 3.0(2019):LINQ 翻译重写
EF Core 3.0 完全重写 LINQ 翻译管线,从 SQL 生成器改为基于 QueryExpression 的语法树转换:
LINQ Expression
↓
QueryExpression (SQL Tree)
↓
RelationalCommand (SQL String + Parameters)
↓
DbDataReader (ADO.NET)
↓
Entity Materializer (Expression.Compile)
↓
Tracked Entity
3.0 移除旧客户端评估(client-side evaluation):所有 LINQ 必须翻译为 SQL,否则抛出异常。这避免了开发期看似正确、生产环境性能灾难的”客户端过滤”。
1.7 EF Core 5.0(2020):成熟期
EF Core 5.0 引入:
- 多对多关系:自动生成中间表,无需显式实体。
- TPT 继承映射:每类型一张表(Table per Type)。
- 事件:
DbContext.SavingChanges、SavedChanges事件。 - 拆分查询(Split Query):
AsSplitQuery()解决 Cartesian Explosion。 - 索引属性:
IndexAttribute配置索引。
1.8 EF Core 6.0(2021):性能优化
EF Core 6.0 LTS 版本大幅优化性能:
- 编译模型(Compiled Model):编译期生成
IModel,减少启动时间 60%+。 - 预编译查询:
EF.CompileQuery缓存查询委托。 SaveChanges批处理:批量 INSERT/UPDATE/DELETE,减少网络往返。AsNoTrackingWithIdentityResolution:避免无跟踪查询的重复实体。
1.9 EF Core 7.0(2022):JSON 列与批量操作
EF Core 7.0 引入:
- JSON 列映射:
OwnsOne(x => x.Contact, builder => builder.ToJson()),支持嵌套 JSON 文档。 - 批量 UPDATE/DELETE:
ExecuteUpdate、ExecuteDelete直接翻译为 SQL,无需加载实体。 - TPC 继承映射:Table per Concrete type,适合深继承树。
- 自定义约定:
IModelCustomizer、IModelFinalizingConvention。
1.10 EF Core 8.0(2023):.NET 8 LTS 同步
EF Core 8.0 引入:
- 原始 SQL 复杂类型:
SqlQuery<T>返回任意类型。 - 复杂类型(Complex Types):值对象无主键,类似 Owned Entity 但不维护身份。
- HIERARCHYID(SQL Server):层次结构数据类型支持。
PrimitiveCollection:原生集合类型映射(如List<int>映射为 JSON 数组)。- 惰性加载代理改进:性能优化。
1.11 EF Core 9.0(2024):性能与 AOT
EF Core 9.0 引入:
- 预编译查询优化:减少运行时反射。
HasIndex改进:支持复合索引、过滤索引。- AOT 友好:减少
Reflection.Emit使用,支持 NativeAOT。 - Azure Cosmos DB for NoSQL 改进:分区键、向量搜索。
1.12 EF Core 10.0(2025):随 .NET 10 LTS 发布
EF Core 10.0 仅支持 .NET 10(不再向下兼容 .NET Framework),LTS 支持至 2028-11,主要引入:
- 命名查询过滤器(Named Query Filters):同一实体可挂多个具名全局过滤器并按名禁用(软删除、多租户可独立开关)。
LeftJoin/RightJoin运算符:直接翻译 .NET 10 新增的一等 LINQ 连接运算符,替代SelectMany + GroupJoin + DefaultIfEmpty组合。- 复杂类型增强:支持可选(可空)复杂类型、struct 复杂类型与映射到 JSON 列(配合 SQL Server 2025 原生
json类型)。 ExecuteUpdateAsync支持普通 lambda:不再强制表达式树,可条件化动态构建更新列。- 向量搜索正式化:SQL Server 2025 / Azure SQL 的
vector类型与VECTOR_DISTANCE一等支持,Cosmos DB 全文与混合检索增强。
1.13 演进时间线
| 时间 | 版本 | 关键里程碑 |
|---|---|---|
| 2008 | EF 1.0 | ADO.NET Entity Framework、EDMX |
| 2010 | EF 4.0 | Code First、DbContext API |
| 2013 | EF 6 | Async、Migrations、成熟期 |
| 2016 | EF Core 1.0 | 全新重写,跨平台 |
| 2017 | EF Core 2.0 | Owned Entity、Query Types |
| 2018 | EF Core 2.1 | 懒加载代理 |
| 2019 | EF Core 3.0 | LINQ 翻译重写、移除客户端评估 |
| 2020 | EF Core 5.0 | 多对多、TPT、Split Query |
| 2021 | EF Core 6.0 LTS | 编译模型、批处理 |
| 2022 | EF Core 7.0 | JSON 列、ExecuteUpdate/Delete |
| 2023 | EF Core 8.0 LTS | Complex Types、原生集合 |
| 2024 | EF Core 9.0 | AOT 改进、Cosmos DB 向量搜索 |
| 2025 | EF Core 10.0 LTS | 命名查询过滤器、LeftJoin/RightJoin、复杂类型 JSON 映射 |
2. 形式化定义
2.1 实体模型的形式化定义
EF Core 的实体模型(Model)是一个六元组:
其中:
- :实体类型集合(Entity Types)。
- :关系集合(Relationships),。
- :属性集合(Properties),。
- :外键集合(Foreign Keys),。
- :索引集合(Indexes),。
- :配置集合(Configurations)。
2.1.1 实体类型的形式化定义
实体类型 是一个四元组:
其中:
- :类型名(如
Blog)。 - :基类型(继承映射)。
- :属性集合。
- :主键属性集合。
2.2 关系的形式化定义
EF Core 中两个实体 的关系 定义为:
其中 表示多重性。
四种基本关系:
- 一对一(One-to-One):
- 一对多(One-to-Many):
- 多对一(Many-to-One):
- 多对多(Many-to-Many):
EF Core 5+ 自动生成多对多中间表:
public class Post
{
public int PostId { get; set; }
public List<Tag> Tags { get; set; }
}
public class Tag
{
public int TagId { get; set; }
public List<Post> Posts { get; set; }
}
// EF Core 自动创建 PostTag 中间表
2.3 ChangeTracker 的状态机
EF Core 的 ChangeTracker 维护实体的状态,定义为五元组状态机:
状态转移:
stateDiagram-v2
[*] --> Detached
Detached --> Added: Add()
Added --> Unchanged: SaveChanges()
Unchanged --> Modified: Update()
Modified --> Unchanged: SaveChanges()
Unchanged --> Deleted: Delete()
Modified --> Deleted: Delete()
Deleted --> Detached: SaveChanges()
Detached --> Detached: Detach()
形式化的状态转移函数 :
| 当前状态 | Add() | Update() | Delete() | Detach() | SaveChanges() |
|---|---|---|---|---|---|
| Detached | Added | Modified | Deleted | Detached | Detached |
| Unchanged | Added | Modified | Deleted | Detached | Unchanged |
| Added | Added | Modified | Deleted | Detached | Unchanged |
| Modified | Added | Modified | Deleted | Detached | Unchanged |
| Deleted | Added | Modified | Deleted | Detached | Detached |
2.4 查询翻译的形式化语义
EF Core 将 LINQ 查询翻译为 SQL,可形式化为函数:
查询翻译管线:
- LINQ Expression Tree (C# 编译器生成)
- QueryExpression (EF Core 内部表示)
- SelectExpression (SQL SELECT 树)
- ShapedQueryExpression (实体物化器)
- RelationalCommand (SQL 字符串 + 参数)
- DbDataReader → Entity Materializer → T
2.4.1 查询翻译示例
var activeBlogs = context.Blogs
.Where(b => b.IsActive && b.Posts.Count > 5)
.OrderBy(b => b.Name)
.Select(b => new { b.Name, PostCount = b.Posts.Count })
.ToListAsync();
翻译为 SQL:
SELECT b.Name, (
SELECT COUNT(*) FROM Posts p WHERE p.BlogId = b.BlogId
) AS PostCount
FROM Blogs AS b
WHERE b.IsActive AND (
SELECT COUNT(*) FROM Posts p WHERE p.BlogId = b.BlogId
) > 5
ORDER BY b.Name;
2.4.2 查询缓存
EF Core 缓存翻译结果,避免重复翻译:
首次执行查询时翻译并缓存,后续执行相同结构查询直接复用缓存。缓存键基于表达式树的结构(非值)。
2.5 迁移的形式化模型
Migration 是数据库模式演进的原子单元:
迁移序列 满足:
__EFMigrationsHistory 表记录已应用迁移:
CREATE TABLE __EFMigrationsHistory (
MigrationId NVARCHAR(150) NOT NULL,
ProductVersion NVARCHAR(32) NOT NULL,
CONSTRAINT PK_EFMigrationsHistory PRIMARY KEY (MigrationId)
);
3. 理论推导与原理解析
3.1 N+1 查询问题的形式化分析
N+1 查询是 ORM 最经典的反模式。形式化地,若查询实体 的 条记录,每条记录关联 的若干记录:
- N+1 模式:1 次查询 (返回 条) + 次查询 (每条 关联一次)。
- 总查询次数:
- 总网络往返:
解决方案 1:Eager Loading(Include)
var blogs = context.Blogs.Include(b => b.Posts).ToList();
EF Core 翻译为 LEFT JOIN:
SELECT b.BlogId, b.Url, p.PostId, p.Title
FROM Blogs AS b
LEFT JOIN Posts AS p ON b.BlogId = p.BlogId;
返回 行(,取决于关联数)。Cartesian Explosion 问题:若 平均 条关联,返回 行,重复 数据 次。
解决方案 2:Split Query(AsSplitQuery)
var blogs = context.Blogs
.Include(b => b.Posts)
.AsSplitQuery()
.ToList();
EF Core 翻译为多个查询:
-- Query 1
SELECT b.BlogId, b.Url FROM Blogs AS b;
-- Query 2
SELECT p.PostId, p.Title, b.BlogId
FROM Blogs AS b
INNER JOIN Posts AS p ON b.BlogId = p.BlogId;
总查询次数:2(与 无关)。避免 Cartesian Explosion,但增加网络往返。
解决方案 3:Explicit Loading
var blogs = context.Blogs.ToList(); // 1 次
var blogIds = blogs.Select(b => b.BlogId).ToList();
var posts = context.Posts.Where(p => blogIds.Contains(p.BlogId)).ToList(); // 1 次
总查询次数:2,性能最优。
3.2 性能模型
3.2.1 查询执行时间模型
EF Core 查询执行时间:
其中:
- :LINQ → SQL 翻译时间(缓存命中时 < 1 μs,未命中时 100-500 μs)。
- :数据库执行 SQL 时间(取决于 SQL 复杂度与索引)。
- :实体物化时间(每实体约 1-5 μs)。
- :ChangeTracker 注册时间(每实体约 0.5-2 μs)。
实测对比(.NET 8, EF Core 8, SQL Server 2022, 1000 行查询):
| 方案 | 总时间(ms) | 翻译 | SQL 执行 | 物化 | 跟踪 |
|---|---|---|---|---|---|
| 默认 | 85 | 0.1 | 12 | 45 | 28 |
| AsNoTracking | 55 | 0.1 | 12 | 43 | 0 |
| AsNoTrackingWithIdentityResolution | 58 | 0.1 | 12 | 43 | 3 |
| CompiledQuery + AsNoTracking | 54 | 0.01 | 12 | 42 | 0 |
| Dapper | 32 | - | 12 | 20 | - |
3.2.2 SaveChanges 性能模型
SaveChanges 执行时间:
EF Core 6+ 的批处理优化:将多个 INSERT/UPDATE/DELETE 合并为单次 DbCommand,减少网络往返。
| 优化 | 1000 条 INSERT 时间(ms) | 网络往返次数 |
|---|---|---|
| 默认(每条单独) | 8500 | 1000 |
| EF Core 6 批处理 | 120 | 1-10 |
BulkExtensions | 80 | 1(COPY) |
| 原生 SqlBulkCopy | 35 | 1 |
3.3 ChangeTracker 的变更检测算法
SaveChanges 调用 DetectChanges,扫描所有 tracked 实体比较当前值与原始值:
Algorithm: DetectChanges
Input: ChangeTracker
Output: Updated state for each entity
1. FOR each entity IN ChangeTracker.Entries DO:
2. IF entity.State == Unchanged THEN:
3. FOR each property IN entity.Properties DO:
4. IF !Equals(property.CurrentValue, property.OriginalValue) THEN:
5. entity.State = Modified
6. property.IsModified = true
7. END IF
8. END FOR
9. END IF
10. END FOR
时间复杂度:, 为 tracked 实体数, 为平均属性数。
优化策略:
AutoDetectChangesEnabled = false:禁用自动检测,手动调用DetectChanges。AsNoTracking查询:不进入 ChangeTracker。ChangeTracker.QueryTrackingBehavior = NoTracking:DbContext 默认不跟踪。- 小批量提交:每 100-1000 条
SaveChanges一次,避免 ChangeTracker 膨胀。
3.4 编译模型的性能优化
EF Core 6 引入编译模型(Compiled Model),在编译期生成 IModel 实例,避免运行时反射构建:
# 生成编译模型
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels
生成代码:
[DbContext(typeof(BlogContext))]
public class BlogContextModel : RuntimeModel
{
private static BlogContextModel _instance;
public static IModel Instance => _instance ??= new BlogContextModel();
public BlogContextModel()
{
Initialize();
OnModelCreating();
}
partial void Initialize();
partial void OnModelCreating();
public override RuntimeEntityEntityType BlogEntity => _blogEntity.Value;
private readonly Lazy<RuntimeEntityType> _blogEntity = new(() =>
{
var entity = new RuntimeEntityType("Blog", typeof(Blog), ...);
entity.AddProperty("BlogId", typeof(int), ...);
entity.AddProperty("Url", typeof(string), ...);
return entity;
});
}
启动时间对比:
| 方案 | DbContext 首次查询(ms) |
|---|---|
| 默认(运行时构建) | 850 |
| 编译模型 | 320 |
| 编译模型 + CompiledQuery | 280 |
3.5 事务与并发控制
3.5.1 默认事务
SaveChanges 默认在事务中执行:
using var transaction = await context.Database.BeginTransactionAsync();
try
{
await context.SaveChangesAsync(); // 内部已开事务
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
隔离级别默认 READ COMMITTED(SQL Server)。
3.5.2 乐观并发
EF Core 通过 [Timestamp](RowVersion)实现乐观并发:
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
UPDATE 时 EF Core 自动添加 RowVersion 检查:
UPDATE Blogs
SET Url = @p1
WHERE BlogId = @p0 AND RowVersion = @p2;
-- 若影响 0 行,抛出 DbUpdateConcurrencyException
3.5.3 悲观并发
显式锁:
using var transaction = await context.Database.BeginTransactionAsync();
// SQL Server: WITH (UPDLOCK)
var blog = await context.Blogs
.FromSqlRaw("SELECT * FROM Blogs WITH (UPDLOCK) WHERE BlogId = {0}", id)
.FirstAsync();
blog.Url = "new-url";
await context.SaveChangesAsync();
await transaction.CommitAsync();
4. 代码示例
4.1 Code First 模型配置(C# 12+)
// C# 12+ / .NET 8+: Code First 模型配置
public class Blog
{
public int BlogId { get; set; }
[Required]
[MaxLength(200)]
public string Url { get; set; }
[Column(TypeName = "datetime2")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public List<Post> Posts { get; set; } = new();
public BlogMetadata Metadata { get; set; } // Owned Entity
}
public class Post
{
public int PostId { get; set; }
[Required]
[MaxLength(500)]
public string Title { get; set; }
public string Content { get; set; }
public DateTime PublishedAt { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
public List<Tag> Tags { get; set; } = new(); // 多对多
}
public class Tag
{
public int TagId { get; set; }
public string Name { get; set; }
public List<Post> Posts { get; set; } = new();
}
public class BlogMetadata // Owned Entity
{
public int Views { get; set; }
public string Owner { get; set; }
}
public class BlogContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
public BlogContext(DbContextOptions<BlogContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Fluent API 配置
modelBuilder.Entity<Blog>(b =>
{
b.HasKey(x => x.BlogId);
b.Property(x => x.Url).IsRequired().HasMaxLength(200);
b.HasIndex(x => x.Url).IsUnique();
b.OwnsOne(x => x.Metadata, m =>
{
m.Property(p => p.Owner).HasMaxLength(100);
});
b.HasMany(x => x.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<Post>(p =>
{
p.HasKey(x => x.PostId);
p.HasIndex(x => new { x.BlogId, x.PublishedAt });
p.HasMany(x => x.Tags)
.WithMany(t => t.Posts)
.UsingEntity(j => j.ToTable("PostTags"));
});
// 全局查询过滤器(多租户)
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == _tenantId);
// 精度配置(decimal)
modelBuilder.Entity<Order>()
.Property(o => o.Amount)
.HasPrecision(18, 2);
// TPC 继承映射
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
}
}
4.2 迁移命令
# 创建迁移
dotnet ef migrations add InitialCreate
# 应用迁移到数据库
dotnet ef database update
# 撤销最后一次迁移(未应用前)
dotnet ef migrations remove
# 生成 SQL 脚本(不应用)
dotnet ef migrations script --output migration.sql
# 生成 idempotent 脚本(多次执行不报错)
dotnet ef migrations script --idempotent --output migration.sql
# 反向工程(Database First)
dotnet ef dbcontext scaffold "Server=.;Database=Blog;Trusted_Connection=True" \
Microsoft.EntityFrameworkCore.SqlServer \
--output-dir Models \
--context-dir Data \
--context BlogContext \
--use-database-names \
--no-onconfiguring
# 删除数据库
dotnet ef database drop --force
# 生成编译模型
dotnet ef dbcontext optimize --output-dir CompiledModels
4.3 查询模式
// Eager Loading(解决 N+1)
var blogs1 = await context.Blogs
.Include(b => b.Posts)
.ThenInclude(p => p.Tags)
.ToListAsync();
// Split Query(避免 Cartesian Explosion)
var blogs2 = await context.Blogs
.Include(b => b.Posts)
.ThenInclude(p => p.Tags)
.AsSplitQuery()
.ToListAsync();
// Explicit Loading
var blog = await context.Blogs.FindAsync(1);
await context.Entry(blog).Collection(b => b.Posts).LoadAsync();
// AsNoTracking(只读查询优化)
var blogs3 = await context.Blogs
.AsNoTracking()
.Where(b => b.IsActive)
.ToListAsync();
// AsNoTrackingWithIdentityResolution(避免重复实体)
var blogs4 = await context.Blogs
.Include(b => b.Posts)
.AsNoTrackingWithIdentityResolution()
.ToListAsync();
// Projection(只查询需要的列)
var blogSummaries = await context.Blogs
.Select(b => new BlogSummary
{
Url = b.Url,
PostCount = b.Posts.Count,
LatestPostDate = b.Posts.Max(p => (DateTime?)p.PublishedAt)
})
.ToListAsync();
// Compiled Query(预编译查询)
private static readonly Func<BlogContext, int, Task<Blog>> _getBlogById =
EF.CompileAsyncQuery((BlogContext ctx, int id) =>
ctx.Blogs.Include(b => b.Posts).FirstOrDefault(b => b.BlogId == id));
var blog = await _getBlogById(context, 1);
// 原生 SQL 查询
var activeBlogs = await context.Blogs
.FromSqlRaw("SELECT * FROM Blogs WHERE IsActive = 1")
.ToListAsync();
// 复杂 SQL + LINQ 组合
var filteredBlogs = await context.Blogs
.FromSqlInterpolated($"SELECT * FROM Blogs WHERE CreatedAt > {dateThreshold}")
.Where(b => b.Url.Contains("dotnet"))
.OrderBy(b => b.CreatedAt)
.ToListAsync();
4.4 批量操作
// EF Core 7+: ExecuteUpdate(直接翻译为 SQL UPDATE)
await context.Blogs
.Where(b => b.CreatedAt < DateTime.UtcNow.AddYears(-1))
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Url, b => b.Url + "/archived")
.SetProperty(b => b.Metadata.Owner, "system"));
// ExecuteDelete(直接翻译为 SQL DELETE)
await context.Posts
.Where(p => p.PublishedAt < DateTime.UtcNow.AddYears(-2))
.ExecuteDeleteAsync();
// ExecuteUpdate 返回受影响行数
int affectedRows = await context.Blogs
.Where(b => b.IsActive == false)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.IsActive, true));
// 批量插入(EF Core 7+ 原生支持)
context.Blogs.AddRange(blogs);
await context.SaveChangesAsync(); // 自动批处理
// 高性能批量插入(第三方库)
await context.BulkInsertAsync(blogs); // EFCore.BulkExtensions
await context.BulkUpdateAsync(blogs);
await context.BulkDeleteAsync(blogs);
4.5 事务与并发
// 显式事务
using var transaction = await context.Database.BeginTransactionAsync(
IsolationLevel.ReadCommitted);
try
{
var blog = new Blog { Url = "https://example.com" };
context.Blogs.Add(blog);
await context.SaveChangesAsync();
var post = new Post { BlogId = blog.BlogId, Title = "Hello" };
context.Posts.Add(post);
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
// 乐观并发
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
try
{
blog.Url = "new-url";
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var dbValues = await entry.GetDatabaseValuesAsync();
if (dbValues == null)
{
// 实体已被删除
return NotFound();
}
// 显示冲突
var currentUrl = (string)dbValues["Url"];
return Conflict(new { message = $"数据库当前 Url: {currentUrl}" });
}
// 跨上下文事务(分布式事务)
using var scope = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted },
TransactionScopeAsyncFlowOption.Enabled);
try
{
await context1.SaveChangesAsync();
await context2.SaveChangesAsync();
scope.Complete();
}
finally
{
scope.Dispose();
}
4.6 全局查询过滤器(多租户)
public interface ITenantEntity
{
string TenantId { get; set; }
}
public class Blog : ITenantEntity
{
public int BlogId { get; set; }
public string Url { get; set; }
public string TenantId { get; set; }
}
public class TenantDbContext : DbContext
{
private readonly string _tenantId;
public TenantDbContext(DbContextOptions<TenantDbContext> options, ITenantProvider tenantProvider)
: base(options)
{
_tenantId = tenantProvider.GetCurrentTenantId();
}
public DbSet<Blog> Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasQueryFilter(b => b.TenantId == _tenantId);
modelBuilder.Entity<Blog>()
.HasIndex(b => new { b.TenantId, b.Url })
.IsUnique();
}
}
// 临时禁用过滤器
var allBlogs = await context.Blogs
.IgnoreQueryFilters()
.ToListAsync();
4.7 自定义拦截器
// SQL 拦截器:记录所有执行的 SQL
public class SqlLoggingInterceptor : DbCommandInterceptor
{
private readonly ILogger<SqlLoggingInterceptor> _logger;
public SqlLoggingInterceptor(ILogger<SqlLoggingInterceptor> logger)
{
_logger = logger;
}
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("SQL: {Sql}", command.CommandText);
_logger.LogDebug("Parameters: {Parameters}",
string.Join(", ", command.Parameters.Cast<DbParameter>().Select(p => $"{p.ParameterName}={p.Value}")));
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
public override ValueTask<int> NonQueryExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
int result,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("SQL affected {Count} rows: {Sql}", result, command.CommandText);
return base.NonQueryExecutedAsync(command, eventData, result, cancellationToken);
}
}
// 注册拦截器
services.AddDbContext<BlogContext>(options =>
options.UseSqlServer(connectionString)
.AddInterceptors(new SqlLoggingInterceptor(logger)));
// 慢查询检测
public class SlowQueryInterceptor : DbCommandInterceptor
{
private const int SlowQueryThresholdMs = 100;
private readonly ILogger<SlowQueryInterceptor> _logger;
public override ValueTask<CommandExecutedDataReader> ReaderExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
CommandExecutedDataReader result,
CancellationToken cancellationToken = default)
{
if (eventData.Duration.TotalMilliseconds > SlowQueryThresholdMs)
{
_logger.LogWarning("Slow query ({Ms}ms): {Sql}",
eventData.Duration.TotalMilliseconds, command.CommandText);
}
return base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
}
}
4.8 跨数据库 Provider
// 可移植 DbContext
public class BlogContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public BlogContext(DbContextOptions<BlogContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 跨 Provider 通用配置
modelBuilder.Entity<Blog>(b =>
{
b.HasKey(x => x.BlogId);
b.Property(x => x.Url).IsRequired().HasMaxLength(200);
// Provider 特定配置
if (Database.ProviderName == "Microsoft.EntityFrameworkCore.SqlServer")
{
b.Property(x => x.CreatedAt).HasColumnType("datetime2");
}
else if (Database.ProviderName == "Npgsql.EntityFrameworkCore.PostgreSQL")
{
b.Property(x => x.CreatedAt).HasColumnType("timestamp with time zone");
}
else if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite")
{
b.Property(x => x.CreatedAt).HasColumnType("TEXT");
}
});
}
}
// 注册不同 Provider
// SQL Server
services.AddDbContext<BlogContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("SqlServer")));
// PostgreSQL
services.AddDbContext<BlogContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("Postgres")));
// SQLite
services.AddDbContext<BlogContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("Sqlite")));
// MySQL
services.AddDbContext<BlogContext>(options =>
options.UseMySql(
Configuration.GetConnectionString("MySql"),
ServerVersion.AutoDetect(Configuration.GetConnectionString("MySql"))));
4.9 CQRS 与 Dapper 集成
// EF Core 处理写操作
public class BlogCommandHandler
{
private readonly BlogContext _context;
public BlogCommandHandler(BlogContext context) => _context = context;
public async Task<int> CreateBlog(CreateBlogCommand command)
{
var blog = new Blog { Url = command.Url };
_context.Blogs.Add(blog);
await _context.SaveChangesAsync();
return blog.BlogId;
}
}
// Dapper 处理读操作(性能优化)
public class BlogQueryHandler
{
private readonly IDbConnection _connection;
public BlogQueryHandler(IDbConnection connection) => _connection = connection;
public async Task<BlogSummaryDto> GetBlogSummary(int blogId)
{
const string sql = @"
SELECT b.Url, COUNT(p.PostId) AS PostCount
FROM Blogs b
LEFT JOIN Posts p ON b.BlogId = p.BlogId
WHERE b.BlogId = @BlogId
GROUP BY b.Url";
return await _connection.QueryFirstOrDefaultAsync<BlogSummaryDto>(
sql, new { BlogId = blogId });
}
public async Task<IEnumerable<BlogListDto>> GetBlogsByTag(string tag)
{
const string sql = @"
SELECT b.BlogId, b.Url
FROM Blogs b
INNER JOIN PostTags pt ON pt.PostBlogId = b.BlogId -- 注意多对多生成
INNER JOIN Tags t ON t.TagId = pt.TagTagId
WHERE t.Name = @Tag";
return await _connection.QueryAsync<BlogListDto>(sql, new { Tag = tag });
}
}
4.10 复杂类型(EF Core 8+)
// EF Core 8+: Complex Types(无主键的值对象)
[ComplexType]
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
public GeoCoordinate Location { get; set; }
}
[ComplexType]
public class GeoCoordinate
{
public double Latitude { get; set; }
public double Longitude { get; set; }
}
public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; }
public Address ShippingAddress { get; set; }
public Address BillingAddress { get; set; } // Complex Type 可重复使用
}
// 配置
modelBuilder.Entity<Customer>(c =>
{
c.ComplexProperty(x => x.ShippingAddress);
c.ComplexProperty(x => x.BillingAddress);
});
// 数据库列:ShippingAddress_Street, ShippingAddress_City, ...
// 注意:Complex Type 不能为 null(与 Owned Entity 不同)
4.11 JSON 列映射(EF Core 7+)
// EF Core 7+: 将对象映射为 JSON 列
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public ContactInfo Contact { get; set; } // 映射为 JSON 列
}
public class ContactInfo
{
public string Email { get; set; }
public string Phone { get; set; }
public List<string> SocialMedia { get; set; }
public Address Address { get; set; } // 嵌套对象
}
// 配置
modelBuilder.Entity<Customer>(c =>
{
c.OwnsOne(x => x.Contact, builder =>
{
builder.ToJson(); // 存储为 JSON 列
builder.OwnsOne(ci => ci.Address);
});
});
// 查询 JSON 属性(SQL Server 2022 / PostgreSQL)
var customersInNY = await context.Customers
.Where(c => c.Contact.Address.City == "New York")
.ToListAsync();
// 翻译为:
// SELECT * FROM Customers WHERE JSON_VALUE(Contact, '$.Address.City') = 'New York'
4.12 异步流式处理(IAsyncEnumerable)
// EF Core 8+: IAsyncEnumerable 流式处理
public async IAsyncEnumerable<Blog> StreamBlogsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var blog in context.Blogs
.AsAsyncEnumerable()
.WithCancellation(cancellationToken))
{
yield return blog;
}
}
// 大数据集处理(避免内存爆炸)
await foreach (var blog in StreamBlogsAsync(cancellationToken))
{
ProcessBlog(blog);
}
// 批量处理
var batchSize = 100;
var batch = new List<Blog>(batchSize);
await foreach (var blog in context.Blogs.AsAsyncEnumerable())
{
batch.Add(blog);
if (batch.Count >= batchSize)
{
ProcessBatch(batch);
batch.Clear();
}
}
if (batch.Count > 0) ProcessBatch(batch);
5. 对比分析
5.1 EF Core vs Dapper vs NHibernate
| 特性 | EF Core | Dapper | NHibernate |
|---|---|---|---|
| 学习曲线 | 中等 | 低 | 高 |
| 性能 | 中 | 高 | 低 |
| 跨数据库 | √ | √ | √ |
| 迁移工具 | √(内置) | × | 第三方 |
| 变更跟踪 | √ | × | √ |
| 复杂查询 | 中 | 高 | 低 |
| Lazy Loading | √ | × | √ |
| 多对多 | √ | × | √ |
| LINQ 翻译 | 强 | × | 中 |
| AOT 支持 | 部分 | √ | × |
| 社区活跃度 | 高 | 高 | 中 |
| 推荐场景 | 业务复杂、领域模型 | 简单 CRUD、性能敏感 | 老项目维护 |
5.2 Code First vs Database First
| 维度 | Code First | Database First |
|---|---|---|
| 起始点 | C# 实体类 | 已有数据库 |
| 优先级 | 业务模型 | 数据库模式 |
| 迁移 | 自动生成 | 手动维护 |
| 团队协作 | 优 | 中 |
| 数据库特性 | 受限 | 完整支持 |
| 适合场景 | 新项目 | 现有数据库 |
5.3 Eager Loading vs Split Query vs Explicit Loading
| 方案 | 查询次数 | 网络往返 | 内存 | SQL 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| Eager Loading | 1 | 1 | 高(Cartesian) | LEFT JOIN | 关联少 |
| Split Query | N(关联数) | N | 中 | N 个 SELECT | 关联多 |
| Explicit Loading | 1 + N | 1 + N | 低 | 简单 | 按需加载 |
| Lazy Loading | 1 + N | 1 + N | 低 | 简单 | 不推荐 |
5.4 TPH vs TPT vs TPC 继承映射
| 策略 | 表数量 | 性能 | 复杂度 | NULL 列 | 适用场景 |
|---|---|---|---|---|---|
| TPH(Table per Hierarchy) | 1 | 最快 | 低 | 多 | 子类型少 |
| TPT(Table per Type) | N(类型数) | 中 | 中 | 无 | 子类型多 |
| TPC(Table per Concrete type) | N(具体类) | 中 | 高 | 无 | 深继承树 |
// TPH(默认)
modelBuilder.Entity<Animal>().UseTphMappingStrategy();
// TPT
modelBuilder.Entity<Animal>().UseTptMappingStrategy();
// TPC(EF Core 7+)
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
5.5 NoTracking vs Tracking
| 维度 | AsNoTracking | AsNoTrackingWithIdentityResolution | Tracking |
|---|---|---|---|
| 性能 | 最快 | 略慢 | 慢 |
| 内存 | 低 | 中 | 高 |
| 重复实体 | 多份 | 一份 | 一份 |
| 修改保存 | × | × | √ |
| 适用场景 | 只读查询 | 只读查询 + 关联 | 写操作 |
6. 常见陷阱与最佳实践
6.1 陷阱:N+1 查询
反例:
// N+1 查询
var blogs = await context.Blogs.ToListAsync(); // 1 次
foreach (var blog in blogs)
{
var postCount = blog.Posts.Count; // 每次都触发 SQL(懒加载)
Console.WriteLine($"{blog.Url}: {postCount}");
}
// 总查询次数:1 + N(N = blogs.Count)
最佳实践:
// Eager Loading
var blogs = await context.Blogs
.Include(b => b.Posts)
.ToListAsync(); // 1 次
// 或 Projection(最优化)
var blogStats = await context.Blogs
.Select(b => new { b.Url, PostCount = b.Posts.Count })
.ToListAsync(); // 1 次
6.2 陷阱:ChangeTracker 膨胀
反例:
// 长时间跟踪大量实体,ChangeTracker 变慢
foreach (var item in hugeList)
{
context.Items.Add(item);
}
await context.SaveChangesAsync(); // 可能 OOM 或超时
最佳实践:
// 批量提交
const int batchSize = 1000;
for (int i = 0; i < hugeList.Count; i += batchSize)
{
var batch = hugeList.Skip(i).Take(batchSize);
context.Items.AddRange(batch);
await context.SaveChangesAsync();
context.ChangeTracker.Clear(); // 清理跟踪
}
// 或使用 EFCore.BulkExtensions
await context.BulkInsertAsync(hugeList);
6.3 陷阱:循环中的查询
反例:
// 每次循环都查询数据库
foreach (var id in ids)
{
var entity = await context.Blogs.FindAsync(id); // N 次查询
Process(entity);
}
最佳实践:
// 批量查询
var entities = await context.Blogs
.Where(b => ids.Contains(b.BlogId))
.ToDictionaryAsync(b => b.BlogId); // 1 次查询
foreach (var id in ids)
{
if (entities.TryGetValue(id, out var entity))
Process(entity);
}
6.4 陷阱:Cartesian Explosion
反例:
// 多重 Include 导致 Cartesian Explosion
var blogs = await context.Blogs
.Include(b => b.Posts)
.Include(b => b.Author)
.Include(b => b.Tags)
.ToListAsync();
// 假设:100 blogs × 50 posts × 3 tags = 15000 行,重复 blog 数据 150 次
最佳实践:
// Split Query
var blogs = await context.Blogs
.Include(b => b.Posts)
.Include(b => b.Author)
.Include(b => b.Tags)
.AsSplitQuery()
.ToListAsync();
// 4 次查询:1 blogs + 1 posts + 1 author + 1 tags
6.5 陷阱:AutoDetectChangesEnabled 性能问题
反例:
// 大量 Add 操作时,每次 Add 都触发 DetectChanges
foreach (var item in hugeList)
{
context.Items.Add(item); // 每次都 DetectChanges
}
最佳实践:
// 禁用自动检测
context.ChangeTracker.AutoDetectChangesEnabled = false;
try
{
context.Items.AddRange(hugeList);
context.ChangeTracker.DetectChanges(); // 手动调用一次
await context.SaveChangesAsync();
}
finally
{
context.ChangeTracker.AutoDetectChangesEnabled = true;
}
6.6 陷阱:DbContext 线程不安全
反例:
// DbContext 不是线程安全的,并发访问会出错
public class Service
{
private readonly BlogContext _context; // 单例
public async Task MultipleRequests()
{
var tasks = Enumerable.Range(0, 10)
.Select(_ => _context.Blogs.ToListAsync());
await Task.WhenAll(tasks); // 异常!
}
}
最佳实践:
// 注册为 Scoped(每次请求一个实例)
services.AddDbContext<BlogContext>(options =>
options.UseSqlServer(connectionString),
ServiceLifetime.Scoped);
// 或使用 DbContextPool
services.AddDbContextPool<BlogContext>(options =>
options.UseSqlServer(connectionString), poolSize: 128);
6.7 最佳实践总结
| 实践 | 说明 |
|---|---|
只读查询使用 AsNoTracking | 性能提升 30-50% |
| 批量操作禁用 AutoDetectChanges | 避免每次 Add 触发 DetectChanges |
| 避免循环内查询 | 改为批量查询 |
| 使用 Projection 减少数据传输 | 只查询需要的列 |
| 多 Include 使用 SplitQuery | 避免 Cartesian Explosion |
| 批量提交避免 ChangeTracker 膨胀 | 每 1000 条提交一次 |
| 索引覆盖查询条件 | 数据库索引是性能基础 |
| 编译模型减少启动时间 | EF Core 6+ 特性 |
| 跨上下文使用显式事务 | BeginTransactionAsync |
乐观并发使用 [Timestamp] | 默认推荐 |
| 长连接使用 DbContextPool | 减少实例化开销 |
| 慢查询检测使用 Interceptor | DbCommandInterceptor |
7. 工程实践
7.1 仓储模式(Repository Pattern)
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
public class Repository<T> : IRepository<T> where T : class
{
private readonly BlogContext _context;
private readonly DbSet<T> _dbSet;
public Repository(BlogContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.AsNoTracking().ToListAsync();
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate) =>
await _dbSet.AsNoTracking().Where(predicate).ToListAsync();
public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);
public Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
return Task.CompletedTask;
}
public Task DeleteAsync(T entity)
{
_dbSet.Remove(entity);
return Task.CompletedTask;
}
}
// 工作单元(Unit of Work)
public interface IUnitOfWork
{
IRepository<Blog> Blogs { get; }
IRepository<Post> Posts { get; }
Task<int> SaveChangesAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly BlogContext _context;
public IRepository<Blog> Blogs { get; }
public IRepository<Post> Posts { get; }
public UnitOfWork(BlogContext context)
{
_context = context;
Blogs = new Repository<Blog>(context);
Posts = new Repository<Post>(context);
}
public Task<int> SaveChangesAsync() => _context.SaveChangesAsync();
}
7.2 多租户架构
public interface ITenantProvider
{
string GetCurrentTenantId();
}
public class HttpContextTenantProvider : ITenantProvider
{
private readonly IHttpContextAccessor _accessor;
public HttpContextTenantProvider(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public string GetCurrentTenantId()
{
return _accessor.HttpContext?.User?.FindFirst("tenant_id")?.Value
?? throw new InvalidOperationException("Tenant not found");
}
}
// 多租户 DbContext
public class MultiTenantDbContext : DbContext
{
private readonly ITenantProvider _tenantProvider;
public MultiTenantDbContext(
DbContextOptions<MultiTenantDbContext> options,
ITenantProvider tenantProvider) : base(options)
{
_tenantProvider = tenantProvider;
}
public DbSet<Blog> Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasQueryFilter(b => b.TenantId == _tenantProvider.GetCurrentTenantId());
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
foreach (var entry in ChangeTracker.Entries<ITenantEntity>())
{
if (entry.State == EntityState.Added)
entry.Entity.TenantId = tenantId;
}
return base.SaveChangesAsync(cancellationToken);
}
}
7.3 软删除模式
public interface ISoftDeletable
{
bool IsDeleted { get; set; }
DateTime? DeletedAt { get; set; }
}
public class Blog : ISoftDeletable
{
public int BlogId { get; set; }
public string Url { get; set; }
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
}
// 全局查询过滤器
modelBuilder.Entity<Blog>()
.HasQueryFilter(b => !b.IsDeleted);
// 软删除扩展方法
public static class SoftDeleteExtensions
{
public static Task SoftDeleteAsync<T>(this DbSet<T> dbSet, T entity)
where T : class, ISoftDeletable
{
entity.IsDeleted = true;
entity.DeletedAt = DateTime.UtcNow;
dbSet.Update(entity);
return Task.CompletedTask;
}
}
// 使用
await context.Blogs.SoftDeleteAsync(blog);
await context.SaveChangesAsync();
// 临时查询已删除数据
var deletedBlogs = await context.Blogs
.IgnoreQueryFilters()
.Where(b => b.IsDeleted)
.ToListAsync();
7.4 审计日志(SaveChanges 拦截)
public class AuditInterceptor : SaveChangesInterceptor
{
private readonly ICurrentUserService _currentUser;
public AuditInterceptor(ICurrentUserService currentUser)
{
_currentUser = currentUser;
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken);
var auditEntries = new List<AuditEntry>();
foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
{
var auditEntry = new AuditEntry(entry)
{
TableName = entry.Metadata.GetTableName(),
UserId = _currentUser.UserId,
Action = entry.State switch
{
EntityState.Added => "INSERT",
EntityState.Modified => "UPDATE",
EntityState.Deleted => "DELETE",
_ => null
}
};
foreach (var property in entry.Properties)
{
if (entry.State == EntityState.Added)
{
auditEntry.NewValues[property.Metadata.Name] = property.CurrentValue;
}
else if (entry.State == EntityState.Deleted)
{
auditEntry.OldValues[property.Metadata.Name] = property.OriginalValue;
}
else if (entry.State == EntityState.Modified && property.IsModified)
{
auditEntry.OldValues[property.Metadata.Name] = property.OriginalValue;
auditEntry.NewValues[property.Metadata.Name] = property.CurrentValue;
}
}
auditEntries.Add(auditEntry);
}
// 保存审计日志(可异步写入独立表)
foreach (var auditEntry in auditEntries)
{
context.Set<AuditLog>().Add(auditEntry.ToAudit());
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
7.5 EF Core Analyzer
// 自定义 Roslyn 分析器:检测 N+1 查询模式
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class NPlusOneQueryAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "EF001";
private static readonly DiagnosticDescriptor Rule = new(
DiagnosticId,
"Potential N+1 query",
"Loop contains DbContext query, possible N+1",
"Performance",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeLoop, SyntaxKind.ForEachStatement);
context.RegisterSyntaxNodeAction(AnalyzeLoop, SyntaxKind.ForStatement);
}
private static void AnalyzeLoop(SyntaxNodeAnalysisContext context)
{
var loop = context.Node;
var semanticModel = context.SemanticModel;
// 检测循环内是否有 DbContext 查询调用
var invocations = loop.DescendantNodes().OfType<InvocationExpressionSyntax>();
foreach (var invocation in invocations)
{
var symbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
if (symbol?.Name == "ToListAsync" || symbol?.Name == "FirstAsync")
{
var diagnostic = Diagnostic.Create(Rule, invocation.GetLocation());
context.ReportDiagnostic(diagnostic);
break;
}
}
}
}
8. 案例研究
8.1 案例一:电商订单系统
场景:电商系统订单管理,包含订单、订单项、商品、用户多对多关系。
public class Order
{
public int OrderId { get; set; }
public string OrderNumber { get; set; }
public DateTime CreatedAt { get; set; }
public OrderStatus Status { get; set; }
public int UserId { get; set; }
public User User { get; set; }
public List<OrderItem> Items { get; set; } = new();
public Address ShippingAddress { get; set; }
}
public class OrderItem
{
public int OrderItemId { get; set; }
public int OrderId { get; set; }
public Order Order { get; set; }
public int ProductId { get; set; }
public Product Product { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
// 高性能查询:使用 Projection 避免加载完整实体
public async Task<OrderDto> GetOrderAsync(int orderId)
{
return await context.Orders
.AsNoTracking()
.Where(o => o.OrderId == orderId)
.Select(o => new OrderDto
{
OrderNumber = o.OrderNumber,
CreatedAt = o.CreatedAt,
Status = o.Status.ToString(),
User = new UserDto { Name = o.User.Name, Email = o.User.Email },
Items = o.Items.Select(i => new OrderItemDto
{
ProductName = i.Product.Name,
Quantity = i.Quantity,
UnitPrice = i.UnitPrice,
Subtotal = i.Quantity * i.UnitPrice
}).ToList(),
TotalAmount = o.Items.Sum(i => i.Quantity * i.UnitPrice)
})
.FirstOrDefaultAsync();
}
// 批量下单:使用事务
public async Task<int> CreateOrderAsync(CreateOrderCommand command)
{
using var transaction = await context.Database.BeginTransactionAsync();
try
{
var order = new Order
{
OrderNumber = GenerateOrderNumber(),
UserId = command.UserId,
Status = OrderStatus.Pending,
ShippingAddress = command.ShippingAddress
};
foreach (var item in command.Items)
{
var product = await context.Products.FindAsync(item.ProductId);
if (product.Stock < item.Quantity)
throw new InvalidOperationException($"Insufficient stock: {product.Name}");
product.Stock -= item.Quantity;
order.Items.Add(new OrderItem
{
ProductId = product.ProductId,
Quantity = item.Quantity,
UnitPrice = product.Price
});
}
context.Orders.Add(order);
await context.SaveChangesAsync();
await transaction.CommitAsync();
return order.OrderId;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
8.2 案例二:内容管理系统(CMS)
场景:CMS 系统,文章与标签多对多关系,分类树形结构。
public class Article
{
public int ArticleId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime PublishedAt { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
public List<Tag> Tags { get; set; } = new();
}
public class Category
{
public int CategoryId { get; set; }
public string Name { get; set; }
public int? ParentCategoryId { get; set; }
public Category Parent { get; set; }
public List<Category> Children { get; set; } = new();
}
// 递归查询分类树(SQL Server CTE)
public async Task<CategoryTreeDto> GetCategoryTreeAsync()
{
const string sql = @"
WITH CategoryCTE AS (
SELECT CategoryId, Name, ParentCategoryId, 0 AS Level
FROM Categories
WHERE ParentCategoryId IS NULL
UNION ALL
SELECT c.CategoryId, c.Name, c.ParentCategoryId, p.Level + 1
FROM Categories c
INNER JOIN CategoryCTE p ON c.ParentCategoryId = p.CategoryId
)
SELECT * FROM CategoryCTE";
var categories = await context.Categories
.FromSqlRaw(sql)
.AsNoTracking()
.ToListAsync();
return BuildTree(categories);
}
// 全文搜索(PostgreSQL pg_trgm)
public async Task<IEnumerable<ArticleSearchResult>> SearchArticlesAsync(string keyword)
{
return await context.Articles
.AsNoTracking()
.Where(a => EF.Functions.TrigramsAreSimilar(a.Title, keyword)
|| EF.Functions.TrigramsAreSimilar(a.Content, keyword))
.OrderByDescending(a => EF.Functions.TrigramSimilarity(a.Title, keyword))
.Select(a => new ArticleSearchResult
{
ArticleId = a.ArticleId,
Title = a.Title,
PublishedAt = a.PublishedAt
})
.ToListAsync();
}
8.3 案例三:SaaS 多租户
场景:SaaS 平台,每个租户独立数据,使用全局查询过滤器。
public class TenantDbContext : DbContext
{
private readonly ITenantProvider _tenantProvider;
public string CurrentTenantId => _tenantProvider.GetCurrentTenantId();
public TenantDbContext(
DbContextOptions<TenantDbContext> options,
ITenantProvider tenantProvider) : base(options)
{
_tenantProvider = tenantProvider;
}
public DbSet<Project> Projects { get; set; }
public DbSet<Task> Tasks { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 所有实体自动添加 TenantId 过滤
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType)
.HasQueryFilter($"TenantId == '{CurrentTenantId}'");
}
}
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entry in ChangeTracker.Entries<ITenantEntity>())
{
if (entry.State == EntityState.Added)
entry.Entity.TenantId = CurrentTenantId;
}
return base.SaveChangesAsync(cancellationToken);
}
}
8.4 案例四:事件溯源(Event Sourcing)
场景:事件溯源系统,聚合根状态由事件重放得到。
public class EventStore
{
private readonly EventStoreContext _context;
public EventStore(EventStoreContext context) => _context = context;
public async Task SaveEventsAsync(Guid aggregateId, IEnumerable<object> events, int expectedVersion)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var currentVersion = await _context.Events
.Where(e => e.AggregateId == aggregateId)
.CountAsync();
if (currentVersion != expectedVersion)
throw new ConcurrencyException($"Expected version {expectedVersion}, actual {currentVersion}");
foreach (var @event in events)
{
_context.Events.Add(new EventRecord
{
AggregateId = aggregateId,
EventType = @event.GetType().FullName,
EventData = JsonSerializer.Serialize(@event, @event.GetType()),
Version = ++currentVersion,
Timestamp = DateTime.UtcNow
});
}
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
public async Task<IEnumerable<object>> LoadEventsAsync(Guid aggregateId)
{
var records = await _context.Events
.Where(e => e.AggregateId == aggregateId)
.OrderBy(e => e.Version)
.AsNoTracking()
.ToListAsync();
return records.Select(r =>
{
var type = Type.GetType(r.EventType);
return JsonSerializer.Deserialize(r.EventData, type);
});
}
}
8.5 案例五:审计日志系统
场景:所有实体变更记录到审计表,支持查询历史。
public class AuditLog
{
public int AuditLogId { get; set; }
public string TableName { get; set; }
public string Action { get; set; } // INSERT / UPDATE / DELETE
public string KeyValues { get; set; } // JSON
public string OldValues { get; set; } // JSON
public string NewValues { get; set; } // JSON
public string UserId { get; set; }
public DateTime Timestamp { get; set; }
}
public class AuditInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken);
foreach (var entry in context.ChangeTracker.Entries())
{
if (entry.State == EntityState.Unchanged) continue;
var audit = new AuditLog
{
TableName = entry.Metadata.GetTableName(),
Action = entry.State.ToString(),
Timestamp = DateTime.UtcNow,
KeyValues = JsonSerializer.Serialize(
entry.Properties.Where(p => p.Metadata.IsPrimaryKey())
.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue))
};
if (entry.State == EntityState.Modified || entry.State == EntityState.Deleted)
{
audit.OldValues = JsonSerializer.Serialize(
entry.Properties.ToDictionary(p => p.Metadata.Name, p => p.OriginalValue));
}
if (entry.State == EntityState.Modified || entry.State == EntityState.Added)
{
audit.NewValues = JsonSerializer.Serialize(
entry.Properties.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue));
}
context.Set<AuditLog>().Add(audit);
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
8.6 案例六:跨数据库迁移
场景:从 SQL Server 迁移到 PostgreSQL,保持业务逻辑不变。
// 抽象 DbContext
public abstract class BaseDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected BaseDbContext(DbContextOptions options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 通用配置
modelBuilder.Entity<Blog>(b =>
{
b.HasKey(x => x.BlogId);
b.Property(x => x.Url).IsRequired().HasMaxLength(200);
b.HasIndex(x => x.Url).IsUnique();
});
// Provider 特定配置
ConfigureForProvider(modelBuilder);
}
protected abstract void ConfigureForProvider(ModelBuilder modelBuilder);
}
// SQL Server
public class SqlServerDbContext : BaseDbContext
{
public SqlServerDbContext(DbContextOptions<SqlServerDbContext> options) : base(options) { }
protected override void ConfigureForProvider(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.CreatedAt)
.HasColumnType("datetime2");
modelBuilder.Entity<Blog>()
.Property(b => b.Id)
.UseIdentityColumn(); // IDENTITY
}
}
// PostgreSQL
public class PostgresDbContext : BaseDbContext
{
public PostgresDbContext(DbContextOptions<PostgresDbContext> options) : base(options) { }
protected override void ConfigureForProvider(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.CreatedAt)
.HasColumnType("timestamp with time zone");
modelBuilder.Entity<Blog>()
.Property(b => b.Id)
.UseHiLo(); // HiLo 序列
// 大小写敏感配置
modelBuilder.UseSnakeCaseNamingConvention();
}
}
8.7 案例七:性能优化实战
场景:列表页查询 10000 条记录,从 8 秒优化到 200 毫秒。
优化前:
public async Task<List<BlogDto>> GetBlogsAsync()
{
var blogs = await context.Blogs
.Include(b => b.Posts)
.Include(b => b.Author)
.ToListAsync(); // 8 秒
return blogs.Select(b => new BlogDto
{
Url = b.Url,
PostCount = b.Posts.Count,
AuthorName = b.Author.Name
}).ToList();
}
问题分析:
- Eager Loading 导致 Cartesian Explosion(10000 × 平均 50 posts = 500000 行)。
- 加载完整实体,但只需 3 个字段。
- ChangeTracker 跟踪 500000 个实体。
优化后:
public async Task<List<BlogDto>> GetBlogsAsync()
{
return await context.Blogs
.AsNoTracking()
.Select(b => new BlogDto
{
Url = b.Url,
PostCount = b.Posts.Count,
AuthorName = b.Author.Name
})
.ToListAsync(); // 200 毫秒
}
进一步分页:
public async Task<PagedResult<BlogDto>> GetBlogsAsync(int page, int pageSize)
{
var query = context.Blogs
.AsNoTracking()
.Select(b => new BlogDto
{
Url = b.Url,
PostCount = b.Posts.Count,
AuthorName = b.Author.Name
});
var totalCount = await query.CountAsync();
var items = await query
.OrderBy(b => b.Url)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<BlogDto>(items, totalCount, page, pageSize);
}
简答题知识点讲解
常见疑问 6:解释 AsNoTracking 与 AsNoTrackingWithIdentityResolution 的区别,并说明何时使用哪个。
解析讲解:
AsNoTracking:查询的实体不进入 ChangeTracker,性能最优。但在关联查询时,若多个父实体引用同一子实体,会创建多份子实体实例。AsNoTrackingWithIdentityResolution:不跟踪,但维护身份映射,确保同一实体只有一份实例。性能略低于AsNoTracking,但保证引用一致性。
使用场景:
- 只读简单查询(无关联):
AsNoTracking - 只读关联查询(需引用一致):
AsNoTrackingWithIdentityResolution - 写操作:默认跟踪
常见疑问 7:EF Core 的全局查询过滤器(HasQueryFilter)有什么用途?在多租户架构中如何使用?
解析讲解:
全局查询过滤器为所有查询自动添加 WHERE 条件,常用于:
- 软删除:
HasQueryFilter(e => !e.IsDeleted)自动过滤已删除实体。 - 多租户:
HasQueryFilter(e => e.TenantId == _tenantId)自动按租户过滤。
多租户用法:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasQueryFilter(b => b.TenantId == _tenantProvider.GetCurrentTenantId());
}
可通过 IgnoreQueryFilters() 临时禁用过滤器(如管理员查看所有租户数据)。
常见疑问 8:解释 EF Core 的乐观并发控制机制。
解析讲解:
EF Core 通过 [Timestamp](byte[] RowVersion)实现乐观并发:
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
UPDATE 时 EF Core 自动添加 RowVersion 检查:
UPDATE Blogs SET Url = @p1
WHERE BlogId = @p0 AND RowVersion = @p2;
若数据库中 RowVersion 已被其他事务修改,影响 0 行,EF Core 抛出 DbUpdateConcurrencyException。开发者捕获异常后可提示用户重新加载数据或合并冲突。
优势:无需显式锁,并发度高,适合 Web 应用。
编程题知识点讲解
常见疑问 9:实现一个支持软删除的 EF Core 扩展,包括全局查询过滤器与软删除方法。
解析讲解:
public interface ISoftDeletable
{
bool IsDeleted { get; set; }
DateTime? DeletedAt { get; set; }
}
public static class SoftDeleteExtensions
{
// 在 OnModelCreating 中调用
public static void ApplySoftDeleteFilter(this ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(ISoftDeletable).IsAssignableFrom(entityType.ClrType))
{
var param = Expression.Parameter(entityType.ClrType, "e");
var prop = Expression.Property(param, nameof(ISoftDeletable.IsDeleted));
var falseConst = Expression.Constant(false);
var filter = Expression.Lambda(Expression.Equal(prop, falseConst), param);
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filter);
}
}
}
// 软删除扩展方法
public static Task SoftDeleteAsync<T>(this DbSet<T> dbSet, Expression<Func<T, bool>> predicate)
where T : class, ISoftDeletable
{
return dbSet.Where(predicate).ExecuteUpdateAsync(s => s
.SetProperty(e => e.IsDeleted, true)
.SetProperty(e => e.DeletedAt, DateTime.UtcNow));
}
}
// 使用
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplySoftDeleteFilter();
}
// 软删除
await context.Blogs.SoftDeleteAsync(b => b.BlogId == id);
await context.SaveChangesAsync();
// 查询已删除数据
var deletedBlogs = await context.Blogs
.IgnoreQueryFilters()
.Where(b => b.IsDeleted)
.ToListAsync();
常见疑问 10:实现一个自定义 IInterceptor,记录所有慢查询(> 100ms)并写入日志。
解析讲解:
public class SlowQueryInterceptor : DbCommandInterceptor
{
private const int SlowQueryThresholdMs = 100;
private readonly ILogger<SlowQueryInterceptor> _logger;
public SlowQueryInterceptor(ILogger<SlowQueryInterceptor> logger)
{
_logger = logger;
}
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
LogIfSlow(command, eventData.Duration);
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
public override ValueTask<int> NonQueryExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
int result,
CancellationToken cancellationToken = default)
{
LogIfSlow(command, eventData.Duration);
return base.NonQueryExecutedAsync(command, eventData, result, cancellationToken);
}
public override ValueTask<object> ScalarExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
object result,
CancellationToken cancellationToken = default)
{
LogIfSlow(command, eventData.Duration);
return base.ScalarExecutedAsync(command, eventData, result, cancellationToken);
}
private void LogIfSlow(DbCommand command, TimeSpan duration)
{
if (duration.TotalMilliseconds > SlowQueryThresholdMs)
{
var parameters = string.Join(", ",
command.Parameters.Cast<DbParameter>()
.Select(p => $"{p.ParameterName}={p.Value}"));
_logger.LogWarning(
"Slow query ({Ms:F1}ms): {Sql}\nParameters: {Parameters}",
duration.TotalMilliseconds,
command.CommandText,
parameters);
}
}
}
// 注册
services.AddDbContext<BlogContext>(options =>
options.UseSqlServer(connectionString)
.AddInterceptors(new SlowQueryInterceptor(logger)));
11.1 官方文档
- EF Core Documentation — Microsoft Learn
- EF Core Migrations
- EF Core Performance
- EF Core Logging and Interception
- EF Core Testing
11.2 经典书籍
- Entity Framework Core in Action by Jon P Smith(EF Core 实战指南,含性能优化)
- Pro Entity Framework Core for ASP.NET Core MVC by Adam Freeman(ASP.NET Core 集成)
- Architecting with Entity Framework Core by Holger Schwichtenberg(企业架构实践)
- Domain-Driven Design by Eric Evans(领域驱动设计,EF Core 实现值对象、聚合根)
- Patterns of Enterprise Application Architecture by Martin Fowler(企业架构模式,含仓储、工作单元)
11.3 学术论文
- Meijer, E., Beckman, B., & Bierman, G. (2003). LINQ: Reconciling Object, Relations and XML in the .NET Framework. SIGMOD 2003.
- Bierman, G., Meijer, E., & Torgersen, M. (2007). Lost in Translation: Formalizing the .NET LINQ Pattern. SAC 2008.
- Young, G. (2010). CQRS Documents. cqrs.files.wordpress.com.
11.4 开源项目
-
EFCore.BulkExtensions:高性能批量操作 https://github.com/borisdj/EFCore.BulkExtensions
-
EFCore.NamingConventions:命名约定(SnakeCase、CamelCase) https://github.com/efcore/EFCore.NamingConventions
-
EFCore.PowerTools:反向工程、模型可视化 https://github.com/ErikEJ/EFCorePowerTools
-
Pomelo.EntityFrameworkCore.MySql:MySQL Provider https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql
-
Npgsql.EntityFrameworkCore.PostgreSQL:PostgreSQL Provider https://github.com/npgsql/efcore.pg
-
AspNetCore.Diagnostics.EntityFrameworkCore:EF Core 健康检查 https://github.com/dotnet/AspNetCore.Diagnostics
11.5 进阶主题
- EF Core Source Generator:编译期生成
IModel,减少启动时间。 - Cosmos DB Provider:NoSQL 数据库的 EF Core 集成。
- In-Memory Provider:单元测试用,但行为与关系数据库不同。
- SQLite Provider:移动应用与桌面应用嵌入式数据库。
- Multitenant Architectures:每租户数据库、共享数据库、Schema 分离策略。
- Event Sourcing with EF Core:事件存储、快照、重放。
- CQRS Pattern:读写分离,EF Core 处理写、Dapper 处理读。
- Distributed Transactions:
TransactionScope与 MSDTC 的限制。 - Database Sharding:分库分表与 EF Core 集成。
- NativeAOT Compatibility:EF Core 9+ 对 AOT 的支持与限制。
附录 A:EF Core 性能基准(.NET 8, EF Core 8)
BenchmarkDotNet v0.13.12
Runtime=.NET 8.0
Database=SQL Server 2022
Table=Blogs (10000 rows)
| Method | Mean | Allocated | Ratio |
|------------------------------------ |---------:|----------:|------:|
| FindAsync (tracked) | 2.45 ms | 248 KB | 1.00 |
| FindAsync (AsNoTracking) | 1.80 ms | 124 KB | 0.73 |
| FirstAsync (tracked) | 2.60 ms | 260 KB | 1.06 |
| FirstAsync (AsNoTracking) | 1.85 ms | 128 KB | 0.76 |
| ToListAsync (1000, tracked) | 18.50 ms | 2.4 MB | 7.55 |
| ToListAsync (1000, AsNoTracking) | 12.30 ms | 1.2 MB | 5.02 |
| ToListAsync (Projection) | 8.20 ms | 640 KB | 3.35 |
| Include (10 blogs × 100 posts) | 45.20 ms | 6.8 MB | 18.45 |
| Include + AsSplitQuery | 28.50 ms | 4.2 MB | 11.63 |
| ExecuteUpdate (1000 rows) | 3.20 ms | 24 KB | 1.31 |
| SaveChanges (1000 inserts) | 85.00 ms | 8.5 MB | 34.69 |
| SaveChanges + AutoDetectChanges off | 62.50 ms | 4.2 MB | 25.51 |
| BulkInsert (EFCore.BulkExtensions) | 15.20 ms | 1.8 MB | 6.20 |
| Dapper Query (1000 rows) | 6.80 ms | 580 KB | 2.78 |
附录 B:EF Core 迁移命令速查
# 创建迁移
dotnet ef migrations add <Name>
# 应用所有待应用迁移
dotnet ef database update
# 应用到指定迁移
dotnet ef database update <TargetMigration>
# 撤销最后一次迁移(未应用前)
dotnet ef migrations remove
# 生成 SQL 脚本
dotnet ef migrations script
dotnet ef migrations script <From> <To>
dotnet ef migrations script --idempotent --output script.sql
# 列出所有迁移
dotnet ef migrations list
# 删除数据库
dotnet ef database drop --force
# 反向工程
dotnet ef dbcontext scaffold "<Connection>" <Provider> \
--output-dir Models \
--context-dir Data \
--context <ContextName> \
--use-database-names \
--no-onconfiguring \
--data-annotations \
--force
# 编译模型
dotnet ef dbcontext optimize --output-dir CompiledModels
# 生成 DbContext 类
dotnet ef dbcontext info
dotnet ef dbcontext list
# CLI 工具更新
dotnet tool update --global dotnet-ef
附录 C:EF Core 配置速查
C.1 Data Annotation
[Table("Blogs")]
public class Blog
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int BlogId { get; set; }
[Required]
[MaxLength(200)]
[Column("Url", TypeName = "nvarchar(200)")]
public string Url { get; set; }
[NotMapped]
public string ComputedProperty => Url.ToUpper();
[ConcurrencyCheck]
public byte[] RowVersion { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
[ForeignKey("BlogId")]
public List<Post> Posts { get; set; }
[InverseProperty("Blog")]
public List<Post> Posts { get; set; }
}
C.2 Fluent API
modelBuilder.Entity<Blog>(b =>
{
b.ToTable("Blogs");
b.HasKey(x => x.BlogId);
b.Property(x => x.BlogId)
.ValueGeneratedOnAdd()
.UseIdentityColumn();
b.Property(x => x.Url)
.IsRequired()
.HasMaxLength(200)
.HasColumnName("Url")
.HasColumnType("nvarchar(200)");
b.Ignore(x => x.ComputedProperty);
b.HasIndex(x => x.Url).IsUnique();
b.HasIndex(x => new { x.CreatedAt, x.IsActive });
b.HasMany(x => x.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId)
.OnDelete(DeleteBehavior.Cascade);
b.OwnsOne(x => x.Metadata, m =>
{
m.Property(p => p.Owner).HasMaxLength(100);
});
b.HasQueryFilter(x => !x.IsDeleted);
});
C.3 继承映射
// TPH(默认)
modelBuilder.Entity<Animal>().UseTphMappingStrategy();
// TPT
modelBuilder.Entity<Animal>().UseTptMappingStrategy();
// TPC
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
public abstract class Animal
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Dog : Animal
{
public string Breed { get; set; }
}
public class Cat : Animal
{
public bool IsIndoor { get; set; }
}