首頁 > 軟體

EntityFrameworkCore之工作單元的封裝

2021-03-16 20:00:32

1. 簡介

工作單元:維護受事務影響的物件列表,並協調物件改變的持久化和解決並行場景的問題

  1. EntityFrameworkCore 中使用 DbContext 封裝了:
    • 實體物件狀態記錄跟蹤
    • 資料庫的互動
    • 資料庫事務
  2. 關於協調物件改變的持久化是通過呼叫 DbContext 的相關方法實現的
  3. 在並行場景下 DbContext 的使用也完全交給了開發者處理,主要靠檔案規範說明 DbContext 的使用。

2. DbContext 生命週期和使用規範

2.1. 生命週期

DbContext 的生命週期建立範例時開始,並在釋放範例時結束。 DbContext 範例旨在用於單個工作單元。 這意味著 DbContext 範例的生命週期通常很短。

使用 Entity Framework Core (EF Core) 時的典型工作單元包括:

  • 建立 DbContext 範例
  • 根據上下文跟蹤實體範例。 實體將在以下情況下被跟蹤
    • 查詢返回
    • 新增附加DbContext
  • 根據需要對所跟蹤的實體進行更改以實現業務規則
  • 呼叫 SaveChangesSaveChangesAsync
    • EF Core 將檢測所做的更改,並將這些更改寫入資料庫。
  • 釋放 DbContext 範例

2.2. 使用規範

  • 使用後釋放 DbContext 非常重要。 這可確保釋放所有非託管資源,並登出任何事件其他勾點(hooks),以防止範例在保持參照時出現記憶體漏失。
  • DbContext 不是執行緒安全的。 不要在執行緒之間共用 DbContext。 請確保在繼續使用 DbContext 範例之前,等待所有非同步呼叫。
  • EF Core 程式碼引發的 InvalidOperationException 可以使 DbContext 進入不可恢復的狀態。
    • 此類異常指示程式錯誤,並且不應該從其中恢復。

2.3. 避免 DbContext 執行緒處理問題

  • Entity Framework Core 不支援在同一 DbContext 範例上執行多個並行操作。
    • 這包括非同步查詢的並行執行以及從多個執行緒進行的任何顯式並行使用。
  • 因此,始終 await 非同步呼叫,或對並行執行的操作使用單獨的 DbContext 範例。
  • EF Core 檢測到嘗試同時使用 DbContext 範例時,將會丟擲異常 InvalidOperationException ,其中包含類:
    • 在上一個操作完成之前,第二個操作已在此 DbContext 中啟動。
      • 使用同一個 DbContext 範例的不同執行緒不保證範例成員是執行緒安全的,因此丟擲此異常。

如果框架沒檢測到並行存取,可能會導致不可預知的行為:應用程式崩潰資料損壞

並行存取 DbContext 範例的常見情況:

  • 非同步操作缺陷

    • 使用非同步方法,EF Core 可以啟動以非阻塞式存取資料庫的操作。 但是,如果呼叫方不等待其中一個方法完成,而是繼續對 DbContext 執行其他操作,則 DbContext 的狀態可能會(並且很可能會)損壞
  • 通過依賴注入隱式共用 DbContext 範例

    • 預設情況下 AddDbContext 擴充套件方法使用有範圍的生命週期來註冊 DbContext 型別。
    • 這樣可以避免在大多數 ASP.NET Core 應用程式中出現並行存取問題
      • 因為在給定時間內只有一個執行緒在執行每個使用者端請求,並且每個請求都有單獨的依賴注入範圍(dependency injection scope)(因此有單獨的 DbContext 範例)。
      • 對於 Blazor Server 託管模型,一個邏輯請求用來維護 Blazor 使用者線路,因此,如果使用預設注入範圍,則每個使用者線路只能提供一個範圍內的 DbContext 範例。
  • 建議

    • 任何顯式的並行執行多個執行緒的程式碼都應確保 DbContext 範例不會同時存取。
    • 使用依賴注入可以通過以下方式實現:
      • DbContext 註冊為範圍的,併為每個執行緒建立範圍的服務提供範例(使用 IServiceScopeFactory)
      • DbContext 註冊為瞬時的(transient)
        • 程式初始化時使用具有 ServiceLifetime 引數的 AddDbContext 方法的過載

參照:

3. 封裝-工作單元

上面的內容來源於官方檔案,結合 使用規範建議 的要求開始進行設計和封裝。

