Voocii博客
首页博客AI 热榜作品集读书友链工具关于

© 2026 Voocii. Built with Next.js & tRPC.

GitHubXEmailRSS


EF Core 高级功能介绍

dotnetrick-hayekrick-hayek2025年6月21日

前言

Entity Framework Core 作为 .NET 生态中最主流的 ORM,一些开发者停留在 DbSet.Add()、SaveChanges()、Include() 这些基础 API 上。但当项目规模扩大、并发上升、业务变复杂之后,EF Core 提供的一系列高级特性才真正开始发挥价值——无论是性能优化、多租户隔离,还是审计追踪、并发控制。

本文整理了 14 个实用的 EF Core 高级功能,每个都配有示例代码。示例需要结合具体的实体、数据库提供程序和 EF Core 版本调整后再使用。


一、查询与性能相关

1. 全局查询过滤器 (Global Query Filters)

在模型级别自动给所有查询附加 WHERE 条件,最常见的场景是软删除和多租户隔离。

public class Order
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    public int TenantId { get; set; }
}

public class AppDbContext : DbContext
{
    private readonly int _currentTenantId;

    public AppDbContext(DbContextOptions options, ITenantProvider tenantProvider)
        : base(options)
    {
        _currentTenantId = tenantProvider.GetCurrentTenantId();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>()
            .HasQueryFilter(o => !o.IsDeleted && o.TenantId == _currentTenantId);
    }
}

需要绕过过滤器时可以用 IgnoreQueryFilters()。这个 API 会同时绕过软删除和租户过滤器,必须只允许经过严格授权的后台操作使用,否则可能造成跨租户数据泄露:

var allOrders = await context.Orders
    .IgnoreQueryFilters()
    .ToListAsync();

2. 编译查询 (Compiled Queries)

对高频执行的查询,EF.CompileQuery 可以缓存表达式树的解析和 SQL 翻译过程,减少重复编译开销。

private static readonly Func<AppDbContext, int, Task<Order?>> GetOrderById =
    EF.CompileAsyncQuery((AppDbContext context, int orderId) =>
        context.Orders.FirstOrDefault(o => o.Id == orderId));

// 调用
var order = await GetOrderById(context, 1001);

3. 拆分查询 (Split Queries)

包含多个 Include 的一对多查询容易产生笛卡尔积爆炸,AsSplitQuery() 把一条大 JOIN 拆成多条独立 SQL。

var orders = await context.Orders
    .Include(o => o.Items)
    .Include(o => o.Payments)
    .AsSplitQuery()
    .ToListAsync();

注意:拆分查询会发送多条 SQL。默认情况下,多条查询之间可能观察到不同的数据快照;如果必须保证一致性,需要根据数据库能力显式使用合适的事务隔离级别。同时还要权衡额外的网络往返,不要只因为查询包含 Include 就默认启用。

4. 只读场景的 NoTracking

var readOnlyOrders = await context.Orders
    .AsNoTracking()
    .Where(o => o.Status == OrderStatus.Completed)
    .ToListAsync();

// 需要按主键去重、避免同一实体重复实例化时
var ordersWithIdentityResolution = await context.Orders
    .Include(o => o.Items)
    .AsNoTrackingWithIdentityResolution()
    .ToListAsync();

5. 原始 SQL 与批量操作

// 混合 LINQ 与原始 SQL
var highValueOrders = await context.Orders
    .FromSqlInterpolated($"SELECT * FROM Orders WHERE Amount > {1000}")
    .Where(o => o.Status == OrderStatus.Pending)
    .ToListAsync();

// EF Core 7+ 批量更新/删除,不需要先加载到内存
await context.Orders
    .Where(o => o.Status == OrderStatus.Cancelled)
    .ExecuteDeleteAsync();

await context.Orders
    .Where(o => o.CreatedAt < DateTime.UtcNow.AddYears(-1))
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(o => o.IsArchived, true));

二、建模相关

6. 拥有类型 (Owned Entity Types)

Owned Entity Types 可以把地址等依赖对象映射到宿主实体的表中。它们仍然属于 EF 的实体类型并参与变更跟踪;如果使用 EF Core 8+,更纯粹的值对象也可以考虑 Complex Types。

public class Order
{
    public int Id { get; set; }
    public Address ShippingAddress { get; set; } = null!;
}

public class Address
{
    public string Street { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;
    public string PostalCode { get; set; } = string.Empty;
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>().OwnsOne(o => o.ShippingAddress, a =>
    {
        a.Property(p => p.Street).HasColumnName("ShippingStreet");
        a.Property(p => p.City).HasColumnName("ShippingCity");
    });
}

7. 影子属性 (Shadow Properties)

属性不出现在 C# 类里,只存在于 EF 模型和数据库表中——很适合配合审计字段,避免污染领域模型。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>()
        .Property<DateTime>("CreatedAt");

