問題
在使用自定義 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<TEntity,TKey>
進行注入,是可以與
_courseRepostory
進行關聯查詢的。
我在
XXXEntityFrameworkCoreModule
的配置,以及自定義倉儲
EfCoreStudentRepository
代碼如下。
XXXEntityFrameworkCoreModule
代碼:
public class XXXEntityFrameworkCoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<XXXDbContext>(op =>
{
op.AddDefaultRepositories();
});
Configure<AbpDbContextOptions>(op => op.UsePostgreSql());
}
}
EfCoreStudentRepository
public class EfCoreStudentRepository : EfCoreRepository<IXXXDbContext, Student, long>, IStudentRepository
{
public EfCoreStudentRepository(IDbContextProvider<IXXXDbContext> dbContextProvider) : base(dbContextProvider)
{
}
public Task<int> GetCountWithStudentlIdAsync(long studentId)
{
return DbSet.CountAsync(x=>x.studentId == studentId);
}
}
原因
原因在異常資訊已經說得十厘清楚了,這裡我們需要了解兩個問題。
- 什麼原因導緻兩個倉儲内部的 DbContext 不一緻?
- 為什麼 ABP vNext 自己實作的倉儲能夠進行關聯查詢呢?
首先我們得知道,倉儲内部的
DbContext
是怎麼擷取的。我們的自定義倉儲都會繼承
EfCoreRepository
,而這個倉儲是實作了
IQuerable<T>
接口的,最終它會通過一個
IDbContextProvider<TDbContext>
獲得一個可用的
DbContext
。
public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IEfCoreRepository<TEntity>
where TDbContext : IEfCoreDbContext
where TEntity : class, IEntity
{
public virtual DbSet<TEntity> DbSet => DbContext.Set<TEntity>();
DbContext IEfCoreRepository<TEntity>.DbContext => DbContext.As<DbContext>();
// 這裡可以看到,是通過 IDbContextProvider 來獲得 DbContext 的。
protected virtual TDbContext DbContext => _dbContextProvider.GetDbContext();
protected virtual AbpEntityOptions<TEntity> AbpEntityOptions => _entityOptionsLazy.Value;
private readonly IDbContextProvider<TDbContext> _dbContextProvider;
private readonly Lazy<AbpEntityOptions<TEntity>> _entityOptionsLazy;
// ... 其他代碼。
}
下面就是
IDbContextProvider<TDbContext>
内部的核心代碼:
public class UnitOfWorkDbContextProvider<TDbContext> : IDbContextProvider<TDbContext> 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<TDbContext>();
var connectionString = _connectionStringResolver.Resolve(connectionStringName);
// 會構造一個 Key,而這個 Key 剛好是泛型類型的 FullName。
var dbContextKey = $"{typeof(TDbContext).FullName}_{connectionString}";
// 内部是從一個字典當中,根據 dbContextKey 擷取 DbContext。如果不存在的話則調用工廠方法建立一個新的 DbContext。
var databaseApi = unitOfWork.GetOrAddDatabaseApi(
dbContextKey,
() => new EfCoreDatabaseApi<TDbContext>(
CreateDbContext(unitOfWork, connectionStringName, connectionString)
));
return ((EfCoreDatabaseApi<TDbContext>)databaseApi).DbContext;
}
// ... 其他代碼。
}
通過以上代碼我們就可以知道,ABP vNext 在倉儲的内部是通過
IDbContextProvider<TDbContext>
中的
TDbContext
泛型,來确定是否建構一個新的
DbContext
對象。
不論是 ABP vNext 針對
IRepository<TEntity,TKey>
,還是我們自己實作的自定義倉儲,它們最終的實作都是基于
EfCoreRepository<TDbContext,TEntity,TKey>
的。而我們
IDbContextProvider<TDbContext>
的泛型,也是這個倉儲基類提供的,後者的
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<TDbContext>
所提供的泛型參數。
public abstract class AbpCommonDbContextRegistrationOptions : IAbpCommonDbContextRegistrationOptionsBuilder
{
// ... 其他代碼
protected AbpCommonDbContextRegistrationOptions(Type originalDbContextType, IServiceCollection services)
{
OriginalDbContextType = originalDbContextType;
Services = services;
DefaultRepositoryDbContextType = originalDbContextType;
CustomRepositories = new Dictionary<Type, Type>();
ReplacedDbContextTypes = new List<Type>();
}
// ... 其他代碼
}
public class AbpDbContextRegistrationOptions : AbpCommonDbContextRegistrationOptions, IAbpDbContextRegistrationOptionsBuilder
{
public Dictionary<Type, object> AbpEntityOptions { get; }
public AbpDbContextRegistrationOptions(Type originalDbContextType, IServiceCollection services)
: base(originalDbContextType, services) // 之類調用的就是上面的構造方法。
{
AbpEntityOptions = new Dictionary<Type, object>();
}
}
public static class AbpEfCoreServiceCollectionExtensions
{
public static IServiceCollection AddAbpDbContext<TDbContext>(
this IServiceCollection services,
Action<IAbpDbContextRegistrationOptionsBuilder> optionsBuilder = null)
where TDbContext : AbpDbContext<TDbContext>
{
// ... 其他代碼。
var options = new AbpDbContextRegistrationOptions(typeof(TDbContext), services);
// ... 其他代碼。
return services;
}
}
是以,我們的預設倉儲的
dbContextKey
是
XXXDbContext
,我們的自定義倉儲繼承
EfCoreRepository<IXXXDbContext,TEntity,TKey>
,是以它的
dbContextKey
就是
IXXXDbContext
。是以自定義倉儲擷取到的
DbContext
就與自定義倉儲的不一緻了,進而提示上述異常。
解決
找到自定自定義倉儲的定義,修改它
EfCoreReposiotry<TDbContext,TEntity,TKey>
的
TDbContext
泛型參數,變更為
XXXDbContext
即可。
public class EfCoreStudentRepository : EfCoreRepository<XXXDbContext, Student, long>, IStudentRepository
{
public EfCoreStudentRepository(IDbContextProvider<XXXDbContext> dbContextProvider) : base(dbContextProvider)
{
}
public Task<int> GetCountWithStudentlIdAsync(long studentId)
{
return DbSet.CountAsync(x=>x.studentId == studentId);
}
}