首先宣告:
其實DbContext 的設計是很棒的,對於使用者更自由,更開放
本文分享的關於工作單元的設計和封裝是針對我們經常面臨的特定的場景下對 DbContext取用生命週期的維護進行的,目的是更優美,更方便的完成對工作單元的使用。

3.1. 分析

DbContext 的生存期從建立範例時開始,並在釋放範例時結束。我們對資料庫的操作都需要通過 DbContext 進行實現。簡單粗暴的方式是使用是 new 一個 DbContext 物件,操作完再進行 Dispose ,但這樣對使用者不友好。考慮再三,我認為可以從解決以下幾個問題:

  • DbContext取用的封裝
  • DbContext生命週期維護的封裝
    • 根據 DbContext 的初始化對依賴注入的設定,使用範圍型的方式依賴注入註冊 DbContext
    • 通過範圍型(IServiceScope)的服務提供者(ServiceProvider)控制 DbContext生命週期
  • DbContextCURD進行封裝
    • 採用倉儲的方式對 Insert, Update, Get, Delete 等資料存取操作進行封裝

3.2. 設計

3.2.1. 類圖

classDiagram class AsyncLocalCurrentUnitOfWorkHandleProvider~TDbContext~{ -AsyncLocal~LocalUowWrapper~ AsyncLocalUow$ +GetCurrentUow()$ IUnitOfWorkHandle~TDbContext~ +SetCurrentUow(IUnitOfWorkHandle~TDbContext~ value)$ } class LocalUowWrapper~TDbContext~{ + IUnitOfWorkHandle~TDbContext~ UnitOfWorkHandle + LocalUowWrapper~TDbContext~ Outer } class IUnitOfWorkHandle~TDbContext~{ <<Interface>> + GetActiveUnitOfWork() TDbContext + SaveChange() int + SaveChangeAsync() Task<int> + IsDisposed() bool } UnitOfWorkHandle~TDbContext~ InnerUnitOfWorkHandle~TDbContext~ class IUnitOfWorkManager{ <<Interface>> +Begin() IUnitOfWorkHandle~TDbContext~ +BeginNew() IUnitOfWorkHandle~TDbContext~ } AsyncLocalCurrentUnitOfWorkHandleProvider~TDbContext~ ..> LocalUowWrapper~TDbContext~ AsyncLocalCurrentUnitOfWorkHandleProvider~TDbContext~ ..> UnitOfWorkHandle~TDbContext~ UnitOfWorkHandle~TDbContext~ ..|> IUnitOfWorkHandle~TDbContext~ InnerUnitOfWorkHandle~TDbContext~ ..|> IUnitOfWorkHandle~TDbContext~ UnitOfWorkManager ..|> IUnitOfWorkManager UnitOfWorkManager ..> UnitOfWorkHandle~TDbContext~ UnitOfWorkManager ..> InnerUnitOfWorkHandle~TDbContext~ UnitOfWorkManager ..> AsyncLocalCurrentUnitOfWorkHandleProvider~TDbContext~

3.2.2. 時序圖

