ASP.NET Core / EF Core 中为何只有最后注册的 DbContext 能正常查询?
结论
这段代码的问题在于,两次注册共用了同一个 connectionString 变量。两个 lambda 都捕获了它,EF Core 后续执行配置委托时,读到的是变量重新赋值后的值。
因此,两个 DbContext 最终都可能连接到最后赋值的数据库。其中一个上下文查询的表不在这个数据库中,SQL Server 就会报出 Invalid object name。
为两个连接字符串分别声明变量即可修正配置。两个 DbContext 可以继续放在同一个项目、同一个 namespace 中,无须拆分项目或修改实体命名空间。
为什么调整注册顺序会改变结果
第一段注册使用了这个 lambda:
options => options.UseSqlServer(connectionString)
C# lambda 捕获外部变量时,不会自动保存变量当时的字符串值。string 虽然是不可变类型,但保存字符串引用的变量仍然可以重新赋值。
AddDbContext 会保存配置委托,等依赖注入容器后续创建配置选项时再执行。题目中的执行过程如下:
- 将 HR 的连接字符串赋给
connectionString。 - 注册
HrDbContext,配置委托捕获connectionString。 - 将 DocumentLibrary 的连接字符串赋给同一个变量。
- 注册
DocumentLibraryDbContext,配置委托也捕获这个变量。 - 后续执行两个委托时,它们都读到 DocumentLibrary 的连接字符串。
此时,HrDbContext 会到 DocumentLibrary 数据库中查询 Employee 表。这个数据库没有该表,查询就会失败。
交换两段代码后,变量最后保存的是 HR 的连接字符串,出错的上下文也随之交换。原因是两个委托读取了同一个变量的最终值。不同类型的 AddDbContext 注册并没有互相覆盖。
为每个 DbContext 使用独立的连接字符串变量
将注册代码改为:
var hrConnectionString =
builder.Configuration.GetConnectionString("HR")
?? throw new InvalidOperationException(
"Connection string 'HR' not found.");
var documentLibraryConnectionString =
builder.Configuration.GetConnectionString("DocumentLibrary")
?? throw new InvalidOperationException(
"Connection string 'DocumentLibrary' not found.");
builder.Services.AddDbContext<HrDbContext>(options =>
options
.UseSqlServer(hrConnectionString)
.EnableDetailedErrors()
.UseLazyLoadingProxies());
builder.Services.AddDbContext<DocumentLibraryDbContext>(options =>
options
.UseSqlServer(documentLibraryConnectionString)
.EnableDetailedErrors()
.UseLazyLoadingProxies());
两个委托现在分别捕获各自的变量。只要后续不再修改这些变量,另一段注册代码就不会影响当前上下文使用的连接字符串。
EnableDetailedErrors() 和 UseLazyLoadingProxies() 与这次连接字符串混用没有直接关系,可以按项目需要保留。
两个上下文的构造函数继续使用各自对应的泛型选项:
public HrDbContext(DbContextOptions<HrDbContext> options)
: base(options)
{
}
public DocumentLibraryDbContext(
DbContextOptions<DocumentLibraryDbContext> options)
: base(options)
{
}
题目中的 HrDbContext 已经采用了正确写法,DocumentLibraryDbContext 保持同样的结构即可。
确认两个上下文实际连接到了哪里
修改后,可以在测试页面的 OnGet() 中,在执行查询前查看两个上下文的连接目标:
using Microsoft.EntityFrameworkCore;
// 放在 OnGet() 中,执行查询之前。
var hrConnection = hrDbContext.Database.GetDbConnection();
var documentConnection =
documentLibraryDbContext.Database.GetDbConnection();
Console.WriteLine(
$"HR: Server={hrConnection.DataSource}, " +
$"Database={hrConnection.Database}");
Console.WriteLine(
$"DocumentLibrary: Server={documentConnection.DataSource}, " +
$"Database={documentConnection.Database}");
核对输出的服务器和数据库名称是否分别符合配置。这里查看的是上下文实际使用的连接对象,比只检查 appsettings.json 更直接。排查时不必输出完整连接字符串,以免将密码等凭据写入日志。
如果两个上下文都已连接到正确的数据库,仍然出现 Invalid object name,再检查实际表名和 schema。例如,只有 Employee 表确实位于 hr schema 下时,才应这样配置:
builder.ToTable("Employee", "hr");
schema 要以数据库中的实际定义为准,不能根据实体的 namespace 推断。
还有哪些地方不需要改
Invalid object name 表示 SQL Server 在当前查询环境中无法解析指定对象,仅凭这个异常无法判断是否存在 migrations 问题。使用现有数据库的 DB-first 项目应先核对连接目标和表映射。修复这里的变量捕获问题,无须引入 migrations 或调用 EnsureCreated()。
调整 base.OnModelCreating(builder) 的调用位置也无法改变连接字符串。题目中通过显式调用 ApplyConfiguration() 配置实体,每个上下文可以有自己的实体配置。即使项目和 namespace 相同,它们也不会自动共用数据库。
通过构造函数向 Razor Pages 的 PageModel 注入 DbContext 是受支持的用法。题目中的两个查询依次执行,没有并行使用同一个上下文的问题。是否封装服务层可以按业务复杂度决定,修复这次异常无须修改测试页面的注入方式。
备注:内容仅供参考。
