EF Core 基础入门
对很多 .NET 后端开发者来说,Entity Framework Core 是最常接触的数据访问框架之一。它的核心价值,不只是“少写 SQL”,而是把对象模型、数据库结构、查询表达式和迁移管理串成一套相对统一的开发体验。
但也正因为它足够方便,很多初学者会在还没真正理解原理的情况下就开始无脑 Include、无脑查整表、无脑更新整对象,最后遇到性能和数据一致性问题就会很迷茫。
所以学习 EF Core 时,既要学会“怎么用”,也要理解“什么时候该谨慎用”。
第一章 EF Core 是什么
EF Core 是 .NET 平台上的 ORM 框架之一。ORM 的核心思想是:
- 用对象表示数据
- 用代码表达查询和更新
- 让程序和数据库之间的映射更自然
它主要解决的问题包括:
- 减少重复的数据库操作样板代码
- 统一实体映射方式
- 用
LINQ表达查询 - 管理数据库结构变更
它特别适合:
- 常规业务系统
- 后台管理系统
- 以 CRUD 为主的 Web API 项目
第二章 必须先掌握的三个核心概念
入门 EF Core 时,最重要的不是先背一堆 API,而是先抓住这三个对象。
2.1 实体类
实体类通常对应数据库中的表记录。
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
2.2 DbSet<T>
DbSet<T> 可以理解成某张表在代码层面的集合入口。
2.3 DbContext
DbContext 是数据库操作入口,负责:
- 管理实体集合
- 跟踪实体状态
- 执行查询
- 保存变更
示例:
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<User> Users => Set<User>();
}
可以先这样记:
实体类 -> 表中的一条记录
DbSet<T> -> 一张表
DbContext -> 整个数据库操作上下文
第三章 如何把 EF Core 接进 ASP.NET Core
3.1 安装 Provider
如果你用 SQL Server,常见包是:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
如果你用 MySQL 或 PostgreSQL,则需要替换为对应 Provider。
3.2 配置连接串
appsettings.json 示例:
{
"ConnectionStrings": {
"Default": "Server=.;Database=DemoDb;Trusted_Connection=True;TrustServerCertificate=True;"
}
}
3.3 注册 DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
这一步的意义是:把数据库上下文注册进依赖注入容器,这样控制器、服务层后续就可以通过构造器注入使用它。
第四章 最基础的增删改查
4.1 查询
查询全部
var users = await dbContext.Users.ToListAsync();
按条件查单条
var user = await dbContext.Users.FirstOrDefaultAsync(x => x.Id == 1);
按主键查找
var user = await dbContext.Users.FindAsync(1);
4.2 新增
var user = new User
{
Name = "Tom",
Email = "tom@example.com",
CreatedAt = DateTime.UtcNow
};
dbContext.Users.Add(user);
await dbContext.SaveChangesAsync();
4.3 更新
var user = await dbContext.Users.FindAsync(1);
if (user != null)
{
user.Name = "Tommy";
await dbContext.SaveChangesAsync();
}
4.4 删除
var user = await dbContext.Users.FindAsync(1);
if (user != null)
{
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync();
}
4.5 为什么一定要记住 SaveChangesAsync
很多初学者会写了 Add、Remove、修改属性之后忘记调用 SaveChangesAsync(),结果代码看起来改了,数据库却没有变化。
你可以把它理解成“真正提交变更”的动作。
第五章 实体状态跟踪到底在做什么
EF Core 的一个核心机制是变更跟踪。
当你通过上下文查询出实体后,DbContext 会跟踪它:
- 原始值是什么
- 当前值变成了什么
- 它现在是新增、修改还是删除状态
这样在调用 SaveChangesAsync() 时,框架才能自动生成对应的 INSERT、UPDATE、DELETE。
5.1 常见状态
AddedModifiedDeletedUnchangedDetached
5.2 为什么这个机制重要
因为它直接决定:
- 为什么只改属性也能更新数据库
- 为什么跨上下文对象更新时要小心
- 为什么列表查询很多时会有内存和性能成本
第六章 迁移机制非常重要
很多人把 EF Core 只当成 CRUD 工具,但它非常实用的一点其实是迁移。
迁移机制用于管理数据库结构变化,也就是:
- 新增表
- 增加字段
- 修改字段约束
- 新增索引
6.1 常见命令
dotnet ef migrations add InitCreate
dotnet ef database update
这两步通常表示:
- 根据当前实体模型生成迁移文件
- 把迁移应用到数据库
6.2 一个基本认知
迁移不是“魔法自动同步数据库”,而是“基于当前模型生成结构变更脚本并纳入版本管理”。
6.3 为什么迁移很重要
因为真实项目开发中,数据库结构会不断演进。如果没有迁移机制,团队协作和环境同步会非常混乱。
第七章 实体映射:数据注解与 Fluent API
实体和表之间的映射通常有两种主流方式。
7.1 数据注解
using System.ComponentModel.DataAnnotations;
public class User
{
public int Id { get; set; }
[MaxLength(50)]
public string Name { get; set; } = string.Empty;
[MaxLength(100)]
public string Email { get; set; } = string.Empty;
}
数据注解适合简单场景,写法直接。
7.2 Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(entity =>
{
entity.ToTable("users");
entity.HasKey(x => x.Id);
entity.Property(x => x.Name).HasMaxLength(50).IsRequired();
entity.Property(x => x.Email).HasMaxLength(100);
});
}
Fluent API 更适合:
- 复杂映射
- 团队统一配置风格
- 关系配置
- 索引和约束配置
7.3 什么时候更推荐 Fluent API
中大型项目里,通常更推荐把复杂映射放在 Fluent API 中集中管理,因为:
- 结构更清晰
- 不污染实体类
- 更适合复杂关系配置
第八章 关系映射:一对多、导航属性与 Include
真实业务里,很少只有单表。
8.1 一对多示例
public class Order
{
public int Id { get; set; }
public string OrderNo { get; set; } = string.Empty;
public List<OrderItem> Items { get; set; } = new();
}
public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public string ProductName { get; set; } = string.Empty;
public Order? Order { get; set; }
}
8.2 查询关联数据
var order = await dbContext.Orders
.Include(x => x.Items)
.FirstOrDefaultAsync(x => x.Id == 1);
Include 的作用是加载导航属性对应的关联数据。
8.3 不要无脑 Include
这是非常常见的误区。
错误思路:
- 列表接口默认把所有关联都带上
这样很容易导致:
- SQL 变复杂
- 返回数据过大
- 查询性能下降
正确思路是:只加载当前场景真正需要的数据。
第九章 查询时最容易忽视的性能问题
EF Core 很方便,但方便不代表可以忽视性能。
9.1 只查需要的字段
错误示例:
var users = await dbContext.Users.ToListAsync();
如果你只需要列表中的 Id 和 Name,更推荐:
var users = await dbContext.Users
.Select(x => new
{
x.Id,
x.Name
})
.ToListAsync();
9.2 读多写少场景使用 AsNoTracking
var users = await dbContext.Users
.AsNoTracking()
.ToListAsync();
适合:
- 列表查询
- 只读详情页
- 不需要更新实体的查询
9.3 注意 N+1 查询问题
如果你在循环里频繁基于导航或额外查询拿关联数据,就可能造成 N+1 问题。
9.4 不要把 ORM 当万能性能优化工具
当查询极其复杂、报表统计很多、需要对 SQL 做细粒度控制时,可以考虑:
- 手写 SQL
- 使用
FromSql - 结合
Dapper
第十章 更新数据时要注意什么
10.1 先查后改是最常见写法
var user = await dbContext.Users.FindAsync(id);
if (user == null)
{
return;
}
user.Name = input.Name;
user.Email = input.Email;
await dbContext.SaveChangesAsync();
10.2 不要随手整对象覆盖
在真实业务里,直接把前端传来的对象整包覆盖到实体上,可能会带来:
- 不该改的字段被改掉
- 并发数据被覆盖
- 审计字段丢失
更稳妥的方式是按字段明确赋值。
10.3 批量更新和删除要特别谨慎
如果批量数据量很大,要关注:
- SQL 执行效率
- 事务大小
- 锁范围
- 日志量
第十一章 事务与一致性
在很多简单场景下,一次 SaveChangesAsync() 就自带事务语义。但如果你的业务涉及多个步骤,仍然要有事务意识。
11.1 典型场景
- 扣库存
- 写订单
- 记录日志
- 更新余额
这些步骤如果必须一起成功,就要考虑事务边界。
11.2 一个简单思路
事务的重点不是“我会不会写 BeginTransaction”,而是先搞清楚:
- 哪些操作必须一起成功
- 失败后是否允许部分成功
- 有没有并发写冲突
第十二章 EF Core 适合什么场景,不适合什么场景
12.1 适合
- 以业务 CRUD 为主的系统
- 模型结构相对清晰的后台项目
- 希望统一管理实体和迁移的团队
12.2 不适合单独依赖它解决所有问题
以下场景要更谨慎:
- 极其复杂的报表查询
- 大量数据库特性深度优化
- 需要强控制 SQL 细节的高性能场景
这时候可以考虑与 Dapper 混合使用。
第十三章 初学者最容易踩的坑
13.1 忘记 SaveChangesAsync
代码改了,但数据库没变。
13.2 无脑 Include
导致 SQL 复杂度和返回数据量暴涨。
13.3 查询列表不加 AsNoTracking
在只读场景下徒增跟踪成本。
13.4 把前端对象直接映射回实体整包更新
容易覆盖掉不该改的字段。
13.5 把 EF Core 当成“不需要理解数据库”的理由
这会让你在建模、索引、事务、并发和性能问题上吃很多亏。
第十四章 入门阶段建议掌握到什么程度
如果你刚开始学 EF Core,建议至少做到:
- 能定义实体和
DbContext - 能完成基础增删改查
- 能使用迁移管理数据库结构
- 知道
Include、投影查询、AsNoTracking的区别 - 理解“查询”和“保存变更”是两类不同动作
- 对跟踪、关系加载和查询性能有基础意识
做到这一步,你就已经具备了把 ASP.NET Core 和数据库真正打通的能力。
下一步阅读
- 如果你想把集合查询和表达式能力打得更扎实,阅读
C# 集合与 LINQ 实战 - 如果你想继续补查询性能和事务边界,阅读
EF Core 查询优化与事务实践 - 如果你想把数据库异常与接口错误响应统一起来,阅读
ASP.NET Core 统一异常处理与统一返回
本文小结
EF Core 的价值不只是“写得少”,而是让你用更自然的方式把对象模型、查询表达式和数据库访问串起来。但真正写业务时,仍然必须关注三件事:
- 建模是否合理
- 查询是否高效
- 数据结构变更是否可管理
把这三块同时掌握,EF Core 才会成为你的开发加速器,而不是后期问题制造机。