• Abp vNext 自定义 Ef Core 仓储引发异常


    问题

    在使用自定义 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);
        }
    }
    

    原因

    原因在异常信息已经说得十分清楚了,这里我们需要了解两个问题。

    1. 什么原因导致两个仓储内部的 DbContext 不一致?
    2. 为什么 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;
        }
    }
    

    所以,我们的默认仓储的 dbContextKeyXXXDbContext,我们的自定义仓储继承 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);
        }
    }
    
  • 相关阅读:
    Django视图层进阶、模板层
    【python学习笔记:Django】3.生活需要仪式感——Hello World
    【python学习笔记:Django】2.启动虚拟环境库出错——Windows PowerShell中无法加载文件 xxxScriptsActivate.ps1,因为在此系统上禁止运行脚本
    【python学习笔记:Django】1.开发环境搭建——“三剑客”:python、django、visual studio code
    【window使用技巧】如何提高某盘下载速度?
    【Python应用】爬取LOL皮肤图片(面向过程编程)
    【Python图形界面编程】:PyQt5编程入门(看这篇就够了!)
    android调用百度地图(1)新手环境搭建基础应用教程
    android实现调用科大讯飞语音识别功能详细步骤
    Genymotion模拟器下载安装到连接使用一站式教程
  • 原文地址:https://www.cnblogs.com/myzony/p/11863489.html
Copyright © 2020-2023  润新知