Appearance
第一个 DbContext
学习如何创建和配置你的第一个 Entity Framework Core 数据上下文
📖 什么是 DbContext?
DbContext 是 EF Core 的核心类,代表与数据库的会话(Session)。它负责:
- 📊 管理数据库连接
- 🔍 执行查询和命令
- 📝 跟踪实体状态
- 💾 保存更改到数据库
- 🔄 处理事务
可以把 DbContext 理解为:
- 工作单元(Unit of Work):跟踪所有变更
- 仓储(Repository):提供数据访问接口
- 身份映射(Identity Map):确保同一实体只加载一次
🚀 快速开始
1. 创建 DbContext 类
csharp
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
// 构造函数:接收配置选项
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
// DbSet 属性:表示数据库中的表
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
public DbSet<Customer> Customers => Set<Customer>();
}2. 定义实体类
csharp
public class Product
{
public int Id { get; set; } // 主键
public string Name { get; set; } // 产品名称
public decimal Price { get; set; } // 价格
public string Description { get; set; } // 描述
// 外键
public int CategoryId { get; set; }
// 导航属性
public Category Category { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
// 导航属性(集合)
public ICollection<Product> Products { get; set; } = new List<Product>();
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}3. 注册 DbContext
Program.cs:
csharp
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// 添加 DbContext 服务
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection"));
});
var app = builder.Build();appsettings.json:
json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyAppDb;Trusted_Connection=True;"
}
}4. 创建数据库
bash
# 安装工具包
dotnet add package Microsoft.EntityFrameworkCore.Tools
# 添加迁移
dotnet ef migrations add InitialCreate
# 应用迁移(创建数据库)
dotnet ef database update5. 使用 DbContext
csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
// GET: api/products
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
return await _context.Products.ToListAsync();
}
// GET: api/products/5
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = await _context.Products.FindAsync(id);
if (product == null)
return NotFound();
return product;
}
// POST: api/products
[HttpPost]
public async Task<ActionResult<Product>> CreateProduct(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetProduct),
new { id = product.Id }, product);
}
// PUT: api/products/5
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, Product product)
{
if (id != product.Id)
return BadRequest();
_context.Entry(product).State = EntityState.Modified;
await _context.SaveChangesAsync();
return NoContent();
}
// DELETE: api/products/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
var product = await _context.Products.FindAsync(id);
if (product == null)
return NotFound();
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return NoContent();
}
}🔧 DbContext 配置详解
方法一:Fluent API(推荐)
csharp
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Product 配置
modelBuilder.Entity<Product>(entity =>
{
// 主键
entity.HasKey(e => e.Id);
// 属性配置
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.Price)
.HasColumnType("decimal(18,2)");
entity.Property(e => e.Description)
.HasMaxLength(500);
// 索引
entity.HasIndex(e => e.Name);
entity.HasIndex(e => e.CategoryId);
// 关系配置
entity.HasOne(e => e.Category)
.WithMany(c => c.Products)
.HasForeignKey(e => e.CategoryId)
.OnDelete(DeleteBehavior.Cascade);
});
// Category 配置
modelBuilder.Entity<Category>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(50);
entity.HasIndex(e => e.Name)
.IsUnique();
});
base.OnModelCreating(modelBuilder);
}
}方法二:数据注解(Data Annotations)
csharp
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class Product
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
[MaxLength(500)]
public string Description { get; set; }
[ForeignKey("Category")]
public int CategoryId { get; set; }
public Category Category { get; set; }
}
public class Category
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(50)]
[Index(IsUnique = true)]
public string Name { get; set; }
public ICollection<Product> Products { get; set; } = new List<Product>();
}对比:
| 特性 | Fluent API | 数据注解 |
|---|---|---|
| 功能完整性 | ✅ 100% | ⚠️ 部分 |
| 代码分离 | ✅ 是 | ❌ 否 |
| 可读性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
建议:优先使用 Fluent API,简单配置可用数据注解。
💡 常用配置模式
1. 分离配置类(推荐大型项目)
csharp
// Product 配置类
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products");
builder.HasKey(e => e.Id);
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(100);
builder.Property(e => e.Price)
.HasColumnType("decimal(18,2)");
builder.HasIndex(e => e.Name);
builder.HasOne(e => e.Category)
.WithMany(c => c.Products)
.HasForeignKey(e => e.CategoryId);
}
}
// Category 配置类
public class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
public void Configure(EntityTypeBuilder<Category> builder)
{
builder.ToTable("Categories");
builder.HasKey(e => e.Id);
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(50);
builder.HasIndex(e => e.Name)
.IsUnique();
}
}
// DbContext 中应用配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 自动应用所有配置类
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}优点:
- ✅ 职责分离
- ✅ 易于维护
- ✅ 支持团队并行开发
2. 软删除全局过滤器
csharp
public interface ISoftDelete
{
bool IsDeleted { get; set; }
DateTime? DeletedAt { get; set; }
}
public class Product : ISoftDelete
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
// 软删除字段
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
}
// DbContext 配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 为所有实现 ISoftDelete 的实体添加全局过滤器
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(ISoftDelete).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType)
.HasQueryFilter(ConvertFilterExpression<ISoftDelete>(
e => !e.IsDeleted));
}
}
}
// 辅助方法
private static LambdaExpression ConvertFilterExpression<TInterface>(
Expression<Func<TInterface, bool>> filter)
{
return filter;
}使用:
csharp
// 自动过滤已删除的记录
var products = await context.Products.ToListAsync();
// SQL: SELECT * FROM Products WHERE IsDeleted = 0
// 包含已删除的记录
var allProducts = await context.Products
.IgnoreQueryFilters()
.ToListAsync();3. 审计日志(自动跟踪创建/修改时间)
csharp
public interface IAuditable
{
DateTime CreatedAt { get; set; }
DateTime? ModifiedAt { get; set; }
string CreatedBy { get; set; }
string ModifiedBy { get; set; }
}
public class Product : IAuditable
{
public int Id { get; set; }
public string Name { get; set; }
// 审计字段
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public string CreatedBy { get; set; }
public string ModifiedBy { get; set; }
}
// DbContext 中自动填充
public override async Task<int> SaveChangesAsync(
CancellationToken cancellationToken = default)
{
var entries = ChangeTracker.Entries<IAuditable>();
foreach (var entry in entries)
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = DateTime.UtcNow;
entry.Entity.CreatedBy = GetCurrentUserId();
break;
case EntityState.Modified:
entry.Entity.ModifiedAt = DateTime.UtcNow;
entry.Entity.ModifiedBy = GetCurrentUserId();
break;
}
}
return await base.SaveChangesAsync(cancellationToken);
}
private string GetCurrentUserId()
{
// 从 HttpContext 或 ClaimsPrincipal 获取当前用户 ID
return "system"; // 示例
}🎯 实体配置最佳实践
1. 主键配置
csharp
// 自增整数(默认)
entity.HasKey(e => e.Id);
// GUID
entity.HasKey(e => e.Id);
entity.Property(e => e.Id)
.HasDefaultValueSql("NEWID()");
// 复合主键
entity.HasKey(e => new { e.OrderId, e.ProductId });2. 字符串配置
csharp
// 必填
entity.Property(e => e.Name)
.IsRequired();
// 最大长度
entity.Property(e => e.Name)
.HasMaxLength(100);
// Unicode
entity.Property(e => e.Name)
.IsUnicode(false); // ASCII
// 固定长度
entity.Property(e => e.Code)
.IsFixedLength()
.HasMaxLength(10);3. 数值配置
csharp
// 精度和小数位
entity.Property(e => e.Price)
.HasColumnType("decimal(18,2)");
// 默认值
entity.Property(e => e.IsActive)
.HasDefaultValue(true);
// 计算列
entity.Property(e => e.TotalPrice)
.HasComputedColumnSql("[Quantity] * [UnitPrice]");4. 日期时间配置
csharp
// 默认值
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("GETUTCDATE()");
// 行版本(并发控制)
entity.Property(e => e.RowVersion)
.IsRowVersion();🔗 关系配置详解
一对多(One-to-Many)
csharp
// Order -> OrderItems
modelBuilder.Entity<Order>(entity =>
{
entity.HasMany(o => o.Items)
.WithOne(i => i.Order)
.HasForeignKey(i => i.OrderId)
.OnDelete(DeleteBehavior.Cascade);
});级联删除行为:
| 行为 | 说明 |
|---|---|
Cascade | 删除父实体时自动删除子实体 |
Restrict | 如果有子实体,禁止删除父实体 |
SetNull | 删除父实体时将子实体的外键设为 NULL |
NoAction | 不执行任何操作(数据库默认) |
多对多(Many-to-Many)
csharp
// Product <-> Tags
modelBuilder.Entity<Product>()
.HasMany(p => p.Tags)
.WithMany(t => t.Products)
.UsingEntity<Dictionary<string, object>>(
"ProductTag", // 连接表名
j => j.HasOne<Tag>().WithMany().HasForeignKey("TagId"),
j => j.HasOne<Product>().WithMany().HasForeignKey("ProductId"),
j =>
{
j.HasKey("ProductId", "TagId");
j.ToTable("ProductTags");
});一对一(One-to-One)
csharp
// Customer -> Address
modelBuilder.Entity<Customer>(entity =>
{
entity.HasOne(c => c.Address)
.WithOne(a => a.Customer)
.HasForeignKey<Address>(a => a.CustomerId);
});🧪 测试 DbContext
单元测试
csharp
[Fact]
public async Task AddProduct_ShouldSaveToDatabase()
{
// Arrange
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(databaseName: "TestDb")
.Options;
await using var context = new AppDbContext(options);
var product = new Product { Name = "Test", Price = 99.99m };
// Act
context.Products.Add(product);
await context.SaveChangesAsync();
// Assert
var savedProduct = await context.Products.FindAsync(product.Id);
Assert.NotNull(savedProduct);
Assert.Equal("Test", savedProduct.Name);
}⚠️ 常见陷阱
1. DbContext 线程安全问题
csharp
// ❌ 错误:多个线程共享同一个 DbContext
Parallel.For(0, 100, async i =>
{
await context.Products.AddAsync(new Product { Name = $"Product {i}" });
await context.SaveChangesAsync(); // 可能抛出异常
});
// ✅ 正确:每个操作创建新的 DbContext
await using var scope = serviceScopeFactory.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();2. 忘记调用 SaveChangesAsync
csharp
// ❌ 错误
context.Products.Add(product);
// 忘记调用 SaveChangesAsync,数据不会保存
// ✅ 正确
context.Products.Add(product);
await context.SaveChangesAsync();3. 同步阻塞异步方法
csharp
// ❌ 错误:可能导致死锁
var products = context.Products.ToListAsync().Result;
// ✅ 正确
var products = await context.Products.ToListAsync();📊 性能优化提示
1. 使用 AsNoTracking 进行只读查询
csharp
// 性能提升 20-50%
var products = await context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();2. 批量插入
csharp
// 传统方式(慢)
foreach (var product in products)
{
context.Products.Add(product);
await context.SaveChangesAsync(); // N 次数据库往返
}
// 批量方式(快)
context.Products.AddRange(products);
await context.SaveChangesAsync(); // 1 次数据库往返3. 投影查询减少数据传输
csharp
// 只查询需要的列
var productNames = await context.Products
.Select(p => p.Name)
.ToListAsync();📚 延伸阅读
💡 小结
关键点:
- ✅ DbContext 是 EF Core 的核心类
- ✅ 使用 Fluent API 进行配置(推荐)
- ✅ 分离配置类便于维护
- ✅ 注意线程安全和异步编程
- ✅ 合理使用性能优化技巧
下一步:
- 学习 定义实体类
- 掌握 CRUD 操作
- 深入理解 DbContext 详解