问题
在使用自定义 Ef Core 仓储和 ABP vNext 注入的默认仓储时,通过两个 Repository 进行 Join 操作,提示 Cannot use multiple DbContext instances within a single query execution. Ensure the query uses a single context instance.
。这个异常信息翻译成中文的大概意思就是,你不能使用两个 DbContext 里面的 DbSet 进行 Join 查询。
如果将自定义仓储改为 IRepository
进行注入,是可以与 _courseRepostory
进行关联查询的。
我在 XXXEntityFrameworkCoreModule
的配置,以及自定义仓储 EfCoreStudentRepository
代码如下。
XXXEntityFrameworkCoreModule
代码:
public class XXXEntityFrameworkCoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext(op =>
{
op.AddDefaultRepositories();
});
Configure(op => op.UsePostgreSql());
}
}
EfCoreStudentRepository
代码:
public class EfCoreStudentRepository : EfCoreRepository, IStudentRepository
{
public EfCoreStudentRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider)
{
}
public Task GetCountWithStudentlIdAsync(long studentId)
{
return DbSet.CountAsync(x=>x.studentId == studentId);
}
}
原因
原因在异常信息已经说得十分清楚了,这里我们需要了解两个问题。
- 什么原因导致两个仓储内部的 DbContext 不一致?
- 为什么 ABP vNext 自己实现的仓储能够进行关联查询呢?
首先我们得知道,仓储内部的 DbContext
是怎么获取的。我们的自定义仓储都会继承 EfCoreRepository
,而这个仓储是实现了 IQuerable
接口的,最终它会通过一个 IDbContextProvider
获得一个可用的 DbContext
。
public class EfCoreRepository : RepositoryBase, IEfCoreRepository
where TDbContext : IEfCoreDbContext
where TEntity : class, IEntity
{
public virtual DbSet DbSet => DbContext.Set();
DbContext IEfCoreRepository.DbContext => DbContext.As();
// 这里可以看到,是通过 IDbContextProvider 来获得 DbContext 的。
protected virtual TDbContext DbContext => _dbContextProvider.GetDbContext();
protected virtual AbpEntityOptions AbpEntityOptions => _entityOptionsLazy.Value;
private readonly IDbContextProvider _dbContextProvider;
private readonly Lazy> _entityOptionsLazy;
// ... 其他代码。
}
下面就是 IDbContextProvider
内部的核心代码:
public class UnitOfWorkDbContextProvider : IDbContextProvider where TDbContext : IEfCoreDbContext
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConnectionStringResolver _connectionStringResolver;
// ... 其他代码。
public TDbContext GetDbContext()
{
var unitOfWork = _unitOfWorkManager.Current;
if (unitOfWork == null)
{
throw new AbpException("A DbContext can only be created inside a unit of work!");
}
var connectionStringName = ConnectionStringNameAttribute.GetConnStringName();
var connectionString = _connectionStringResolver.Resolve(connectionStringName);
// 会构造一个 Key,而这个 Key 刚好是泛型类型的 FullName。
var dbContextKey = $"{typeof(TDbContext).FullName}_{connectionString}";
// 内部是从一个字典当中,根据 dbContextKey 获取 DbContext。如果不存在的话则调用工厂方法创建一个新的 DbContext。
var databaseApi = unitOfWork.GetOrAddDatabaseApi(
dbContextKey,
() => new EfCoreDatabaseApi(
CreateDbContext(unitOfWork, connectionStringName, connectionString)
));
return ((EfCoreDatabaseApi)databaseApi).DbContext;
}
// ... 其他代码。
}
通过以上代码我们就可以知道,ABP vNext 在仓储的内部是通过 IDbContextProvider
中的 TDbContext
泛型,来确定是否构建一个新的 DbContext
对象。
不论是 ABP vNext 针对 IRepository
,还是我们自己实现的自定义仓储,它们最终的实现都是基于 EfCoreRepository
的。而我们 IDbContextProvider
的泛型,也是这个仓储基类提供的,后者的 TDbContext
就是前者的泛型参数。
所以当我们在模块添加 DbContext
的过城中,只要调用了 AddDefaultRepositories()
方法,ABP vNext 就会遍历你提供的 TDbContext
所定义的实体,然后为这些实体建立默认的仓储。
在注入仓储的时候,找到了获得默认仓储实现类型的方法,可以看到这里它使用的是 DefaultRepositoryDbContextType
作为默认的 TDbContext
类型。
protected virtual Type GetDefaultRepositoryImplementationType(Type entityType)
{
var primaryKeyType = EntityHelper.FindPrimaryKeyType(entityType);
// 重点在于构造仓储类型时,传递的 Options.DefaultRepositoryDbContextType 参数,这个参数就是后面 EfCoreRepository 的 TDbContext 泛型。
if (primaryKeyType == null)
{
return Options.SpecifiedDefaultRepositoryTypes
? Options.DefaultRepositoryImplementationTypeWithoutKey.MakeGenericType(entityType)
: GetRepositoryType(Options.DefaultRepositoryDbContextType, entityType);
}
return Options.SpecifiedDefaultRepositoryTypes
? Options.DefaultRepositoryImplementationType.MakeGenericType(entityType, primaryKeyType)
: GetRepositoryType(Options.DefaultRepositoryDbContextType, entityType, primaryKeyType);
}
最后我发现这个就是在模块调用 AddAbpContext
所提供的泛型参数。
public abstract class AbpCommonDbContextRegistrationOptions : IAbpCommonDbContextRegistrationOptionsBuilder
{
// ... 其他代码
protected AbpCommonDbContextRegistrationOptions(Type originalDbContextType, IServiceCollection services)
{
OriginalDbContextType = originalDbContextType;
Services = services;
DefaultRepositoryDbContextType = originalDbContextType;
CustomRepositories = new Dictionary();
ReplacedDbContextTypes = new List();
}
// ... 其他代码
}
public class AbpDbContextRegistrationOptions : AbpCommonDbContextRegistrationOptions, IAbpDbContextRegistrationOptionsBuilder
{
public Dictionary AbpEntityOptions { get; }
public AbpDbContextRegistrationOptions(Type originalDbContextType, IServiceCollection services)
: base(originalDbContextType, services) // 之类调用的就是上面的构造方法。
{
AbpEntityOptions = new Dictionary();
}
}
public static class AbpEfCoreServiceCollectionExtensions
{
public static IServiceCollection AddAbpDbContext(
this IServiceCollection services,
Action optionsBuilder = null)
where TDbContext : AbpDbContext
{
// ... 其他代码。
var options = new AbpDbContextRegistrationOptions(typeof(TDbContext), services);
// ... 其他代码。
return services;
}
}
所以,我们的默认仓储的 dbContextKey
是 XXXDbContext
,我们的自定义仓储继承 EfCoreRepository
,所以它的 dbContextKey
就是 IXXXDbContext
。所以自定义仓储获取到的 DbContext
就与自定义仓储的不一致了,从而提示上述异常。
解决
找到自定自定义仓储的定义,修改它 EfCoreReposiotry
的 TDbContext
泛型参数,变更为 XXXDbContext
即可。
public class EfCoreStudentRepository : EfCoreRepository, IStudentRepository
{
public EfCoreStudentRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider)
{
}
public Task GetCountWithStudentlIdAsync(long studentId)
{
return DbSet.CountAsync(x=>x.studentId == studentId);
}
}