×

深入理解 EF Core 的 DbContext:你可能一直用错的核心组件

独孤求败 独孤求败 发表于2026-06-22 15:40:00 浏览30 评论0

抢沙发发表评论

DbContext 是 EF Core 的核心,正因为如此,理解它的能力范围非常重要。

DbContext 是一个与数据库(通过特定连接字符串)关联的抽象层。

总体来说,它提供了一种通过这个抽象层来操作数据库的简单方式。

在 DbContext 中,你经常可以看到代表数据库表的属性。通常看起来是这样的:

public sealed class AppDbContext : DbContext
{
  public DbSet<User> Users { getset; }
}

通过这些属性,你可以获得非常强大的能力——可以进行查询、添加、删除和更新数据,而且都是通过泛型类型 DbSet<TEntity> 来完成的,无需直接编写 SQL。当然,如果你确实需要,DbContext 也提供了执行原始 SQL 的方式。

从本质上讲,DbContext 是通过特定连接对真实数据库的一个抽象,你通常不需要直接操作数据库。DbContext 几乎已经提供了你避免使用原始 SQL 所需的一切。

但这里存在一个对 DbContext 的常见误解。很多开发者把 DbContext 看作是基础设施层的一部分,然后在它之上再构建一层抽象,比如 Repository 模式和 Unit of Work 模式。

我不隐瞒——几年前我也是这么想的。如果你感兴趣,可以看看我之前的文章。

在 ASP.NET Core 中实现 Repository 和 Unit of Work 模式
Repository 和 Unit of Work 模式旨在在数据访问层和……之间创建一个抽象层
medium.com

但现在,我强烈感觉我们并不是每次都需要这种额外的抽象。DbContext 本身就已经实现了 Repository 模式(通过 DbSet 属性)和 Unit of Work(针对数据库层面)。

如果你想理解为什么 IRepository 和 IUnitOfWork 是多余的,只需要看看下面这张图:

(此处为原文中的示意图)

正如你所看到的,在这里增加一层抽象其实并不合适。

但我大概能猜到你在想什么:如果不使用 IRepository 模式,我们该如何将相关实体分离,让 DbContext 在知识领域上更符合单一职责原则?

通常你会有一个很大的 DbContext,里面包含很多不同的属性(表)。使用 repository 模式时,你会对这些表进行分组或过滤,以遵循单一职责原则。

你可能不知道的是:你可以拥有任意多个 DbContext。如果我们按逻辑模块拆分数据库上下文——比如订单模块——那么它只包含与订单相关的内容,或者任何你想要的数据库结构。

这样就不再只有一个巨大的 AppDbContext,而是可以按领域拆分:UserDbContext、BillingDbContext、FilesDbContext。

基本上会变成这样:

// was
publicclassAppDbContext : DbContext
{
    public DbSet<User> Users { getset; }

    public DbSet<Payment> Payments { getset; }

    public DbSet<Refund> Refunds { getset; }

    public DbSet<File> Files { getset; }

    public DbSet<FileAttribute> FileAttributes { getset; }
}

//became
publicclassUserDbContext : DbContext
{
    public DbSet<User> Users { getset; }
}

publicclassBillingDbContext : DbContext
{
    public DbSet<Payment> Payments { getset; }

    public DbSet<Refund> Refunds { getset; }
}

publicclassFileDbContext : DbContext
{
    public DbSet<File> Files { getset; }
    
    public DbSet<FileAttribute> FileAttributes { getset; }
}

如果你观察上面的 DbContext,会发现一个共同点——它们都通过属性表示数据库表。这种方式非常方便:当你获取某个特定的 DbContext 时,你已经清楚它关联的所有表。

但想象一下,如果你有非常多的属性——上百甚至上千个——或者你需要类似泛型模式的功能,这时你可能希望这样使用:

dbContext.GetDbSet<TEntity>(); //where TEntity any entity

在构建库或 NuGet 包时,这尤其有用,因为你并不知道具体 DbContext 中有哪些属性。

而真正让人开心的是,在 DbContext 中你可以直接使用 Set<TEntity>(),并像操作 DbSet<TEntity> 一样使用它。

所以下面这两行代码本质上是等价的:

db.Users.FirstOrDefaultAsync();

db.Set<User>().FirstOrDefaultAsync();

但要注意:如果实体不存在或不属于该 DbContext,使用 Set<TEntity>() 很容易抛出 InvalidOperationException


如何配置数据库表

我们还可以一起看看如何配置表结构。

最常见的方式是使用实体类上的特性(attributes)。后端会根据这些特性创建表,这些特性被称为 DataAnnotations

示例如下:

using System.ComponentModel.DataAnnotations;

