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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM