1. 什么是Entity Framework?
Entity Framework(简称EF)是微软推出的一个开源ORM(对象关系映射)框架,它让.NET开发者可以用面向对象的方式来操作数据库。想象一下,你不再需要写那些繁琐的SQL语句,而是直接操作C#对象,EF会自动帮你把这些对象操作转换成数据库操作。这就像是你和一个翻译官一起工作——你只需要说C#,EF负责把它翻译成SQL。
我第一次接触EF是在2010年,当时还在用ADO.NET手动拼接SQL字符串。记得有次因为一个单引号没转义导致整个系统崩溃,排查了大半天。自从用了EF,这类低级错误再也没出现过。现在EF已经发展到8.0版本,功能越来越强大,但核心思想始终没变:让数据访问变得更简单。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么选择Entity Framework?
2.1 开发效率提升显著
用原生ADO.NET写一个简单的CRUD操作,你需要:
- 创建连接字符串
- 建立SqlConnection
- 编写SqlCommand
- 处理参数化查询
- 手动处理DataReader
- 记得关闭连接
而用EF,同样的操作只需要:
csharp复制var user = new User { Name = "张三" };
context.Users.Add(user);
await context.SaveChangesAsync();
我做过统计,同样的功能,用EF比用ADO.NET少写约60%的代码。特别是在复杂业务场景下,这个优势更加明显。
2.2 强类型带来的安全性
以前用字符串拼接SQL时,最怕SQL注入。EF的强类型查询从根本上杜绝了这个问题:
csharp复制// 错误示范(SQL注入风险)
var sql = $"SELECT * FROM Users WHERE Name = '{userInput}'";
// EF正确做法
var users = context.Users.Where(u => u.Name == userInput).ToList();
去年我们系统做安全审计时,用EF写的模块全部零SQL注入漏洞,而遗留的ADO.NET代码中发现了3处高危漏洞。
2.3 数据库无关性
EF支持多种数据库提供程序:
- SQL Server
- MySQL
- PostgreSQL
- SQLite
- Oracle
我们项目从SQL Server迁移到PostgreSQL时,只改了配置字符串和几个特定的SQL函数调用,95%的数据访问代码无需修改。这种灵活性在云原生时代特别有价值。
3. 环境准备与安装
3.1 开发环境要求
推荐使用最新稳定版:
- Visual Studio 2022 (17.4+)
- .NET 6+ SDK
- SQL Server LocalDB (开发用)
注意:如果你用VS Code,需要安装C#扩展和NuGet包管理器。
3.2 安装Entity Framework Core
通过NuGet包管理器控制台安装:
powershell复制Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
或者使用.NET CLI:
bash复制dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
我建议同时安装这两个包,因为:
- Microsoft.EntityFrameworkCore 是核心包
- Microsoft.EntityFrameworkCore.SqlServer 是SQL Server提供程序
3.3 数据库准备
创建一个简单的测试数据库:
sql复制CREATE DATABASE EFDemo;
GO
USE EFDemo;
GO
CREATE TABLE Users (
Id INT PRIMARY KEY IDENTITY,
Name NVARCHAR(100) NOT NULL,
Email NVARCHAR(100) UNIQUE,
CreatedAt DATETIME2 DEFAULT GETDATE()
);
4. 第一个EF应用程序
4.1 定义数据模型
创建User类:
csharp复制public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
}
4.2 创建DbContext
DbContext是EF的核心,它代表与数据库的会话:
csharp复制public class AppDbContext : DbContext
{
public DbSet<User> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDemo;Trusted_Connection=True;");
}
}
实际项目中,建议把连接字符串放在appsettings.json中。
4.3 基本CRUD操作
创建记录
csharp复制using var context = new AppDbContext();
var newUser = new User
{
Name = "李四",
Email = "lisi@example.com"
};
context.Users.Add(newUser);
await context.SaveChangesAsync();
查询记录
csharp复制// 获取所有用户
var users = await context.Users.ToListAsync();
// 条件查询
var user = await context.Users
.Where(u => u.Name.Contains("李"))
.FirstOrDefaultAsync();
更新记录
csharp复制var user = await context.Users.FindAsync(1);
if (user != null)
{
user.Email = "newemail@example.com";
await context.SaveChangesAsync();
}
删除记录
csharp复制var user = await context.Users.FindAsync(1);
if (user != null)
{
context.Users.Remove(user);
await context.SaveChangesAsync();
}
5. 高级查询技巧
5.1 延迟加载 vs 立即加载
默认情况下,EF使用延迟加载:
csharp复制// 不会立即查询数据库
var query = context.Users.Where(u => u.Name.StartsWith("A"));
// 真正执行查询
var users = await query.ToListAsync();
对于关联数据,可以使用Include立即加载:
csharp复制var orders = await context.Orders
.Include(o => o.User)
.Include(o => o.Items)
.ToListAsync();
我遇到过一个N+1查询问题:循环中访问导航属性导致生成大量SQL查询。用Include一次性加载关联数据后,查询次数从101次降到了1次。
5.2 复杂查询
EF支持各种复杂查询:
csharp复制// 分页查询
var page = await context.Users
.Where(u => u.CreatedAt > DateTime.Now.AddMonths(-1))
.OrderBy(u => u.Name)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// 分组统计
var stats = await context.Users
.GroupBy(u => u.CreatedAt.Date)
.Select(g => new
{
Date = g.Key,
Count = g.Count()
})
.ToListAsync();
5.3 原生SQL查询
当LINQ不能满足需求时,可以用原生SQL:
csharp复制var users = await context.Users
.FromSqlRaw("SELECT * FROM Users WHERE Name LIKE 'A%'")
.ToListAsync();
重要:使用参数化查询防止SQL注入
csharp复制var nameFilter = "A%";
var users = await context.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Name LIKE {nameFilter}")
.ToListAsync();
6. 性能优化实战
6.1 批量操作
普通做法(性能差):
csharp复制foreach (var item in items)
{
context.Add(item);
await context.SaveChangesAsync(); // 每次循环都提交
}
优化做法:
csharp复制foreach (var item in items)
{
context.Add(item);
}
await context.SaveChangesAsync(); // 一次性提交
EF Core 7+新增批量操作API:
csharp复制await context.BulkInsertAsync(items);
6.2 跟踪与非跟踪查询
默认情况下,EF会跟踪查询返回的实体。对于只读场景,可以用AsNoTracking提升性能:
csharp复制var users = await context.Users
.AsNoTracking()
.ToListAsync();
在我的基准测试中,对于包含1000条记录的查询,AsNoTracking能减少约40%的内存使用和30%的查询时间。
6.3 连接池配置
在Startup.cs中配置DbContext池:
csharp复制services.AddDbContextPool<AppDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("Default"));
});
这可以显著减少创建DbContext实例的开销。我们电商项目使用连接池后,高峰期的数据库连接数从800降到了150。
7. 常见问题与解决方案
7.1 并发冲突处理
EF使用乐观并发控制。在模型类中添加时间戳字段:
csharp复制public class User
{
[Timestamp]
public byte[] RowVersion { get; set; }
}
更新时捕获DbUpdateConcurrencyException:
csharp复制try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
// 解决冲突逻辑...
}
7.2 迁移管理
安装EF工具:
bash复制dotnet tool install --global dotnet-ef
创建迁移:
bash复制dotnet ef migrations add InitialCreate
应用迁移:
bash复制dotnet ef database update
提示:团队开发时,建议把迁移文件也纳入版本控制。
7.3 日志记录
查看EF生成的SQL:
csharp复制optionsBuilder.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information);
我们项目中发现一个查询生成了2MB的SQL,通过日志找到后优化成了简单的JOIN查询。
8. 实际项目经验分享
8.1 分层架构中的EF使用
推荐的分层方式:
code复制- Presentation Layer (MVC/WebAPI)
- Application Layer (业务逻辑)
- Infrastructure Layer (EF Core)
- Domain Layer (实体模型)
DbContext应该只在Infrastructure层使用。我见过把DbContext直接注入Controller的项目,后期维护简直是噩梦。
8.2 仓储模式实践
基本仓储接口:
csharp复制public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
实现时可以封装EF的通用操作。但要注意避免"过度抽象",简单的CRUD项目可能不需要仓储模式。
8.3 单元测试策略
使用内存数据库测试:
csharp复制var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(databaseName: "TestDb")
.Options;
using var context = new AppDbContext(options);
// 测试代码...
我们项目要求所有数据访问代码必须有单元测试,内存数据库帮了大忙。但要注意它和真实数据库的行为差异。