sequenceDiagram participant User participant UnitOfWorkManager participant UnitOfWorkHandle participant InnerUnitOfWorkHandle participant AsyncLocalCurrentUnitOfWorkHandleProvider par Begin() 建立 IUnitOfWorkHandle User->>+UnitOfWorkManager: Begin() UnitOfWorkManager->>AsyncLocalCurrentUnitOfWorkHandleProvider: 讀取當前 UnitOfWorkHandle AsyncLocalCurrentUnitOfWorkHandleProvider -->> UnitOfWorkManager: alt 當前不存在 UnitOfWorkHandle UnitOfWorkManager->>UnitOfWorkManager: BeginNew else 當前存在 UnitOfWorkHandle UnitOfWorkManager->>InnerUnitOfWorkHandle: 建立物件 InnerUnitOfWorkHandle-->>UnitOfWorkManager: end UnitOfWorkManager-->>-User: and BeginNew() 建立 IUnitOfWorkHandle User->>+UnitOfWorkManager: BeginNew() UnitOfWorkManager->>UnitOfWorkHandle: 建立物件 UnitOfWorkHandle-->>UnitOfWorkManager: UnitOfWorkManager->>AsyncLocalCurrentUnitOfWorkHandleProvider: 設定當前 UnitOfWorkHandle alt 當前值不為空 AsyncLocalCurrentUnitOfWorkHandleProvider->>AsyncLocalCurrentUnitOfWorkHandleProvider: 構建新 LocalUowWrapper 並將當前值作為其Outer屬性 else 當前值為空 AsyncLocalCurrentUnitOfWorkHandleProvider->>AsyncLocalCurrentUnitOfWorkHandleProvider: 賦值為新構建 LocalUowWrapper end AsyncLocalCurrentUnitOfWorkHandleProvider -->> UnitOfWorkManager: UnitOfWorkManager-->>-User: end User->>+InnerUnitOfWorkHandle: Dispose() InnerUnitOfWorkHandle->>InnerUnitOfWorkHandle: 空實現 InnerUnitOfWorkHandle-->>-User: return User->>+UnitOfWorkHandle: Dispose() UnitOfWorkHandle->>UnitOfWorkHandle: 釋放當前 IServiceScope UnitOfWorkHandle->>AsyncLocalCurrentUnitOfWorkHandleProvider: 清除當前 UnitOfWorkHandle alt 當前值的Outer不為空 AsyncLocalCurrentUnitOfWorkHandleProvider->>AsyncLocalCurrentUnitOfWorkHandleProvider: 將當前值設定為原始值的Outer屬性 else 當前值的Outer為空 AsyncLocalCurrentUnitOfWorkHandleProvider->>AsyncLocalCurrentUnitOfWorkHandleProvider: 當前值設定為: null end AsyncLocalCurrentUnitOfWorkHandleProvider -->> UnitOfWorkHandle: UnitOfWorkHandle-->>-User: return

3.2.3. 說明

  • 使用泛型的方式封裝工作單元相關類,當前生效的 DbContext 顯而易見,存取多個資料庫時互不干擾;
  • IUnitOfWorkManager 負責建立工作單元處理器
  • 通過 IUnitOfWorkHandle<TDbContext> 可存取當前的 DbContext 已完成資料存取,執行事務等相關操作;
  • 靜態類AsyncLocalCurrentUnitOfWorkHandleProvider<TDbContext> 提供 DbContext 封裝型別 IUnitOfWorkHandle<TDbContext> 物件的存取;
    • GetCurrentUow(): IUnitOfWorkHandle<TDbContext>void SetCurrentUow(IUnitOfWorkHandle<TDbContext> value) 負責將當前 IUnitOfWorkHandle<TDbContext> 的包裝型別 LocalUowWrapper 的物件儲存到型別為AsyncLocal<LocalUowWrapper> 的欄位中,保證執行緒隔離;
  • LocalUowWrapper<TDbContext> 型別提供了 Outer 屬性用於處理當工作單元處理器在巢狀方法中穿梭時,保證當前的工作單元處理器是設計者需要的;
  • InnerUnitOfWorkHandle<TDbContext> 的實現是:
    • 應對場景是:
      • 外部存在工作單元時使用該工作單元;
      • 外部不存在時需要建立工作單元;
    • 此實現類不是 DbContext 物件的代理,而是為了巢狀接力;
    • 也就是通過 UnitOfWorkManager 物件呼叫 Begin() 時在兩種場景下返回不同型別的物件,詳細參見原始碼部分。

3.3. 原始碼