publicsealedclassUser
{
    [Key]
    publiclong Id { getset; }

    [Required]
    [MaxLength(255)]
    publicstring Email { getset; } = null!;

    [Required]
    publicstring PasswordHash { getset; } = null!;
}

我个人并不喜欢在属性上堆满 DataAnnotations,因为当属性很多时,代码会变得非常混乱。

不过在小型或中型项目里,为了快速交付,它仍然是可以接受的。

但 Fluent API(实体配置)几乎在所有情况下都更好。它让代码更清晰,并且能更精细地控制属性(包括 shadow 属性)。

上面的例子可以改写为:

// entity
publicsealedclassUser
{
    publiclong Id { getset; }

    publicstring Email { getset; } = null!;

    publicstring PasswordHash { getset; } = null!;
}

// configuration
publicsealedclassUserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.HasKey(u => u.Id);

        builder.Property(u => u.Email)
            .IsRequired()
            .HasMaxLength(255);

        builder.HasIndex(u => u.Email)
            .IsUnique();

        builder.Property(u => u.PasswordHash)
            .IsRequired();

        //Shadow Property
        builder.Property<byte[]>("RowVersion")
            .IsRowVersion()
            .IsConcurrencyToken();
    }
}

//using configuration
publicclassUserDbContext : DbContext
{
    public DbSet<User> Users { getset; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(UserConfiguration).Assembly);
    }
}

一些实用技巧

使用 EF Core + LINQ 非常方便,但正因为方便,人们往往忽略了它的底层工作方式,以及 LINQ 是如何被转换成 SQL 的。这在处理大型查询时尤其重要。

让我们看一个例子。

假设你有一个实体关联到另一个实体,而那个实体又关联到第三个实体。

结构如下:

public sealedclassUser
{
    publiclong Id { getset; }

    publicstring Email { getset; } = null!;

    public ICollection<File> Files { getset; } = [];
}

publicsealedclassFile
{
    publiclong Id { getset; }

    publicstring FileName { getset; } = null!;

    publicstring BlobUrl { getset; } = null!;

    public ICollection<FileAttribute> Attributes { getset; } = [];
}

publicsealedclassFileAttribute
{
    publiclong Id { getset; }

    publicstring Key { getset; } = null!;

    publicstring Value { getset; } = null!;
}

现在你有一个简单需求——获取某个用户的所有文件属性并显示在 UI 上。

你可能会这样写:

var userWithFiles = await _context.Users
    .Include(user => user.Files)
    .ThenInclude(file => file.Attributes)
    .FirstOrDefaultAsync(user => uuser.Id == userId);

问题在于:这样 EF Core 会生成一个巨大的 JOIN 查询。

如果一个用户有 100 个文件,每个文件 5 个属性,你最终会得到 500 行结果

更好的方式是使用 .AsSplitQuery(),它会执行 3 个干净的 SELECT 语句。

修改后如下:

var userWithFiles = await _context.Users
    .AsSplitQuery()// <-- a new method 
    .Include(u => u.Files)
    .ThenInclude(f => f.Attributes)
    .FirstOrDefaultAsync(u => u.Id == userId);

Find 和 First 的本质区别

看看下面的代码:

var firstUser = db.Users.First(user => user.Id == userId);

var foundUser = db.Users.Find(userId);

Find 用于根据主键查找实体(通常是 ID),而 First 可以根据任意属性查找。

关键区别是:Find 会使用 change tracker(DbContext 内部缓存)。如果实体已经在 DbContext 中被跟踪,它会直接返回,而不会查询数据库。

而 First 每次都会生成查询并访问数据库。

这也是为什么在复杂逻辑(比如重试场景)中更适合使用 First

链式使用 .Where()

很多人不知道 .Where() 可以链式调用,而这种写法能让复杂的 WHERE 条件更易读。

例如:

// don't cool
var users = db.Users.Where(user => user.Email != null && user.Age >= 21 && user.CreatedAt >= new DateTimeOffset(20260205000, TimeSpan.Zero));

// cool 💪
var users1 = db.Users
    .Where(user => user.Email != null)
    .Where(user => user.Age >= 21)
    .Where(user => user.CreatedAt >= new DateTimeOffset(20260205000, TimeSpan.Zero));

🤝 我们在 LinkedIn 上连接吧!
如果你喜欢这篇文章,或者想进一步讨论这些内容,欢迎在 LinkedIn 上联系我!我很想听听你的想法并保持交流。

EF Core 是一个非常强大的工具,但也正因为如此,我们常常没有足够认真地思考如何正确使用和配置它。希望这篇文章能给你带来一些新的启发。


群贤毕至

访客