一、問題 該問題經常出現在 ABP vNext 框架當中,要復現該問題十分簡單,只需要你註入一個 倉儲,在任意一個地方調用 方法。 例如上面的測試代碼,不出意外就會提示 異常,具體的異常內容信息: 其實已經說得十分明白了,因為你要調用的 已經被釋放了,所以會出現這個異常信息。 二、原因 2.1 為什 ...
一、問題
該問題經常出現在 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();
}
}