mcpField
E
ef-core

ef-core

github.com/codewithmukesh数据与数据库7913.0k 次安装12 周前更新
代码仓库

关于 ef-core

ef-core 为使用 EF Core(.NET 10)构建数据访问层的开发者提供指导,涵盖查询、持久化、迁移和建模模式,并强调投影查询与迁移审查。

主要功能

  1. 01DbContext 配置与注册
  2. 02投影查询和分页模式
  3. 03批量操作与编译查询
  4. 04拦截器和全局查询筛选器
  5. 05迁移流程与反模式说明

适用场景

  1. 01构建 CRUD 数据访问层
  2. 02优化高频读取查询
  3. 03管理软删除或多租户
  4. 04审查并应用数据库迁移

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill ef-core

安装后在 Claude Code 中输入/ef-core确认已加载。

详细介绍

EF Core(.NET 10)

核心原则

  1. EF Core 是默认 ORM — 除非有特定理由(极致性能需求、没有外键约束的旧数据库),否则请使用 EF Core。参见 ADR-003。
  2. DbContext 是工作单元 — 不要再用另一层工作单元抽象封装它。EF Core 已在内部实现工作单元和 Repository 模式。
  3. 查询应使用投影 — 使用 .Select() 将数据投影到 DTO,而不是加载完整实体。这样可以避免过度获取和 N+1 问题。
  4. 迁移是代码 — 应像对待其他源代码一样对待迁移:审查并测试,绝不要在生产环境中自动应用。

模式

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