ABP vNext 不使用工作单位为何会抛出非常

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

二手鞋平台接踵整理,炒鞋热迎来“末日风暴”

二手鞋平台接踵整理,炒鞋热迎来“末日风暴”