3.3.1. 工作單元

  1. IUnitOfWorkManagerUnitOfWorkManager

    using Microsoft.EntityFrameworkCore;
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace EntityFramework.UnitOfWorks
    {
        public interface IUnitOfWorkManager
        {
            IUnitOfWorkHandle<TDbContext> Begin<TDbContext>() where TDbContext : DbContext;
    
            IUnitOfWorkHandle<TDbContext> BeginNew<TDbContext>() where TDbContext : DbContext;
        }
    }
    
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading;
    
    namespace EntityFramework.UnitOfWorks.Impl
    {
        public class UnitOfWorkManager : IUnitOfWorkManager
        {
            private readonly IServiceProvider _serviceProvider;
    
            public UnitOfWorkManager(IServiceProvider serviceProvider)
            {
                _serviceProvider = serviceProvider;
            }
    
            public IUnitOfWorkHandle<TDbContext> Begin<TDbContext>() where TDbContext : DbContext
            {
                var uowHander = AsyncLocalCurrentUnitOfWorkHandleProvider<TDbContext>.Current;
                if (uowHander == null)
                {
                    return BeginNew<TDbContext>();
                }
                else
                {
                    return new InnerUnitOfWorkHandle<TDbContext>();
                }
            }
    
            public IUnitOfWorkHandle<TDbContext> BeginNew<TDbContext>() where TDbContext : DbContext
            {
                IServiceScope serviceScope = _serviceProvider.CreateScope();
                var uowHander = new UnitOfWorkHandle<TDbContext>(serviceScope);
                AsyncLocalCurrentUnitOfWorkHandleProvider<TDbContext>.Current = uowHander;
    
                return uowHander;
            }
        }
    }
    
  2. IUnitOfWorkHandle<TDbContext>, UnitOfWorkHandle<TDbContext>InnerUnitOfWorkHandle<TDbContext>

    using Microsoft.EntityFrameworkCore;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace EntityFramework.UnitOfWorks
    {
        public interface IUnitOfWorkHandle<TDbContext> : IDisposable where TDbContext:DbContext
        {
            TDbContext GetActiveUnitOfWork();
    
            int SaveChange();
    
            Task<int> SaveChangeAsync();
    
            bool IsDisposed { get; }
        }
    }
    
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace EntityFramework.UnitOfWorks.Impl
    {
        public class UnitOfWorkHandle<TDbContext> : IUnitOfWorkHandle<TDbContext> where TDbContext : DbContext
        {
            private readonly IServiceScope _serviceScope;
    
            public bool IsDisposed { get; private set; }
    
            public UnitOfWorkHandle(IServiceScope serviceScope)
            {
                _serviceScope = serviceScope;
            }
    
            public TDbContext GetActiveUnitOfWork()
            {
                return _serviceScope.ServiceProvider.GetRequiredService<TDbContext>();
            }
    
            public void Dispose()
            {
                _serviceScope.Dispose();
                IsDisposed = true;
                // 清空當前 Handle 或回到 OuterHandle
                AsyncLocalCurrentUnitOfWorkHandleProvider<TDbContext>.Current = null;
    
            }
    
            public int SaveChange()
            {
                var dbContext = GetActiveUnitOfWork();
                if (dbContext == null)
                {
                    return 0;
                }
                return dbContext.SaveChanges();
            }
    
            public async Task<int> SaveChangeAsync()
            {
                var dbContext = GetActiveUnitOfWork();
                if (dbContext == null)
                {
                    return await Task.FromResult(0);
                }
                return await dbContext.SaveChangesAsync(CancellationToken.None);
            }
        }
    }
    
    using Microsoft.EntityFrameworkCore;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace Nuctech.TrDevice.EntityFramework.UnitOfWorks.Impl
    {
        public class InnerUnitOfWorkHandle<TDbContext> : IUnitOfWorkHandle<TDbContext> where TDbContext : DbContext
        {
            public bool IsDisposed { get; private set; }
    
            public void Dispose()
            {
                IsDisposed = true;
            }
    
            public TDbContext GetActiveUnitOfWork()
                => AsyncLocalCurrentUnitOfWorkHandleProvider<TDbContext>.Current?.GetActiveUnitOfWork();
    
            public int SaveChange()
            {
                return 0;
            }
    
            public Task<int> SaveChangeAsync()
            {
                return Task.FromResult(0);
            }
        }
    }
    
  3. AsyncLocalCurrentUnitOfWorkHandleProvider<TDbContext>LocalUowWrapper<TDbContext>

    • 由於這兩個類是強關聯的,所以這裡將LocalUowWrapper<TDbContext> 定義為其內部類 LocalUowWrapper
    using Microsoft.EntityFrameworkCore;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading;
    
    namespace EntityFramework.UnitOfWorks.Impl
    {
        public class AsyncLocalCurrentUnitOfWorkHandleProvider<TDbContext> where TDbContext : DbContext
        {
            private static readonly AsyncLocal<LocalUowWrapper> AsyncLocalUow = new AsyncLocal<LocalUowWrapper>();
    
            public static UnitOfWorkHandle<TDbContext> Current
            {
                get { return GetCurrentUow(); }
                set { SetCurrentUow(value); }
            }
    
    
    
            private static UnitOfWorkHandle<TDbContext> GetCurrentUow()
            {
                var uow = AsyncLocalUow.Value?.UnitOfWorkHandle;
                if (uow == null)
                {
                    return null;
                }
    
                if (uow.IsDisposed)
                {
                    AsyncLocalUow.Value = null;
                    return null;
                }
    
                return uow;
            }
    
            private static void SetCurrentUow(UnitOfWorkHandle<TDbContext> value)
            {
                lock (AsyncLocalUow)
                {
                    if (value == null)
                    {
                        if (AsyncLocalUow.Value == null)
                        {
                            return;
                        }
    
                        if (AsyncLocalUow.Value.Outer == null)
                        {
                            AsyncLocalUow.Value.UnitOfWorkHandle = null;
                            AsyncLocalUow.Value = null;
                            return;
                        }
                        var oldValue = AsyncLocalUow.Value;
                        AsyncLocalUow.Value = AsyncLocalUow.Value.Outer;
                        oldValue.Outer = null;
                    }
                    else
                    {
                        if (AsyncLocalUow.Value?.UnitOfWorkHandle == null)
                        {
                            if (AsyncLocalUow.Value != null)
                            {
                                AsyncLocalUow.Value.UnitOfWorkHandle = value;
                            }
    
                            AsyncLocalUow.Value = new LocalUowWrapper(value);
                            return;
                        }
                        var newValue = new LocalUowWrapper(value) { Outer = AsyncLocalUow.Value };
                        AsyncLocalUow.Value = newValue;
                    }
                }
            }
    
            private class LocalUowWrapper
            {
                public UnitOfWorkHandle<TDbContext> UnitOfWorkHandle { get; set; }
    
                public LocalUowWrapper Outer { get; set; }
    
                public LocalUowWrapper(UnitOfWorkHandle<TDbContext> unitOfWorkHandle)
                {
                    UnitOfWorkHandle = unitOfWorkHandle;
                }
            }
        }
    }
    

