mcpField
E
efcore-patterns

efcore-patterns

github.com/aaronontheweb开发1.2k1.6k 次安装今天更新
代码仓库

关于 efcore-patterns

面向使用 Entity Framework Core 的 .NET 开发者,汇集查询、迁移、重试和 DbContext 生命周期等实践。内容涵盖默认无跟踪查询、批量操作及查询拆分。

主要功能

  1. 01默认使用 NoTracking
  2. 02迁移的 CLI 管理
  3. 03专用迁移服务模式
  4. 04瞬时故障重试策略
  5. 05批量更新与删除
  6. 06查询拆分配置

适用场景

  1. 01优化只读查询性能
  2. 02创建和应用数据库迁移
  3. 03处理瞬时数据库故障
  4. 04加载多个导航集合

安装

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

$npx skills add https://github.com/aaronontheweb/dotnet-skills --skill efcore-patterns

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

详细介绍

Entity Framework Core 模式

何时使用此技能

在以下情况使用此技能:

  • 在新项目中设置 EF Core
  • 优化查询性能
  • 管理数据库迁移
  • 将 EF Core 与 .NET Aspire 集成
  • 调试更改跟踪问题
  • 高效加载多个导航集合(查询拆分)

核心原则

  1. 默认使用 NoTracking - 大多数查询都是只读的;需要跟踪时再启用
  2. 绝不要手动编辑迁移 - 始终使用 CLI 命令
  3. 专用迁移服务 - 将迁移执行与应用启动分离
  4. 使用 ExecutionStrategy 进行重试 - 处理数据库瞬时故障
  5. 显式更新 - 使用 NoTracking 时,显式将实体标记为待更新

模式 1:默认使用 NoTracking

配置 DbContext,默认禁用更改跟踪。这可以提升读操作密集型工作负载的性能。

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
        // Disable change tracking by default for better performance on read-only queries
        // Use .AsTracking() explicitly for queries that need to track changes
        ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
    }

    public DbSet<Order> Orders => Set<Order>();
    public DbSet<Customer> Customers => Set<Customer>();
}
NoTracking 生效时

只读查询可正常运行:

// ✅ Fast read - no tracking overhead
var orders = await dbContext.Orders
    .Where(o => o.Status == OrderStatus.Pending)
    .ToListAsync();

写入操作需要显式处理:

// ❌ WRONG - Entity not tracked, SaveChanges does nothing
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // Nothing happens!

// ✅ CORRECT - Explicitly mark entity for update
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
dbContext.Orders.Update(order); // Marks entire entity as modified
await dbContext.SaveChangesAsync();

// ✅ ALSO CORRECT - Use AsTracking() for the query
var order = await dbContext.Orders
    .AsTracking()
    .FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // Works!
何时使用跟踪
场景使用跟踪?原因
在 UI 中显示数据否只读,无需更新
API GET 端点否返回数据,不进行修改
更新单个实体是或显式调用 Update()需要保存更改
包含导航属性的复杂更新是跟踪会处理关系
批量操作否 + ExecuteUpdate效率更高
显式添加/更新模式
public class OrderService
{
    private readonly ApplicationDbContext _db;

    // CREATE - Always use Add (works regardless of tracking)
    public async Task<Order> CreateOrderAsync(Order order)
    {
        _db.Orders.Add(order);
        await _db.SaveChangesAsync();
        return order;
    }

    // UPDATE - Explicitly mark as modified
    public async Task UpdateOrderStatusAsync(Guid orderId, OrderStatus newStatus)
    {
        var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == orderId)
            ?? throw new NotFoundException($"Order {orderId} not found");

        order.Status = newStatus;
        order.UpdatedAt = DateTimeOffset.UtcNow;

        // Explicitly mark as modified since DbContext uses NoTracking by default
        _db.Orders.Update(order);
        await _db.SaveChangesAsync();
    }

    // DELETE - Attach and remove
    public async Task DeleteOrderAsync(Guid orderId)
    {
        var order = new Order { Id = orderId };
        _db.Orders.Remove(order);
        await _db.SaveChangesAsync();
    }
}

模式 2:绝不要手动编辑迁移

重要: 始终使用 EF Core CLI 命令管理迁移。绝不要:

  • 手动编辑迁移文件(在 Up()/Down() 中编写自定义 SQL 除外)
  • 直接删除迁移文件
  • 重命名迁移文件
  • 在项目之间复制迁移
创建迁移
# Create a new migration
dotnet ef migrations add AddCustomerTable \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# With a specific DbContext (if you have multiple)
dotnet ef migrations add AddCustomerTable \
    --context ApplicationDbContext \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api
删除迁移
# Remove the last migration (if not yet applied)
dotnet ef migrations remove \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# NEVER do this:
# rm Migrations/20240101_AddCustomerTable.cs  # ❌ BAD!
应用迁移
# Apply all pending migrations
dotnet ef database update \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# Apply to a specific migration
dotnet ef database update AddCustomerTable \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# Rollback to a previous migration
dotnet ef database update PreviousMigrationName \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api
生成 SQL 脚本
# Generate SQL script for all migrations
dotnet ef migrations script \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api \
    --output migrations.sql

