关于 ef-core
ef-core 为使用 EF Core(.NET 10)构建数据访问层的开发者提供指导,涵盖查询、持久化、迁移和建模模式,并强调投影查询与迁移审查。
主要功能
- 01DbContext 配置与注册
- 02投影查询和分页模式
- 03批量操作与编译查询
- 04拦截器和全局查询筛选器
- 05迁移流程与反模式说明
适用场景
- 01构建 CRUD 数据访问层
- 02优化高频读取查询
- 03管理软删除或多租户
- 04审查并应用数据库迁移
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill ef-core安装后在 Claude Code 中输入/ef-core确认已加载。
详细介绍
EF Core(.NET 10)
核心原则
- EF Core 是默认 ORM — 除非有特定理由(极致性能需求、没有外键约束的旧数据库),否则请使用 EF Core。参见 ADR-003。
- DbContext 是工作单元 — 不要再用另一层工作单元抽象封装它。EF Core 已在内部实现工作单元和 Repository 模式。
- 查询应使用投影 — 使用
.Select()将数据投影到 DTO,而不是加载完整实体。这样可以避免过度获取和 N+1 问题。 - 迁移是代码 — 应像对待其他源代码一样对待迁移:审查并测试,绝不要在生产环境中自动应用。
模式
DbContext 配置
使用 IEntityTypeConfiguration<T>,使实体配置彼此分离且易于发现。
// Persistence/AppDbContext.cs
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
// Persistence/Configurations/OrderConfiguration.cs
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.HasKey(o => o.Id);
builder.Property(o => o.Total)
.HasPrecision(18, 2);
builder.HasMany(o => o.Items)
.WithOne()
.HasForeignKey(i => i.OrderId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasIndex(o => o.CustomerId);
builder.HasIndex(o => o.CreatedAt);
}
}
注册
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
查询投影(避免过度获取)
// GOOD — project to DTO, only loads needed columns
public async Task<OrderResponse?> GetOrderAsync(Guid id, CancellationToken ct)
{
return await db.Orders
.Where(o => o.Id == id)
.Select(o => new OrderResponse(
o.Id,
o.Total,
o.CreatedAt,
o.Items.Select(i => new OrderItemResponse(i.ProductName, i.Quantity, i.Price)).ToList()))
.FirstOrDefaultAsync(ct);
}
分页
public async Task<PagedList<OrderSummary>> ListOrdersAsync(int page, int pageSize, CancellationToken ct)
{
var query = db.Orders
.OrderByDescending(o => o.CreatedAt)
.Select(o => new OrderSummary(o.Id, o.CustomerName, o.Total, o.Status));
var totalCount = await query.CountAsync(ct);
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(ct);
return new PagedList<OrderSummary>(items, totalCount, page, pageSize);
}
ExecuteUpdateAsync / ExecuteDeleteAsync
绕过变更跟踪的批量操作,性能更佳。
// Update without loading entities
await db.Orders
.Where(o => o.Status == OrderStatus.Pending && o.CreatedAt < cutoff)
.ExecuteUpdateAsync(s => s
.SetProperty(o => o.Status, OrderStatus.Expired)
.SetProperty(o => o.UpdatedAt, clock.GetUtcNow()),
ct);
// Delete without loading entities
await db.Orders
.Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < archiveCutoff)
.ExecuteDeleteAsync(ct);
拦截器
使用拦截器处理审计跟踪和软删除等横切关注点。
public class AuditInterceptor(TimeProvider clock) : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
var context = eventData.Context;
if (context is null) return ValueTask.FromResult(result);
var now = clock.GetUtcNow();
foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = now;
entry.Entity.UpdatedAt = now;
break;
case EntityState.Modified:
entry.Entity.UpdatedAt = now;
break;
}
}
return ValueTask.FromResult(result);
}
}
// Registration
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
options
.UseNpgsql(connectionString)
.AddInterceptors(sp.GetRequiredService<AuditInterceptor>()));
编译查询
用于以相同结构频繁执行的热点查询。
public class OrderQueries
{
public static readonly Func<AppDbContext, Guid, CancellationToken, Task<Order?>> GetById =
EF.CompileAsyncQuery((AppDbContext db, Guid id, CancellationToken ct) =>
db.Orders
.Include(o => o.Items)
.FirstOrDefault(o => o.Id == id));
}
// Usage
var order = await OrderQueries.GetById(db, orderId, ct);
值转换器
// Store enum as string
builder.Property(o => o.Status)
.HasConversion<string>()
.HasMaxLength(50);
// Strongly-typed IDs
public readonly record struct OrderId(Guid Value);
builder.Property(o => o.Id)
.HasConversion(id => id.Value, value => new OrderId(value));
迁移工作流
# Create a migration
dotnet ef migrations add AddOrderIndex --project src/MyApp.Infrastructure --startup-project src/MyApp.Api
# Review the generated migration — ALWAYS review before applying
# Check for data loss, index strategy, constraint names
# Apply to development database
dotnet ef database update --project src/MyApp.Infrastructure --startup-project src/MyApp.Api
# Generate SQL script for production
dotnet ef migrations script --idempotent --output migrations.sql
全局查询筛选器
// Soft delete filter
builder.HasQueryFilter(o => !o.IsDeleted);
// Multi-tenant filter
builder.HasQueryFilter(o => o.TenantId == _tenantProvider.TenantId);
// Bypass when needed
var allOrders = await db.Orders.IgnoreQueryFilters().ToListAsync(ct);
反模式
不要用 Repository 封装 DbContext
// BAD — unnecessary abstraction that limits EF Core's power
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id);
Task AddAsync(Order order);
Task SaveChangesAsync();
}
// GOOD — use DbContext directly in handlers
public class Handler(AppDbContext db)
{
public async Task<Order?> Handle(GetOrder.Query query, CancellationToken ct)
{
return await db.Orders.FindAsync([query.Id], ct);
}
}
不要使用延迟加载
// BAD — lazy loading causes N+1 queries and hides data access
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseLazyLoadingProxies()); // DON'T
// GOOD — explicit loading with Include or projection
var orders = await db.Orders
.Include(o => o.Items)
.Where(o => o.CustomerId == customerId)
.ToListAsync(ct);
不要先调用 .ToListAsync() 再在内存中筛选
// BAD — loads ALL orders, filters in C#
var orders = await db.Orders.ToListAsync(ct);
var pending = orders.Where(o => o.Status == OrderStatus.Pending);
// GOOD — filter in the database
var pending = await db.Orders
.Where(o => o.Status == OrderStatus.Pending)
.ToListAsync(ct);
不要忘记等待异步方法
// BAD — missing await, returns before save completes
public void Handle(CreateOrder.Command command)
{
db.Orders.Add(order);
db.SaveChangesAsync(); // Fire-and-forget BUG
}
// GOOD
public async Task Handle(CreateOrder.Command command, CancellationToken ct)
{
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
}
决策指南
| 场景 | 建议 |
|---|---|
| 标准 CRUD | 使用带投影的 DbContext |
| 批量更新(100 行以上) | ExecuteUpdateAsync / ExecuteDeleteAsync |
| 热点读取查询 | 编译查询 |
| 复杂报表查询 | 使用 FromSqlInterpolated 的原始 SQL 或 Dapper |
| 审计跟踪 | SaveChangesInterceptor |
| 多租户 | 全局查询筛选器 |
| 软删除 | 全局查询筛选器 + 拦截器 |
| 强类型 ID | 值转换器 |
| 生产环境迁移 | 幂等 SQL 脚本,绝不自动迁移 |
评论
支持 Markdown