Abp vNext 自定義 Ef Core 倉儲引發異常
- 2019 年 11 月 15 日
- 筆記
問題
在使用自定義 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); } }

