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

来源:https://www.cnblogs.com/myzony/archive/2019/10/10/11647030.html
-Advertisement-
Play Games

一、問題 該問題經常出現在 ABP vNext 框架當中,要復現該問題十分簡單,只需要你註入一個 倉儲,在任意一個地方調用 方法。 例如上面的測試代碼,不出意外就會提示 異常,具體的異常內容信息: 其實已經說得十分明白了,因為你要調用的 已經被釋放了,所以會出現這個異常信息。 二、原因 2.1 為什 ...


一、問題

該問題經常出現在 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();
    }
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 錯誤提示 使用vscode安裝nuget插件之後出現錯誤: 原因 主要是nuget插件里的拉組件的js文件沒有進行小寫的控制 解決 打開路徑下的文件fetchPackageVersions.js 修改代碼 ...node_fetch_1.default( , utils_1.getFetchOpti ...
  • 本文是對c#中Dictionary內部實現原理進行簡單的剖析。如有表述錯誤,歡迎指正。 主要對照源碼來解析,目前對照源碼的版本是.Net Framwork 4.8,源碼地址。 1. 關鍵的欄位和Entry結構 2. 添加鍵值(Add) 2.1 數組entries和buckets初始化 2.2 添加鍵 ...
  • String類的幾個方法的應用示例: using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks; namespace ConsoleAp ...
  • 有很多人在找客戶過程中遇到了一些問題,有些網站,有些系統裡面經常會把手機號碼的中間四位屏蔽掉 中國的手機號是11位,知道手機的前三位,後四位如何補全中間四位,其實是有軟體可以做到的 前提知道這個號碼是哪個地方的,如果不知道那數據量就太龐大了,先看看軟體圖 因為涉及到號碼的隱私我就塗掉了,使用很簡單, ...
  • //實體類 [Table("invoiceinfo", Schema = "obs")] public class invoice { [Key] public string invoice_num { get; set; } public string merchant_id { get; set... ...
  • 場景 表示時間的數據格式為浮點數,如下: 需要將其格式化為{H:min:s.ms}格式的字元串,效果如下: 註: 博客主頁:https://blog.csdn.net/badao_liumang_qizhi 關註公眾號 霸道的程式猿 獲取編程相關電子書、教程推送與免費下載。 實現 效果 ...
  • 千呼萬喚始出來, asp.net core 3.0 更新簡記 ...
  • 點這裡進入ABP進階教程目錄 更新數據傳輸對象 打開應用層(即JD.CRS.Application)的Course\Dto\GetAllCoursesInput.cs //Course數據傳輸對象(查詢條件) 增加一行代碼 1 using Abp.Application.Services.Dto; ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...