一、問題
該問題經常出現在 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();
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/108802.html
標籤:.NET Core
上一篇:ABP進階教程1 - 條件查詢
下一篇:ABP進階教程2 - 組合查詢