3.3.2. 單元測試

  • 以下單元測試基本涵蓋了常見的工作單元使用情況;
  • 事務時直接使用 DbContext 啟動的事務,更復雜的情況請檢視官方檔案;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using EntityFramework.UnitOfWorks;
using EntityFramework.UnitOfWorks.Impl;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace EntityFramework.UnitOfWorkTest
{
        public class UnitOfWorkTests
    {
        public const string ConnectString = @"Data Source=(localdb)MSSQLLocalDB;Initial Catalog=TestDatabase";

        private readonly IServiceProvider _serviceProvider;
        private readonly IUnitOfWorkManager _unitOfWorkManager;

        public UnitOfWorkTests()
        {
            IServiceCollection services = new ServiceCollection();
            services.AddDbContext<PersionDbContext>(options => options.UseSqlServer(ConnectString));
            services.AddTransient<IUnitOfWorkManager, UnitOfWorkManager>();
            _serviceProvider = services.BuildServiceProvider();

            _unitOfWorkManager = _serviceProvider.GetRequiredService<IUnitOfWorkManager>();

        }

        /// <summary>
        /// 正常操作
        /// </summary>
        /// <returns></returns>
        [Fact]
        public async Task ShouldNormalOperation()
        {
            using (var uowHandle = _unitOfWorkManager.Begin<PersionDbContext>())
            {
                var lists = uowHandle.GetActiveUnitOfWork().Persions.ToList();
                //清理
                lists.ForEach(u => uowHandle.GetActiveUnitOfWork().Persions.Remove(u));
                uowHandle.SaveChange();
            }

            await AddUser("張三");
            await AddUser("李四");
            await AddUser("王五");

            using (var uowHandle = _unitOfWorkManager.Begin<PersionDbContext>())
            {
                var lists = uowHandle.GetActiveUnitOfWork().Persions.ToList();
                lists.Count.ShouldBe(3);
                //清理
                lists.ForEach(u => uowHandle.GetActiveUnitOfWork().Persions.Remove(u));
                uowHandle.SaveChange();
            }

            async Task AddUser(string name)
            {
                using (var uowHandle = _unitOfWorkManager.Begin<PersionDbContext>())
                {
                    uowHandle.GetActiveUnitOfWork().Persions.Add(new Persion() { Name = name });
                    await uowHandle.SaveChangeAsync();
                }
            }
        }

        /// <summary>
        /// 超出使用範圍使用工作單元
        /// </summary>
        [Fact]
        public void ShouldNotUseUow()
        {
            var uowHandle = _unitOfWorkManager.Begin<PersionDbContext>();
            uowHandle.Dispose();
            Assert.Throws<ObjectDisposedException>(() => uowHandle.GetActiveUnitOfWork().Persions.ToList());
        }

        /// <summary>
        /// 工作單元巢狀時,當前工作單IUnitOfWorkHandle和DbContext的實際情況
        /// </summary>
        [Fact]
        public void ShouldAcrossMutiFunction()
        {
            using (var uowHandle = _unitOfWorkManager.Begin<PersionDbContext>())
            {
                var outerDbContext = uowHandle.GetActiveUnitOfWork();
                uowHandle.ShouldBeOfType<UnitOfWorkHandle<PersionDbContext>>();
                using (var innerUowHandle = _unitOfWorkManager.BeginNew<PersionDbContext>())
                {
                    var innerDbContext = innerUowHandle.GetActiveUnitOfWork();
                    innerUowHandle.GetActiveUnitOfWork().ShouldNotBe(outerDbContext);
                    innerUowHandle.ShouldBeOfType<UnitOfWorkHandle<PersionDbContext>>();
                    innerUowHandle.ShouldNotBe(uowHandle);

                    using (var innerInnerUowHandle = _unitOfWorkManager.BeginNew<PersionDbContext>())
                    {
                        innerInnerUowHandle.ShouldBeOfType<UnitOfWorkHandle<PersionDbContext>>();
                        innerInnerUowHandle.GetActiveUnitOfWork().ShouldNotBe(outerDbContext);
                        innerInnerUowHandle.ShouldNotBe(uowHandle);
                    }
                    innerUowHandle.GetActiveUnitOfWork().ShouldBe(innerDbContext);
                }

                using (var innerUowHandle = _unitOfWorkManager.Begin<PersionDbContext>())
                {
                    innerUowHandle.ShouldBeOfType<InnerUnitOfWorkHandle<PersionDbContext>>();
                    innerUowHandle.GetActiveUnitOfWork().ShouldBe(outerDbContext);
                }
                uowHandle.GetActiveUnitOfWork().ShouldBe(outerDbContext);
            }

        }

        /// <summary>
        /// 使用資料庫事務
        /// </summary>
        /// <param name="isCommit">是否提交資料</param>
        /// <returns></returns>
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public async Task ShouldCommitTransaction(bool isCommit)
        {
            using (var uowHandle = _unitOfWorkManager.Begin<PersionDbContext>())
            {
                var lists = uowHandle.GetActiveUnitOfWork().Persions.ToList();
                lists.ForEach(u => uowHandle.GetActiveUnitOfWork().Persions.Remove(u));
                uowHandle.SaveChange();
            }

            List<string> names = new List<string> { "張三", "李四", "王老五" };

            using (var uowHandle = _unitOfWorkManager.Begin<PersionDbContext>())
            {
                using (var transaction = uowHandle.GetActiveUnitOfWork().Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted))
                {
                    for (int i = 0; i < names.Count; i++)
                    {
                        uowHandle.GetActiveUnitOfWork().Persions.Add(new Persion() { Name = names[i] });
                        //事務期間的SaveChange不會提交到資料庫
                        await uowHandle.SaveChangeAsync();
                    }

                    if (isCommit)
                    {
                        transaction.Commit();
                    }
                    else
                    {
                        transaction.Rollback();
                    }
                }
            }

            using (var uowHandle = _unitOfWorkManager.Begin<PersionDbContext>())
            {
                uowHandle.GetActiveUnitOfWork().Persions.Count().ShouldBe(isCommit ? 3 : 0);
            }
        }
    }
}