    modelBuilder.Entity<Order>()
        .Property<string>("CreatedBy");
}

// 读写影子属性
context.Entry(order).Property("CreatedAt").CurrentValue = DateTime.UtcNow;
var createdAt = context.Entry(order).Property<DateTime>("CreatedAt").CurrentValue;

8. 值转换器 (Value Converters)

public enum OrderStatus { Pending, Paid, Shipped, Cancelled }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 枚举存成字符串,而不是默认的 int
    modelBuilder.Entity<Order>()
        .Property(o => o.Status)
        .HasConversion<string>();

    // 自定义转换器:落库前加密,读出后解密
    modelBuilder.Entity<Customer>()
        .Property(c => c.PhoneNumber)
        .HasConversion(
            plain => EncryptionHelper.Encrypt(plain),
            cipher => EncryptionHelper.Decrypt(cipher));

    // JSON 列(EF Core 7+,具体能力取决于数据库提供程序)
    // ToJson() 适用于 owned entity,而不是普通的 List<string>。
    modelBuilder.Entity<Order>()
        .OwnsOne(o => o.Metadata, b => b.ToJson());
}

这里的 Metadata 应该是一个拥有实体,例如包含 Source、Campaign 等属性的对象;如果 Tags 是 List<string>,应根据数据库提供程序选择数组映射、值转换器或单独的实体表。

9. 继承映射策略 (TPH / TPT / TPC)

public abstract class Payment
{
    public int Id { get; set; }
    public decimal Amount { get; set; }
}

public class CreditCardPayment : Payment
{
    public string CardNumberLast4 { get; set; } = string.Empty;
}

public class AlipayPayment : Payment
{
    public string AlipayAccount { get; set; } = string.Empty;
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // TPH(单表继承,默认策略):所有子类共用一张表,靠 Discriminator 列区分
    modelBuilder.Entity<Payment>()
        .HasDiscriminator<string>("PaymentType")
        .HasValue<CreditCardPayment>("CreditCard")
        .HasValue<AlipayPayment>("Alipay");

    // TPT(每个类型一张表,EF Core 5+):每个子类单独建表,通过外键关联基类表
    // modelBuilder.Entity<CreditCardPayment>().ToTable("CreditCardPayments");
    // modelBuilder.Entity<AlipayPayment>().ToTable("AlipayPayments");
}

三、并发与事务

10. 乐观并发控制

public class Order
{
    public int Id { get; set; }
    public decimal Amount { get; set; }

    [Timestamp]
    public byte[] RowVersion { get; set; } = default!;
}

try
{
    order.Amount = 200;
    await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
    var entry = ex.Entries.Single();
    var databaseValues = await entry.GetDatabaseValuesAsync();

    if (databaseValues is null)
    {
        // 记录已被删除
    }
    else
    {
        // 读取数据库最新值,和当前值进行合并;这里只更新 OriginalValues,
        // 并不会自动完成冲突解决或重试。
        entry.OriginalValues.SetValues(databaseValues);
        // 根据业务决定如何合并 CurrentValues,然后再次 SaveChangesAsync()
    }
}

11. 拦截器 (Interceptors)——审计字段的优雅实现

比起重写 SaveChanges,拦截器更适合横切关注点,和依赖注入配合也更自然。

public class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
    private readonly ICurrentUserService _currentUser;

    public AuditSaveChangesInterceptor(ICurrentUserService currentUser)
    {
        _currentUser = currentUser;
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        var context = eventData.Context;
        if (context is null) return base.SavingChangesAsync(eventData, result, cancellationToken);

        var now = DateTime.UtcNow;
        var userId = _currentUser.UserId;

        foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
        {
            if (entry.State == EntityState.Added)
            {
                entry.Property(nameof(IAuditable.CreatedAt)).CurrentValue = now;
                entry.Property(nameof(IAuditable.CreatedBy)).CurrentValue = userId;
            }
            if (entry.State is EntityState.Added or EntityState.Modified)
            {
                entry.Property(nameof(IAuditable.UpdatedAt)).CurrentValue = now;
                entry.Property(nameof(IAuditable.UpdatedBy)).CurrentValue = userId;
            }
        }

        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }
}

// 注册
services.AddDbContext<AppDbContext>((sp, options) =>
{
    options.UseSqlServer(connectionString)
           .AddInterceptors(sp.GetRequiredService<AuditSaveChangesInterceptor>());
});

DbCommandInterceptor 还能拦截原始 SQL 执行,方便做慢查询日志或 SQL 审计。慢查询应该在命令执行完成后判断,不能在 ReaderExecutingAsync 中读取耗时:

