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 查询。ide

若是将自定义仓储改成 IRepository<TEntity,TKey> 进行注入,是能够与 _courseRepostory 进行关联查询的。post

我在 XXXEntityFrameworkCoreModule 的配置,以及自定义仓储 EfCoreStudentRepository 代码以下。ui

XXXEntityFrameworkCoreModule 代码:this

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);
    }
}

缘由

缘由在异常信息已经说得十分清楚了,这里咱们须要了解两个问题。code

  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> 内部的核心代码:blog

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);
    }
}
相关文章
相关标签/搜索