问题

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

Abp vNext 自定义 Ef Core 仓储引发异常的更多相关文章

  1. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(五)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  2. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(一)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  3. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(二)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  4. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(三)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  5. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(四)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  6. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(五)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  7. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(一)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  8. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(二)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  9. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(三)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

随机推荐

  1. 2018 php 面试

    排序算法 快速排序 快速排序是十分常用的高效率的算法,其思想是:先选一个标尺,用它把整个队列过一遍筛选,以保证左边的元素都不大于它,其右边都不小于它 function quickSort($arr){ ...

  2. docker的使用 一容器命令

    Docker容器命令 前提 执行容器的前提是有镜像 . #创建并启动容器 docker run [options] images [command][args] // option 的说明 --nam ...

  3. 第二章Java内存区域与内存溢出异常

    第二章 Java内存区域与内存溢出异常 一.概述 对与Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每个new操作去写delete/free代码,不容易出现内存泄露和内存溢出问 题, ...

  4. Jackson中@JsonProperty等常用注解

    Java生态圈中有很多处理JSON和XML格式化的类库,Jackson是其中比较著名的一个.虽然JDK自带了XML处理类库,但是相对来说比较低级 本文将介绍的Jackson常用注解:精简概述 Jack ...

  5. mvc请求管道(一)

    一.前言 在平常做后台开发的时候,经常会说到请求管道,很多开发者都知道这个,也能说几句,可能没法详细的去介绍,今天就来详细的说一下这个. 二.到达IIS之前 请看下面这个流程图.从用户打开浏览器到请求 ...

  6. [NOIp2009] luogu P1071 潜伏者

    翘课间操和体育课来水博客. 题目描述 CCF的题面贼长,但貌似除了背景以外,每句话都删不掉.不写啦,反正也是Ctrl C的. Solution 显然这是一道码农题. #include<cstdi ...

  7. 记一次EF Core DBContext在Action委托中GC异常的问题.

    今天在开发过程中发现.在SaveChanges的时候偶尔会抛出异常:Cannot access a disposed object. A common cause of this error is d ...

  8. 为什么重写equals必须重写hoshCode的基础分析

    为什么重写equals必须重写hoshCode的基础分析 1.我们先来了解下原生的equals和hashCode代码 原生equals:它判断的是两个对象是否相等 原生hashCode值:它是根据内存 ...

  9. 13.多级代理下Nginx透传真实IP

    1.基于代理(七层负载均衡)情况下 透传客户端的真实IP 环境: 10.0.0.5 proxy_node1 一级代理 10.0.0.6 proxy_node2 二级代理 10.0.0.7 proxy_ ...

  10. Intellij idea 自动生成serialVersionUID

    1 什么是UID 网络间的数据传输最终都是要转化为二进制流的方式进行传输,为了方便转换以及进行验证,我们应该把对角序列化,当实现Seriabizable接口时,UID就是一个必须的属性,可以方便进行版 ...