4. 封裝-倉儲

4.1. 分析

  • 關於倉儲類的封裝主要是為了更方便的使用工作單元
  • 面向我們經常使用的操作: Select, Insert, Update, Delete 四個方向進行封裝。

4.2. 設計

4.2.1. 類圖

classDiagram class IRepository~TDbContext, TEntity~{ +IUnitOfWorkManager UnitOfWorkManager +TDbContext CurrentDbContext +BeginUnitOfWork() IUnitOfWorkHandle~TDbContext~ +BeginNewUnitOfWork() IUnitOfWorkHandle~TDbContext~ +Insert(TEntity entity) TEntity +GetAll() IQueryable~TEntity~ +GetAllList() List~TEntity~ +Update(TEntity entity) TEntity +Delete(TEntity entity) } EFRepository~TDbContext, TEntity~ ..|> IRepository~TDbContext, TEntity~ EFRepository~TDbContext, TEntity~ ..> AsyncLocalCurrentUnitOfWorkHandleProvider~TDbContext~ EFRepository~TDbContext, TEntity~ ..> AsyncLocalCurrentUnitOfWorkHandleProvider~TDbContext~ EFRepository~TDbContext, TEntity~ ..> IUnitOfWorkManager EFRepository~TDbContext, TEntity~ ..> IUnitOfWorkHandle

