ABP vNext 不使用工作单元为什么会抛出异常
一、问题
该问题经常出现在 ABP vNext 框架当中,要复现该问题十分简单,只需要你注入一个 IRepository<T,TKey> 仓储,在任意一个地方调用 IRepository<T,TKey>.ToList() 方法。
[Fact]
public void TestMethod()
{
var rep = GetRequiredService<IHospitalRepository>();
var result = rep.ToList();
}
例如上面的测试代码,不出意外就会提示 System.ObjectDisposedException 异常,具体的异常内容信息:
System.ObjectDisposedException : Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
其实已经说得十分明白了,因为你要调用的 DbContext 已经被释放了,所以会出现这个异常信息。
二、原因
2.1 为什么能够调用 LINQ 扩展?
我们之所以能够在 IRepository<TEntity,TKey> 接口上面,调用 LINQ 相关的流畅接口,是因为其父级接口 IReadOnlyRepository<TEntity,TKey> 继承了 IQueryable<TEntity> 接口。如果使用的是 Entity Framework Core 框架,那么在解析 IRepository<T,Key> 的时候,我们得到的是一个 EfCoreRepository<TDbContext, TEntity,TKey> 实例。
针对这个实例,类型 EfCoreRepository<TDbContext, TEntity> 则是它的基类型,继续跳转到其基类 RepositoryBase<TEntity> 我们就能看到它实现了 IQueryable<T> 接口必备的几个属性。
public abstract class RepositoryBase<TEntity> : BasicRepositoryBase<TEntity>, IRepository<TEntity>
where TEntity : class, IEntity
{
// ... 忽略的代码。
public virtual Type ElementType => GetQueryable().ElementType;
public virtual Expression Expression => GetQueryable().Expression;
public virtual IQueryProvider Provider => GetQueryable().Provider;
// ... 忽略的代码。
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<TEntity> GetEnumerator()
{
return GetQueryable().GetEnumerator();
}
protected abstract IQueryable<TEntity> GetQueryable();
// ... 忽略的代码。
}
2.2 IQueryable 使用的 DbContext
上一个小节的代码中,我们可以看出最后的 IQueryable<TEntity> 是通过抽象方法 GetQueryable() 取得的。这个抽象方法,在 EF Core 当中的实现如下。
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>();
protected virtual TDbContext DbContext => _dbContextProvider.GetDbContext();
private readonly IDbContextProvider<TDbContext> _dbContextProvider;
// ... 忽略的代码。
public EfCoreRepository(IDbContextProvider<TDbContext> dbContextProvider)
{
_dbContextProvider = dbContextProvider;
// ... 忽略的代码。
}
// ... 忽略的代码。
protected override IQueryable<TEntity> GetQueryable()
{
return DbSet.AsQueryable();
}
// ... 忽略的代码。
}
所以我们就可以知道,当调用 IQueryable<TEntity>.ToList() 方法时,实际是使用的 IDbContextProvider<TDbContext> 解析出来的数据库上下文对象。
跳转到这个 DbContextProvider 的具体实现,可以看到他是通过 IUnitOfWorkManager(工作单元管理器) 得到可用的工作单元,然后通过工作单元提供的 IServiceProvider 解析所需要的数据库上下文对象。
public class UnitOfWorkDbContextProvider<TDbContext> : IDbContextProvider<TDbContext>
where TDbContext : IEfCoreDbContext
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public UnitOfWorkDbContextProvider(
IUnitOfWorkManager unitOfWorkManager)
{
_unitOfWorkManager = unitOfWorkManager;
}
// ... 上述代码有所精简。
public TDbContext GetDbContext()
{
var unitOfWork = _unitOfWorkManager.Current;
// ... 忽略部分代码。
// 重点在 CreateDbContext() 方法内部。
var databaseApi = unitOfWork.GetOrAddDatabaseApi(
dbContextKey,
() => new EfCoreDatabaseApi<TDbContext>(
CreateDbContext(unitOfWork, connectionStringName, connectionString)
));
return ((EfCoreDatabaseApi<TDbContext>)databaseApi).DbContext;
}
private TDbContext CreateDbContext(IUnitOfWork unitOfWork, string connectionStringName, string connectionString)
{
// ... 忽略部分代码。
using (DbContextCreationContext.Use(creationContext))
{
var dbContext = CreateDbContext(unitOfWork);
// ... 忽略部分代码。
return dbContext;
}
}
private TDbContext CreateDbContext(IUnitOfWork unitOfWork)
{
return unitOfWork.Options.IsTransactional
? CreateDbContextWithTransaction(unitOfWork)
// 重点 !!!
: unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();
}
public TDbContext CreateDbContextWithTransaction(IUnitOfWork unitOfWork)
{
// ... 忽略部分代码。
if (activeTransaction == null)
{
// 重点 !!!
var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();
// ... 忽略部分代码。
return dbContext;
}
else
{
// ... 忽略部分代码。
// 重点 !!!
var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();
// ... 忽略部分代码。
return dbContext;
}
}
}
2.3 DbContext 和工作单元的销毁
可以看到,仓储使用到的数据库上下文对象是通过工作单元的 IServiceProvider 进行解析的。回想之前关于工作单元的文章讲解,不论是手动开启工作单元,还是通过拦截器或者特性的方式开启,最终都是使用的 IUnitOfWorkManager.Begin() 进行构建的。
public class UnitOfWorkManager : IUnitOfWorkManager, ISingletonDependency
{
// ... 省略的不相关的代码。
private readonly IHybridServiceScopeFactory _serviceScopeFactory;
// ... 省略的不相关的代码。
public IUnitOfWork Begin(UnitOfWorkOptions options, bool requiresNew = false)
{
// ... 省略的不相关的代码。
var unitOfWork = CreateNewUnitOfWork();
// ... 省略的不相关的代码。
return unitOfWork;
}
// ... 省略的不相关的代码。
private IUnitOfWork CreateNewUnitOfWork()
{
var scope = _serviceScopeFactory.CreateScope();
try
{
// ... 省略的不相关的代码。
// 所以 IUnitOfWork 里面获得的 ServiceProvider 是一个子容器。
var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
// ... 省略的不相关的代码。
// 工作单元被释放的动作。
unitOfWork.Disposed += (sender, args) =>
{
_ambientUnitOfWork.SetUnitOfWork(outerUow);
// 子容器被释放时,通过子容器解析的 DbContext 也被释放了。
scope.Dispose();
};
return unitOfWork;
}
catch
{
scope.Dispose();
throw;
}
}
}
工作单元的 ServiceProvider 是通过继承 IServiceProviderAccessor 得到的,也就是说在构建工作单元的时候,这个 Provider 就是工作单元管理器创建的子容器。
那么回到之前的代码,我们得知 DbContext 是通过工作单元的 ServiceProvider 创建的,当工作单元被释放的时候,也会连带这个子容器被释放。那么我们之前解析出来的 DbContext ,也就会随着子容器的释放而被释放。如果要验证上述猜想,只需要编写类似代码即可。
[Fact]
public void TestMethod()
{
using (var scope = GetRequiredService<IServiceProvider>().CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<IHospitalDbContext>();
scope.Dispose();
}
}

