Skip to content

第一个 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 update

5. 使用 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 进行配置(推荐)
  • ✅ 分离配置类便于维护
  • ✅ 注意线程安全和异步编程
  • ✅ 合理使用性能优化技巧

下一步:

  1. 学习 定义实体类
  2. 掌握 CRUD 操作
  3. 深入理解 DbContext 详解

基于 MIT 许可发布