4.2.2. 時序圖

sequenceDiagram participant EFRepository participant AsyncLocalCurrentUnitOfWorkHandleProvider participant IUnitOfWorkManager participant IUnitOfWorkHandle par 執行後需要DbContext繼續生效的方法 note right of EFRepository: 比如返回IQueryable的方法 EFRepository->>AsyncLocalCurrentUnitOfWorkHandleProvider: 獲取當前生效的 DbContext AsyncLocalCurrentUnitOfWorkHandleProvider-->>EFRepository: alt 存在 EFRepository->>EFRepository: 使用此上下文執行操作 else 不存在 EFRepository->>EFRepository: 丟擲 ArgumentNullException 異常 end and 執行後DbContext不生效也沒影響的方法 EFRepository->>+IUnitOfWorkManager: Begin()一個工作單元 note right of IUnitOfWorkManager: 建立的工作單元處理器型別受外部影響,前面已介紹 IUnitOfWorkManager->>IUnitOfWorkHandle: IUnitOfWorkHandle-->>IUnitOfWorkManager: IUnitOfWorkManager-->>-EFRepository: Begin()一個工作單元 EFRepository->>IUnitOfWorkHandle: 獲取生效的DbContext並進行操作 IUnitOfWorkHandle-->>EFRepository: EFRepository->>IUnitOfWorkHandle: 釋放資源: Dispose IUnitOfWorkHandle-->>EFRepository: end

4.2.3. 原始碼

  1. IRepository<TDbContext, TEntity>
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using EntityFramework.UnitOfWorks;

namespace EntityFramework.Repositories

    public interface IRepository<TDbContext, TEntity> where TEntity : class where TDbContext : DbContext
    {
        IUnitOfWorkManager UnitOfWorkManager { get; }

        TDbContext CurrentDbContext { get; }

        IUnitOfWorkHandle<TDbContext> BeginUnitOfWork();

        IUnitOfWorkHandle<TDbContext> BeginNewUnitOfWork();

        #region Select/Get/Query

        IQueryable<TEntity> GetAll();

        IQueryable<TEntity> GetAllIncluding(params Expression<Func<TEntity, object>>[] propertySelectors);

        List<TEntity> GetAllList();

        Task<List<TEntity>> GetAllListAsync();

        List<TEntity> GetAllList(Expression<Func<TEntity, bool>> predicate);

        Task<List<TEntity>> GetAllListAsync(Expression<Func<TEntity, bool>> predicate);

        T Query<T>(Func<IQueryable<TEntity>, T> queryMethod);

        TEntity Single(Expression<Func<TEntity, bool>> predicate);

        Task<TEntity> SingleAsync(Expression<Func<TEntity, bool>> predicate);


        TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate);

        Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate);
        #endregion


        TEntity Insert(TEntity entity);

        Task<TEntity> InsertAsync(TEntity entity);

        TEntity Update(TEntity entity);

        Task<TEntity> UpdateAsync(TEntity entity);

        void Delete(TEntity entity);

        Task DeleteAsync(TEntity entity);

    }
}
  1. EFRepository<TDbContext, TEntity>
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using EntityFramework.UnitOfWorks;
using EntityFramework.UnitOfWorks.Impl;

namespace EntityFramework.Repositories.Impl
{
    public class EFRepository<TDbContext, TEntity> : IRepository<TDbContext, TEntity> where TEntity : class where TDbContext : DbContext
    {
        private readonly IUnitOfWorkManager _unitOfWorkManager;

        public EFRepository(IUnitOfWorkManager unitOfWorkManager)
        {
            _unitOfWorkManager = unitOfWorkManager;
        }

        public virtual TDbContext CurrentDbContext => AsyncLocalCurrentUnitOfWorkHandleProvider<TDbContext>.Current?.GetActiveUnitOfWork();

        public IUnitOfWorkManager UnitOfWorkManager => _unitOfWorkManager;

        public IUnitOfWorkHandle<TDbContext> BeginUnitOfWork()
        {
            return _unitOfWorkManager.Begin<TDbContext>();
        }

        public IUnitOfWorkHandle<TDbContext> BeginNewUnitOfWork()
        {
            return _unitOfWorkManager.BeginNew<TDbContext>();
        }

