天天看點

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();
    }
}
           
ABP vNext 不使用工作單元為什麼會抛出異常

既然如此,工作單元是什麼時候被釋放的呢...因為攔截器預設是為倉儲建立了攔截器,是以在獲得到 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();
    }
}

           
ABP vNext 不使用工作單元為什麼會抛出異常

三、解決

解決方法很簡單,在有類似操作的外部通過

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