public class SlowQueryLoggingInterceptor : DbCommandInterceptor
{
    public override ValueTask<DbDataReader> ReaderExecutedAsync(
        DbCommand command,
        CommandExecutedEventData eventData,
        DbDataReader result,
        CancellationToken cancellationToken = default)
    {
        if (eventData.Duration > TimeSpan.FromSeconds(1))
        {
            Log.Warning("慢查询: {Sql}", command.CommandText);
        }
        return base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
    }
}

12. 执行策略与自动重试

services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString,
        sqlOptions => sqlOptions.EnableRetryOnFailure(
            maxRetryCount: 3,
            maxRetryDelay: TimeSpan.FromSeconds(5),
            errorNumbersToAdd: null)));

// 显式事务必须用 ExecutionStrategy 包裹,否则重试会失败。
// 委托可能被重复执行,其中的业务操作必须具备幂等性。
var strategy = context.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
    await using var transaction = await context.Database.BeginTransactionAsync();
    try
    {
        context.Orders.Add(newOrder);
        await context.SaveChangesAsync();

        context.Inventory.Update(inventoryItem);
        await context.SaveChangesAsync();

        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
});

不要把发送邮件、发布消息等不可回滚的外部副作用直接放进可重试委托中;应使用 Outbox 等方案。更复杂的场景还应考虑“事务已经提交,但客户端未收到结果”这种不确定状态。


四、多租户与工程化

13. 动态数据库连接切换

按用户所在地区或租户动态选择数据库连接,常见于分库分区场景。

public class TenantDbContextFactory : IDbContextFactory<AppDbContext>
{
    private readonly ITenantProvider _tenantProvider;
    private readonly IConfiguration _configuration;

    public TenantDbContextFactory(ITenantProvider tenantProvider, IConfiguration configuration)
    {
        _tenantProvider = tenantProvider;
        _configuration = configuration;
    }

    public AppDbContext CreateDbContext()
    {
        var region = _tenantProvider.GetCurrentRegion(); // 如 "cn-east", "us-west"
        var connectionString = _configuration.GetConnectionString($"Db_{region}");

        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlServer(connectionString)
            .Options;

        return new AppDbContext(options);
    }
}

不建议直接在 OnModelCreating 里按请求租户动态调整 Schema:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>().ToTable("Orders", schema: _tenantProvider.GetSchemaName());
}

EF Core 会缓存模型,OnModelCreating 不会为每个请求重新执行。这样可能导致第一个租户生成的 Schema 被后续租户复用。若确实需要按租户使用不同 Schema,必须实现按租户区分的 IModelCacheKeyFactory,并额外处理迁移、模型缓存数量和连接生命周期。实践中,按租户切换数据库连接通常更简单;单库多租户则更适合使用 TenantId 列配合全局查询过滤器。

14. DbContext 池化 (DbContext Pooling)

高并发 API 场景下,复用 DbContext 实例可以明显减少每次请求创建/销毁的开销。

services.AddDbContextPool<AppDbContext>(options =>
    options.UseSqlServer(connectionString), poolSize: 128);

注意:池化的 DbContext 实例会被复用,构造函数中的请求级初始化不会在每次从池中取出时重新执行。EF Core 没有通用的 DbContext.Initialize() 回调。应避免在池化 Context 中保存请求级状态;如果必须设置租户,应由请求管道在每次解析后显式设置,并确保所有自定义状态都被重置。也可以改用 IDbContextFactory 创建短生命周期 Context。

public class AppDbContext : DbContext
{
    public int CurrentTenantId { get; private set; }

    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    // 每次从池中取出后,由中间件显式调用;不要依赖构造函数设置请求级租户。
    public void SetTenant(int tenantId)
    {
        CurrentTenantId = tenantId;
        ChangeTracker.Clear(); // 清理上一次请求残留的追踪状态
    }
}

// 中间件里
app.Use(async (context, next) =>
{
    var dbContext = context.RequestServices.GetRequiredService<AppDbContext>();
    var tenantId = context.User.GetTenantId();
    dbContext.SetTenant(tenantId);
    await next();
});

小结

这 14 个功能大致可以归为四类:

类别功能解决的问题
查询性能全局过滤器、编译查询、拆分查询、NoTracking、批量操作减少不必要的开销、避免笛卡尔积
建模拥有类型、影子属性、值转换器、继承映射让领域模型更贴近业务而不被数据库结构绑架
并发事务乐观锁、拦截器、执行策略数据一致性与云环境下的瞬时故障容错
工程化动态连接切换、DbContext 池化多租户架构、高并发下的资源复用

实践中最值得优先掌握的是全局查询过滤器 + 拦截器这对组合——前者解决多租户/软删除的一致性问题,后者解决审计字段和横切逻辑。两者结合可以覆盖很多企业级项目的通用需求,但仍需配合权限校验、测试和生产监控。

评论 (0)

暂无评论,快来抢沙发吧!

目录
  • 前言
  • 一、查询与性能相关
  • 二、建模相关
  • 三、并发与事务
  • 四、多租户与工程化
  • 小结