        public TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();
            return uowHander.GetActiveUnitOfWork().Set<TEntity>().FirstOrDefault(predicate);
        }

        public async Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();
            return await uowHander.GetActiveUnitOfWork().Set<TEntity>().FirstOrDefaultAsync(predicate);
        }

        public IQueryable<TEntity> GetAll()
        {
            var context = CurrentDbContext;
            if (context != null)
            {
                return context.Set<TEntity>().AsQueryable();
            }
            throw new ArgumentNullException(nameof(CurrentDbContext));
        }

        public IQueryable<TEntity> GetAllIncluding(params Expression<Func<TEntity, object>>[] propertySelectors)
        {
            var context = CurrentDbContext;
            if (context != null)
            {
                var query = context.Set<TEntity>().AsQueryable();
                if (propertySelectors != null)
                {
                    foreach (var propertySelector in propertySelectors)
                    {
                        query = query.Include(propertySelector);
                    }
                }
                return query;
            }
            throw new ArgumentNullException(nameof(CurrentDbContext));
        }

        public List<TEntity> GetAllList()
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();
            return uowHander.GetActiveUnitOfWork().Set<TEntity>().ToList();

        }

        public List<TEntity> GetAllList(Expression<Func<TEntity, bool>> predicate)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();
            return uowHander.GetActiveUnitOfWork().Set<TEntity>().Where(predicate).ToList();
        }

        public async Task<List<TEntity>> GetAllListAsync()
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();
            return await uowHander.GetActiveUnitOfWork().Set<TEntity>().ToListAsync();
        }

        public async Task<List<TEntity>> GetAllListAsync(Expression<Func<TEntity, bool>> predicate)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();
            return await uowHander.GetActiveUnitOfWork().Set<TEntity>().Where(predicate).ToListAsync();
        }

        public T Query<T>(Func<IQueryable<TEntity>, T> queryMethod)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();
            return queryMethod(GetAll());
        }

        public TEntity Single(Expression<Func<TEntity, bool>> predicate)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();
            return uowHander.GetActiveUnitOfWork().Set<TEntity>().SingleOrDefault(predicate);
        }

        public async Task<TEntity> SingleAsync(Expression<Func<TEntity, bool>> predicate)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();
            return await uowHander.GetActiveUnitOfWork().Set<TEntity>().SingleOrDefaultAsync(predicate);
        }

        public TEntity Insert(TEntity entity)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();

            entity = uowHander.GetActiveUnitOfWork().Set<TEntity>().Add(entity).Entity;

            uowHander.SaveChange();
            return entity;
        }

        public async Task<TEntity> InsertAsync(TEntity entity)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();

            entity = uowHander.GetActiveUnitOfWork().Set<TEntity>().Add(entity).Entity;

            await uowHander.SaveChangeAsync();
            return entity;
        }


        public TEntity Update(TEntity entity)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();

            var context = uowHander.GetActiveUnitOfWork();
            AttachIfNot(context, entity);
            context.Entry(entity).State = EntityState.Modified;

            uowHander.SaveChange();
            return entity;
        }

        public async Task<TEntity> UpdateAsync(TEntity entity)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();

            var context = uowHander.GetActiveUnitOfWork();
            AttachIfNot(context, entity);
            context.Entry(entity).State = EntityState.Modified;

            await uowHander.SaveChangeAsync();
            return entity;

        }

        public void Delete(TEntity entity)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();

            var context = uowHander.GetActiveUnitOfWork();
            AttachIfNot(context, entity);
            context.Set<TEntity>().Remove(entity);

            uowHander.SaveChange();
        }

        public async Task DeleteAsync(TEntity entity)
        {
            using var uowHander = _unitOfWorkManager.Begin<TDbContext>();

            var context = uowHander.GetActiveUnitOfWork();
            AttachIfNot(context, entity);
            context.Set<TEntity>().Remove(entity);

            await uowHander.SaveChangeAsync();
        }

        protected virtual void AttachIfNot(DbContext dbContext, TEntity entity)
        {
            var entry = dbContext.ChangeTracker.Entries().FirstOrDefault(ent => ent.Entity == entity);
            if (entry != null)
            {
                return;
            }
            dbContext.Set<TEntity>().Attach(entity);
        }
    }
}

5. 總結

本片文章目的是分享自己將DbContext的使用封裝為工作單元的心得,希望給大家一點啟發。

歡迎大家留言討論,如果文章有錯誤歡迎指正。


IT145.com E-mail:sddin#qq.com