ABP vNext 不使用工作单元为什么会抛出异常

摘要:
我们之所以可以在IRepository<TEntity,TKey>接口上调用LINQ相关的平滑接口,是因为它的父接口IReadOnlyRepository<TEntiy,TKey〕继承了IQueryable<TEntity>接口。}2.2在IQueryable使用的DbContext部分的代码中,我们可以看到最终的IQueryable是通过抽象方法GetQueryable()获得的。跳转到此DbContextProvider的特定实现。您可以看到,它通过IUnitOfWorkManager获取可用的工作单元,然后通过工作单元提供的IServiceProvider解析所需的数据库上下文对象。

一、问题

该问题经常出现在 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 不使用工作单元为什么会抛出异常第1张

既然如此,工作单元是什么时候被释放的呢...因为拦截器默认是为仓储建立了拦截器,所以在获得到 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 不使用工作单元为什么会抛出异常第2张

三、解决

解决方法很简单,在有类似操作的外部通过 [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 不使用工作单元为什么会抛出异常》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Linux服务器性能检测命令集锦cookie的设置与销毁下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

ABP+AdminLTE+Bootstrap Table权限管理系统第六节--abp控制器扩展及json封装以及6种处理时间格式化的方法

返回总目录:ABP+AdminLTE+Bootstrap Table权限管理系统一期   一,控制器AbpController    说完了Swagger ui 我们再来说一下abp对控制器的处理和json的封装.    首先我们定义一个控制器,在新增控制器的时候,控制器会自动继承自AbpController,AbpController对ASP.NET M...

入门系列- ABP 本地化

本地化 ABP的本地化系统与Microsoft.Extensions.Localization无缝集成,并与AspnetCore的本地化文档兼容. 它添加了一些实用功能和增强功能, 使其更易于在实际开发中应用. Volo.Abp.Localization Package 启动模板默认已经安装了此nuget包, 所以在大多数情况下, 你不需要手动安装它. V...

ABP 动态 WebApi 隐藏接口的方法(一)

在ABP实际开发过程中既有可能会遇到不希望将某些方法暴露,那么就需要想办法将接口隐藏起来。 方法一: 通过修改修饰符实现。例如将方法修改为 private,这种方式比较常用。但这种方式就将方法的访问范围限制在当前类。所以再来看下一个。 方法二: 使用 ABP 提供的RemoteServiceAttribute (命名空间:Abp.Application.S...

Abp vNext异常处理的缺陷/改造方案

吐槽Abp Vnext异常处理! 哎呀,是一个喷子 目前项目使用Abp VNext开发,免不了要全局处理异常、提示服务器异常信息。 1. Abp官方异常处理 Abp项目默认会启动内置的异常处理,默认不将异常信息发送到客户端。 在AppModule文件ConfigureServices方法中使用以下代码: Configure<AbpException...

ABP+AdminLTE+Bootstrap Table权限管理系统一期

       初衷    学而时习之,不亦说乎,温顾温知新,可以为师矣.           看懂远不如动手去做,动手做才能发现很多自己不懂的问题,不断的反思和总结,“乐于分享是一种境界的突破”。" 分享是很有意思,也是可以锻炼人的。 分享意味着自我的不断净化提升,不给自己后退的余地。为什么这么说呢?因为:一,分享的就是你所知道的,你所知道的是你投资时间...

五、Abp vNext 基础篇丨博客聚合功能

介绍 业务篇章先从客户端开始写,另外补充一下我给项目起名的时候没多想起的太随意了,结果后面有些地方命名冲突了需要通过手动using不过问题不大。 开工 应用层 根据第三章分层架构里面讲到的现在我们模型已经创建好了,下一步应该是去Application.Contracts层创建我们的业务接口和Dto. public interface IBlog...