# Generate idempotent script (safe to run multiple times)
dotnet ef migrations script \
    --idempotent \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

模式 3:使用 Aspire 的专用迁移服务

使用专用迁移服务,将迁移执行与主应用分离。这样可以确保:

  • 在应用启动前完成迁移
  • 清晰划分关注点
  • 在测试环境中有序执行数据填充
项目结构
src/
├── MyApp.AppHost/           # Aspire orchestration
├── MyApp.Api/               # Main application
├── MyApp.Infrastructure/    # DbContext and migrations
└── MyApp.MigrationService/  # Dedicated migration runner
MigrationService Program.cs
using MyApp.Infrastructure.Data;
using MyApp.MigrationService;
using Microsoft.EntityFrameworkCore;

var builder = Host.CreateApplicationBuilder(args);

// Add Aspire service defaults
builder.AddServiceDefaults();

// Add PostgreSQL DbContext
var connectionString = builder.Configuration.GetConnectionString("appdb")
    ?? throw new InvalidOperationException("Connection string 'appdb' not found.");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString, npgsqlOptions =>
        npgsqlOptions.MigrationsAssembly("MyApp.Infrastructure")));

// Add the migration worker
builder.Services.AddHostedService<MigrationWorker>();

var host = builder.Build();
host.Run();
MigrationWorker.cs
public class MigrationWorker : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IHostApplicationLifetime _hostApplicationLifetime;
    private readonly ILogger<MigrationWorker> _logger;

    public MigrationWorker(
        IServiceProvider serviceProvider,
        IHostApplicationLifetime hostApplicationLifetime,
        ILogger<MigrationWorker> logger)
    {
        _serviceProvider = serviceProvider;
        _hostApplicationLifetime = hostApplicationLifetime;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Migration service starting...");

        try
        {
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

            await RunMigrationsAsync(dbContext, stoppingToken);

            _logger.LogInformation("Migration service completed successfully.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Migration service failed: {Error}", ex.Message);
            throw;
        }
        finally
        {
            // Stop the application after migrations complete
            _hostApplicationLifetime.StopApplication();
        }
    }

    private async Task RunMigrationsAsync(ApplicationDbContext dbContext, CancellationToken ct)
    {
        // Use execution strategy for transient failure handling
        var strategy = dbContext.Database.CreateExecutionStrategy();

        await strategy.ExecuteAsync(async () =>
        {
            var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync(ct);

            if (pendingMigrations.Any())
            {
                _logger.LogInformation("Applying {Count} pending migrations...",
                    pendingMigrations.Count());

                await dbContext.Database.MigrateAsync(ct);

                _logger.LogInformation("Migrations applied successfully.");
            }
            else
            {
                _logger.LogInformation("No pending migrations. Database is up to date.");
            }
        });
    }
}
AppHost 配置
var builder = DistributedApplication.CreateBuilder(args);

var postgres = builder.AddPostgres("postgres");
var db = postgres.AddDatabase("appdb");

// Migrations run first, then exit
var migrations = builder.AddProject<Projects.MyApp_MigrationService>("migrations")
    .WaitFor(db)
    .WithReference(db);

// API waits for migrations to complete
var api = builder.AddProject<Projects.MyApp_Api>("api")
    .WaitForCompletion(migrations)  // Key: waits for migrations to finish
    .WithReference(db);

模式 4:使用 ExecutionStrategy 处理瞬时故障

对于可能发生瞬时故障的操作,始终使用 CreateExecutionStrategy():

public async Task UpdateWithRetryAsync(Guid id, Action<Order> update)
{
    var strategy = _dbContext.Database.CreateExecutionStrategy();

    await strategy.ExecuteAsync(async () =>
    {
        var order = await _dbContext.Orders
            .AsTracking()
            .FirstOrDefaultAsync(o => o.Id == id);

        if (order is null) return;

        update(order);
        await _dbContext.SaveChangesAsync();
    });
}

重要: 不能将 CreateExecutionStrategy() 与用户发起的事务一起使用。如果需要在事务中重试:

var strategy = _dbContext.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
    // Transaction must be INSIDE the strategy callback
    await using var transaction = await _dbContext.Database.BeginTransactionAsync();

    try
    {
        // ... your operations ...
        await _dbContext.SaveChangesAsync();
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
});

模式 5:使用 ExecuteUpdate/ExecuteDelete 执行批量操作

对于批量操作,请使用 EF Core 7+ 的 ExecuteUpdateAsync 和 ExecuteDeleteAsync,而不是加载实体:

// ❌ SLOW - Loads all entities into memory
var expiredOrders = await _db.Orders
    .Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
    .ToListAsync();

foreach (var order in expiredOrders)
{
    order.Status = OrderStatus.Expired;
}
await _db.SaveChangesAsync();

// ✅ FAST - Single SQL UPDATE statement
await _db.Orders
    .Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(o => o.Status, OrderStatus.Expired)
        .SetProperty(o => o.UpdatedAt, DateTimeOffset.UtcNow));