既然如此,工作单元是什么时候被释放的呢...因为拦截器默认是为仓储建立了拦截器,所以在获得到 DbContext 的时候,拦截器已经将之前的 DbContext 释放掉了。
public override void Intercept(IAbpMethodInvocation invocation)
{
if (!UnitOfWorkHelper.IsUnitOfWorkMethod(invocation.Method, out var unitOfWorkAttribute))
{
invocation.Proceed();
return;
}
// 我在这里...
using (var uow = _unitOfWorkManager.Begin(CreateOptions(invocation, unitOfWorkAttribute)))
{
invocation.Proceed();
uow.Complete();
}
}
要验证 DbContext 是随工作单元一起释放,也十分简单,编写以下代码即可进行测试。
[Fact]
public void TestMethod()
{
var rep = GetRequiredService<IHospitalRepository>();
var mgr = GetRequiredService<IUnitOfWorkManager>();
using (var uow = mgr.Begin())
{
var count = rep.Count();
uow.Dispose();
uow.Complete();
}
}

三、解决
解决方法很简单,在有类似操作的外部通过 [UnitOfWork] 特性或者 IUnitOfManager.Begin 开启一个新的工作单元即可。
[Fact]
public void TestMethod()
{
var rep = GetRequiredService<IHospitalRepository>();
var mgr = GetRequiredService<IUnitOfWorkManager>();
using (var uow = mgr.Begin())
{
var count = rep.Count();
uow.Complete();
}
}
ABP vNext 不使用工作单元为什么会抛出异常的更多相关文章
- [Abp vNext 源码分析] - 4. 工作单元
一.简要说明 统一工作单元是一个比较重要的基础设施组件,它负责管理整个业务流程当中涉及到的数据库事务,一旦某个环节出现异常自动进行回滚处理. 在 ABP vNext 框架当中,工作单元被独立出来作为一 ...
- 手工搭建基于ABP的框架 - 工作单元以及事务管理
一个业务功能往往不只由一次数据库请求(或者服务调用)实现.为了功能的完整性,我们希望如果该功能执行一半时出错,则撤销前面已执行的改动.在数据库层面上,事务管理实现了这种完整性需求.在ABP中,一个完整 ...
- ABP(现代ASP.NET样板开发框架)系列之12、ABP领域层——工作单元(Unit Of work)
点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之12.ABP领域层——工作单元(Unit Of work) ABP是“ASP.NET Boilerplate Pr ...
- ABP框架 - 工作单元
文档目录 本节内容: 简介 在ABP中管理连接和事务 约定的工作单元 UnitOfWork 特性 IUnitOfWorkManager 工作单元详情 禁用工作单元 非事务性工作单元 工作单元方法调用另 ...
- ABP理论学习之工作单元(Unit of Work)
返回总目录 本篇目录 公共连接和事务管理方法 ABP中的连接和事务管理 仓储类 应用服务 工作单元 工作单元详解 关闭工作单元 非事务的工作单元 工作单元方法调用其它 工作单元作用域 自动保存 IRe ...
- 解析ABP框架中的事务处理和工作单元,ABP事务处理
通用连接和事务管理方法连接和事务管理是使用数据库的应用程序最重要的概念之一.当你开启一个数据库连接,什么时候开始事务,如何释放连接...诸如此类的. 正如大家都知道的,.Net使用连接池(connec ...
- 基于DDD的.NET开发框架 - ABP工作单元(Unit of Work)
返回ABP系列 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应 ...
- ABP的工作单元
http://www.aspnetboilerplate.com/Pages/Documents/Unit-Of-Work 工作单元位于领域层. ABP的数据库连接和事务处理: 1,仓储类 ASP ...
- ABP领域层——工作单元(Unit Of work)
ABP领域层——工作单元(Unit Of work) 点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之12.ABP领域层——工作单元(Unit Of work) ...
随机推荐
- 2019DX#5
Solved Pro.ID Title Ratio(Accepted / Submitted) 1001 fraction 辗转相除 4.17%(7/168) ok 1002 three arr ...
- CF 551 E GukiZ and GukiZiana
https://codeforces.com/contest/551/problem/E 分块真强. 题意就是1.区间加,2.询问整个区间中,最远的两个x的距离. 分块,然后,每次找位子用二分找即可. ...
- 2019 HZNU Winter Training Day 13 Comprehensive Training
A.Jongmah CodeForces-1110D 题意:你在玩一个数字游戏,有一堆写在瓦片上的数字,希望你能组成最多的三元组(三个数字相同,或顺子). 这题用到的方法是动态规划.f[i][j] ...
- LVM的创建及管理
创建及管理LVM分区. Lvm(logical volume manager)逻辑卷管理 作用:动态调整磁盘容量,提高磁盘管理的灵活性. 注意:/boot分区用于存放引导文件,不能基于LVM创建. ...
- 从“HDU 2005 第几天?”谈起
在程序设计中,日期时间的处理经常会遇到.在C语言程序设计的一些教材中会出现如下例子或习题. [例1]第几天? (HDU 2005) 给定一个日期,输出这个日期是该年的第几天. Input输入数据有多组 ...
- Redis集群的离线安装以及原理理解
一.本文主要是记录一下Redis集群在linux系统下离线的安装步骤,毕竟在生产环境下一般都是无法联网的,Redis的集群的Ruby环境安装过程还是很麻烦的,涉及到很多的依赖的安装,所以写了一个文章来 ...
- php装上sqlserver驱动后仍然显示Call to undefined function sqlsrv_connect()问题
今天老师要求本来的php+mysql改为php+sqlserver,在网上搜索了相应的教程,说是“需要下载安装Microsoft Drivers for PHP for SQL Server驱动”,下 ...
- Python3 爬虫之 Scrapy 框架安装配置(一)
博客地址:http://www.moonxy.com 基于 Python 3.6.2 的 Scrapy 爬虫框架使用,Scrapy 的爬虫实现过程请参照本人的另一篇博客:Python3 爬虫之 Scr ...
- 53 (OC) 皮肤切换
1:添加皮肤 1.1:iOS皮肤切换方案 http://www.cocoachina.com/ios/20171012/20762.html 1.2:整合 iOS几种换肤思路和demo https:/ ...
- 003:CSS三大重点之一:盒子模型
目录 1:盒子模型 2:边框: 2.1:合写 2.2:适用于:table系元素.边框合并 3:内边距 4:外边距: 4.1:盒子居中三大条件 4.2:外边距合并.外边距塌陷(父子嵌套)解决方法三种 前 ...