// ✅ FAST - Single SQL DELETE statement
await _db.Orders
    .Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < cutoffDate)
    .ExecuteDeleteAsync();

常见问题

1. 使用 NoTracking 时忘记更新
// ❌ Silent failure - entity not tracked
var customer = await _db.Customers.FindAsync(id);
customer.Name = "New Name";
await _db.SaveChangesAsync(); // Does nothing!

// ✅ Explicit update
var customer = await _db.Customers.FindAsync(id);
customer.Name = "New Name";
_db.Customers.Update(customer);
await _db.SaveChangesAsync();
2. N+1 查询问题
// ❌ N+1 queries - one query per order
var customers = await _db.Customers.ToListAsync();
foreach (var customer in customers)
{
    var orders = customer.Orders; // Lazy load triggers query
}

// ✅ Eager loading - single query
var customers = await _db.Customers
    .Include(c => c.Orders)
    .ToListAsync();
3. 多个 DbContext 实例导致跟踪冲突
// ❌ Tracking conflict - entity tracked by different context
var order1 = await _db1.Orders.AsTracking().FindAsync(id);
var order2 = await _db2.Orders.AsTracking().FindAsync(id);
order2.Status = OrderStatus.Shipped;
await _db2.SaveChangesAsync(); // May throw or behave unexpectedly

// ✅ Use single context or detach first
_db1.Entry(order1).State = EntityState.Detached;
4. 未始终一致地使用异步操作
// ❌ Blocking call in async context
var orders = _db.Orders.ToList(); // Blocks thread

// ✅ Async all the way
var orders = await _db.Orders.ToListAsync();
5. 在循环中查询
// ❌ Query per iteration
foreach (var orderId in orderIds)
{
    var order = await _db.Orders.FindAsync(orderId);
    // process order
}

// ✅ Single query
var orders = await _db.Orders
    .Where(o => orderIds.Contains(o.Id))
    .ToListAsync();

DI 中的 DbContext 生命周期

ASP.NET Core(默认采用 Scoped 生命周期)
// Scoped = one instance per HTTP request
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString));
后台服务(创建作用域)
public class MyBackgroundService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // ✅ Create scope for each unit of work
        using var scope = _serviceProvider.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

        // ... use dbContext ...
    }
}
Actor / 长生命周期对象(工厂模式)
public class OrderActor : ReceiveActor
{
    private readonly IDbContextFactory<ApplicationDbContext> _dbFactory;

    public OrderActor(IDbContextFactory<ApplicationDbContext> dbFactory)
    {
        _dbFactory = dbFactory;

        ReceiveAsync<GetOrder>(async msg =>
        {
            // Create fresh context for each operation
            await using var db = await _dbFactory.CreateDbContextAsync();
            var order = await db.Orders.FindAsync(msg.OrderId);
            Sender.Tell(order);
        });
    }
}

// Registration
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString));

模式 6:使用查询拆分避免笛卡尔积爆炸

通过 Include() 加载多个导航集合时,EF Core 会生成单个查询,可能导致笛卡尔积爆炸。如果有 10 个订单,每个订单有 10 个商品,结果会是 100 行,而不是 10 + 10 行。

全局配置(适用于大多数情况)

在 DbContext 配置中全局启用查询拆分:

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString, npgsqlOptions =>
        {
            npgsqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
        }));
单个查询覆盖设置

确定单个查询效率更高时,使用单查询:

// Use single query when you know the structure is well-understood
var orders = await dbContext.Orders
    .Include(o => o.Items)
    .Include(o => o.Payments)
    .AsSingleQuery()  // Override global split behavior
    .ToListAsync();
权衡
行为优点缺点
SplitQuery不会产生笛卡尔积爆炸,更适合大型集合需要多次往返,可能存在一致性问题
SingleQuery只需一次往返,具备事务一致性多个集合可能导致笛卡尔积爆炸

建议:全局默认使用 SplitQuery,对于已知单查询效果更好的特定查询,使用 AsSingleQuery() 覆盖设置。

适合优先使用 SingleQuery 的情况
  • 小型且结构明确的导航关系图(2-3 层)
  • 总是需要所有相关数据的查询
  • 往返开销低于笛卡尔积爆炸影响的性能关键路径
适合优先使用 SplitQuery 的情况
  • 大型或难以预测的导航关系图
  • 多对多关系
  • 加载的集合可能并非全部都需要的查询

使用 EF Core 进行测试

内存中提供程序(仅用于单元测试)
// Only for simple unit tests - doesn't match real database behavior
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
    .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
    .Options;

using var context = new ApplicationDbContext(options);
使用 TestContainers 的真实数据库(集成测试)

有关正确的数据库测试方式,请参阅 testcontainers-integration-tests 技能。

// Use real PostgreSQL in container
var container = new PostgreSqlBuilder()
    .WithImage("postgres:16-alpine")
    .Build();

await container.StartAsync();

var options = new DbContextOptionsBuilder<ApplicationDbContext>()
    .UseNpgsql(container.GetConnectionString())
    .Options;

评论

支持 Markdown