造輪子之ORM整合

飯勺oO發表於2023-10-08

Dotnet的ORM千千萬,還是喜歡用EF CORE

前面一些基礎完成的差不多了,接下來可以整合資料庫了,官方出品的ORM還是比較香。所以接下來就是來整合EF CORE。

安裝包

首先我們需要安裝一下EF CORE的NUGET包,有如下幾個:

Microsoft.EntityFrameworkCore.Proxies
Microsoft.EntityFrameworkCore.Sqlite
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Tools

其中Microsoft.EntityFrameworkCore.Sqlite我們可以根據我們實際使用的資料庫進行替換。
而Microsoft.EntityFrameworkCore.Proxies則是用於啟用EF中的懶載入模式。.
Microsoft.EntityFrameworkCore.Design和Microsoft.EntityFrameworkCore.Tools則是用於資料庫遷移

DbContext

接下來建立我們的DbContext檔案


namespace Wheel.EntityFrameworkCore
{
    public class WheelDbContext : DbContext
    {
        public WheelDbContext(DbContextOptions<WheelDbContext> options) : base(options)
        {

        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }

}

在Program中新增DbContext


var connectionString = builder.Configuration.GetConnectionString("Default") ?? throw new InvalidOperationException("Connection string 'Default' not found.");

builder.Services.AddDbContext<WheelDbContext>(options =>
    options.UseSqlite(connectionString)
        .UseLazyLoadingProxies()
);

在配置檔案中新增連線字串

"ConnectionStrings": {
  "Default": "Data Source=Wheel.WebApi.Host.db"
}

封裝Repository

在AddDbContext之後,我們就可以在程式中直接注入WheelDbContext來操作我們的資料庫了。但是為了我們以後可能隨時切換ORM,我們還是封裝一層Repository來操作我們的資料庫。
新增IBasicRepository泛型介面:

    public interface IBasicRepository<TEntity, TKey> where TEntity : class 
    {
        Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default);
        Task InsertManyAsync(List<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default);
        Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default);
        Task UpdateAsync(Expression<Func<TEntity, bool>> predicate, Expression<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>> setPropertyCalls, bool autoSave = false, CancellationToken cancellationToken = default);
        Task UpdateManyAsync(List<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default);
        Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default);
        Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default);
        Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default);
        Task DeleteManyAsync(List<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default);
        Task<TEntity?> FindAsync(TKey id, CancellationToken cancellationToken = default);
        Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
        Task<bool> AnyAsync(CancellationToken cancellationToken = default);
        Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
        Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default, params Expression<Func<TEntity, object>>[] propertySelectors);
        Task<List<TSelect>> SelectListAsync<TSelect>(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TSelect>> selectPredicate, CancellationToken cancellationToken = default);
        Task<List<TSelect>> SelectListAsync<TSelect>(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TSelect>> selectPredicate, CancellationToken cancellationToken = default, params Expression<Func<TEntity, object>>[] propertySelectors);
        Task<(List<TSelect> items, long total)> SelectPageListAsync<TSelect>(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TSelect>> selectPredicate, int skip, int take, string orderby = "Id", CancellationToken cancellationToken = default);
        Task<(List<TSelect> items, long total)> SelectPageListAsync<TSelect>(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TSelect>> selectPredicate, int skip, int take, string orderby = "Id", CancellationToken cancellationToken = default, params Expression<Func<TEntity, object>>[] propertySelectors);
        Task<(List<TEntity> items, long total)> GetPageListAsync(Expression<Func<TEntity, bool>> predicate, int skip, int take, string orderby = "Id", CancellationToken cancellationToken = default);
        Task<(List<TEntity> items, long total)> GetPageListAsync(Expression<Func<TEntity, bool>> predicate, int skip, int take, string orderby = "Id", CancellationToken cancellationToken = default, params Expression<Func<TEntity, object>>[] propertySelectors);
        IQueryable<TEntity> GetQueryable(bool noTracking = true);

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

        Task<int> SaveChangeAsync(CancellationToken cancellationToken = default);
        Expression<Func<TEntity, bool>> BuildPredicate(params (bool condition, Expression<Func<TEntity, bool>> predicate)[] conditionPredicates);
    }

    public interface IBasicRepository<TEntity> : IBasicRepository<TEntity, object> where TEntity : class
    {

    }

IBasicRepository<TEntity, TKey>用於單主鍵的表結構,IBasicRepository : IBasicRepository<TEntity, object>用於複合主鍵的表結構。
然後我們來實現一下BasicRepository:


    public class EFBasicRepository<TEntity, TKey> : IBasicRepository<TEntity, TKey> where TEntity : class
    {
        private readonly WheelDbContext _dbContext;

        private DbSet<TEntity> DbSet => _dbContext.Set<TEntity>();

        public EFBasicRepository(WheelDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
        {
            var savedEntity = (await _dbContext.Set<TEntity>().AddAsync(entity, cancellationToken)).Entity;
            if (autoSave)
            {
                await _dbContext.SaveChangesAsync(cancellationToken);
            }
            return savedEntity;
        }
        public async Task InsertManyAsync(List<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
        {
            await _dbContext.Set<TEntity>().AddRangeAsync(entities, cancellationToken);
            if (autoSave)
            {
                await _dbContext.SaveChangesAsync(cancellationToken);
            }
        }
        public async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
        {
            var savedEntity = _dbContext.Set<TEntity>().Update(entity).Entity;

            if (autoSave)
            {
                await _dbContext.SaveChangesAsync(cancellationToken);
            }
            return savedEntity;
        }
        public async Task UpdateAsync(Expression<Func<TEntity, bool>> predicate, Expression<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>> setPropertyCalls, bool autoSave = false, CancellationToken cancellationToken = default)
        {
            await _dbContext.Set<TEntity>().Where(predicate).ExecuteUpdateAsync(setPropertyCalls, cancellationToken);
            if (autoSave)
            {
                await _dbContext.SaveChangesAsync(cancellationToken);
            }
        }
        public async Task UpdateManyAsync(List<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
        {
            _dbContext.Set<TEntity>().UpdateRange(entities);
            if (autoSave)
            {
                await _dbContext.SaveChangesAsync(cancellationToken);
            }
        }
        public async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default)
        {
            var entity = await _dbContext.Set<TEntity>().FindAsync(id, cancellationToken);
            if(entity != null)
                _dbContext.Set<TEntity>().Remove(entity);
            if (autoSave)
            {
                await _dbContext.SaveChangesAsync(cancellationToken);
            }
        }
        public async Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
        {
            _dbContext.Set<TEntity>().Remove(entity);
            if (autoSave)
            {
                await _dbContext.SaveChangesAsync(cancellationToken);
            }
        }
        public async Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
        {
            await _dbContext.Set<TEntity>().Where(predicate).ExecuteDeleteAsync(cancellationToken);
            if (autoSave)
            {
                await _dbContext.SaveChangesAsync(cancellationToken);
            }
        }
        public async Task DeleteManyAsync(List<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
        {
            _dbContext.Set<TEntity>().RemoveRange(entities);
            if (autoSave)
            {
                await _dbContext.SaveChangesAsync(cancellationToken);
            }
        }
        public async Task<TEntity?> FindAsync(TKey id, CancellationToken cancellationToken = default)
        {
            return await _dbContext.Set<TEntity>().FindAsync(id, cancellationToken);
        }
        public async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
        {
            return await _dbContext.Set<TEntity>().AsNoTracking().FirstOrDefaultAsync(predicate, cancellationToken);
        }
        public async Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
        {
            return await _dbContext.Set<TEntity>().Where(predicate).ToListAsync(cancellationToken);
        }
        public async Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default, params Expression<Func<TEntity, object>>[] propertySelectors)
        {
            return await GetQueryableWithIncludes(propertySelectors).Where(predicate).ToListAsync(cancellationToken);
        }
        public async Task<List<TSelect>> SelectListAsync<TSelect>(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TSelect>> selectPredicate, CancellationToken cancellationToken = default, params Expression<Func<TEntity, object>>[] propertySelectors)
        {
            return await GetQueryableWithIncludes(propertySelectors).Where(predicate).Select(selectPredicate).ToListAsync(cancellationToken);
        }
        public async Task<List<TSelect>> SelectListAsync<TSelect>(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TSelect>> selectPredicate, CancellationToken cancellationToken = default)
        {
            return await GetQueryable().Where(predicate).Select(selectPredicate).ToListAsync(cancellationToken);
        }
        public async Task<(List<TSelect> items, long total)> SelectPageListAsync<TSelect>(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TSelect>> selectPredicate, int skip, int take, string orderby = "Id", CancellationToken cancellationToken = default)
        {
            var query = GetQueryable().Where(predicate).Select(selectPredicate);
            var total = await query.LongCountAsync(cancellationToken);
            var items = await query.OrderBy(orderby)
                .Skip(skip).Take(take)
                .ToListAsync(cancellationToken);
            return (items, total);
        }
        public async Task<(List<TSelect> items, long total)> SelectPageListAsync<TSelect>(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TSelect>> selectPredicate, int skip, int take, string orderby = "Id", CancellationToken cancellationToken = default, params Expression<Func<TEntity, object>>[] propertySelectors)
        {
            var query = GetQueryableWithIncludes(propertySelectors).Where(predicate).Select(selectPredicate);
            var total = await query.LongCountAsync(cancellationToken);
            var items = await query.OrderBy(orderby)
                .Skip(skip).Take(take)
                .ToListAsync(cancellationToken);
            return (items, total);
        }
        public async Task<(List<TEntity> items, long total)> GetPageListAsync(Expression<Func<TEntity, bool>> predicate, int skip, int take, string orderby = "Id", CancellationToken cancellationToken = default)
        {
            var query = GetQueryable().Where(predicate);
            var total = await query.LongCountAsync(cancellationToken);
            var items = await query.OrderBy(orderby)
                .Skip(skip).Take(take)
                .ToListAsync(cancellationToken);
            return (items, total);
        }
        public async Task<(List<TEntity> items, long total)> GetPageListAsync(Expression<Func<TEntity, bool>> predicate, 
            int skip, int take, string orderby = "Id", CancellationToken cancellationToken = default, params Expression<Func<TEntity, object>>[] propertySelectors)
        {
            var query = GetQueryableWithIncludes(propertySelectors).Where(predicate);
            var total = await query.LongCountAsync(cancellationToken);
            var items = await query.OrderBy(orderby)
                .Skip(skip).Take(take)
                .ToListAsync(cancellationToken);
            return (items, total);
        }

        public Task<bool> AnyAsync(CancellationToken cancellationToken = default)
        {
            return DbSet.AnyAsync(cancellationToken);
        }

        public Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
        {
            return DbSet.AnyAsync(predicate, cancellationToken);
        }
        public IQueryable<TEntity> GetQueryable(bool noTracking = true)
        {
            if (noTracking)
            {
                return _dbContext.Set<TEntity>().AsNoTracking();
            }
            return _dbContext.Set<TEntity>();
        }
        public IQueryable<TEntity> GetQueryableWithIncludes(params Expression<Func<TEntity, object>>[] propertySelectors)
        {
            return Includes(GetQueryable(), propertySelectors);
        }

        public Expression<Func<TEntity, bool>> BuildPredicate(params (bool condition, Expression<Func<TEntity, bool>> predicate)[] conditionPredicates)
        {
            if(conditionPredicates == null || conditionPredicates.Length == 0)
            {
                throw new ArgumentNullException("conditionPredicates can not be null.");
            }
            Expression<Func<TEntity, bool>>? buildPredicate = null;
            foreach (var (condition, predicate) in conditionPredicates)
            {
                if (condition)
                {
                    if (buildPredicate == null)
                        buildPredicate = predicate;
                    else if(predicate != null)
                        buildPredicate = buildPredicate.And(predicate);
                }
            }
            if(buildPredicate == null)
            {
                buildPredicate = (o) => true;
            }
            return buildPredicate;
        }

        private static IQueryable<TEntity> Includes(IQueryable<TEntity> query, Expression<Func<TEntity, object>>[] propertySelectors)
        {
            if (propertySelectors != null && propertySelectors.Length > 0)
            {
                foreach (var propertySelector in propertySelectors)
                {
                    query = query.Include(propertySelector);
                }
            }

            return query;
        }
        public async Task<int> SaveChangeAsync(CancellationToken cancellationToken = default)
        {
            return await _dbContext.SaveChangesAsync(cancellationToken);
        }

        protected DbSet<TEntity> GetDbSet()
        {
            return _dbContext.Set<TEntity>();
        }

        protected IDbConnection GetDbConnection()
        {
            return _dbContext.Database.GetDbConnection();
        }

        protected IDbTransaction? GetDbTransaction()
        {
            return _dbContext.Database.CurrentTransaction?.GetDbTransaction();
        }

    }


    public class EFBasicRepository<TEntity> : EFBasicRepository<TEntity, object>, IBasicRepository<TEntity> where TEntity : class
    {
        public EFBasicRepository(WheelDbContext dbContext) : base(dbContext)
        {
        }
    }

這樣我們CURD的操作的Repository就實現好了。
在列表查詢和分頁查詢中,特意實現了SelectList,避免在某些場景下每次查詢資料庫都查詢所有表欄位卻只使用了其中幾個欄位。也能有效提高查詢效能。
這裡分頁查詢特意使用了元組返回值,避免我們在分頁查詢時需要寫兩次操作,一次查總數,一次查真實資料。
還有實現了一個BuildPredicate來拼接我們的條件表示式,個人由於寫太多WhereIf有點膩了,所以弄了個方法來幹掉WhereIf,雖然這個方法可能不算完美。
實際操作如下圖:image.png
當然BuildPredicate這個方法也不只有在查詢方法中可以使用,在刪除和更新方法中,我們同樣可以根據條件這樣拼接條件表示式。

新增到依賴注入

由於Autofac的RegisterAssemblyTypes不支援泛型介面注入,所以我們這裡需要使用RegisterGeneric來註冊我們的泛型倉儲。
在WheelAutofacModule新增如下程式碼即可:

builder.RegisterGeneric(typeof(EFBasicRepository<,>)).As(typeof(IBasicRepository<,>)).InstancePerDependency();
builder.RegisterGeneric(typeof(EFBasicRepository<>)).As(typeof(IBasicRepository<>)).InstancePerDependency();

工作單元UOW

工作單元模式用於協調多個倉儲的操作並確保它們在一個事務中進行。
這裡我們來實現一個簡單的工作單元模式。
首先實現一個DbTransaction:

namespace Wheel.Uow
{
    public interface IDbTransaction : IDisposable, IAsyncDisposable
    {
        Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
        Task CommitAsync(CancellationToken cancellationToken = default);
        Task RollbackAsync(CancellationToken cancellationToken = default);
    }
    public class DbTransaction : IDbTransaction
    {
        private readonly DbContext _dbContext;

        IDbContextTransaction? CurrentDbContextTransaction;

        bool isCommit = false;
        bool isRollback = false;
        public DbTransaction(DbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)
        {
            CurrentDbContextTransaction = await _dbContext.Database.BeginTransactionAsync(cancellationToken);
            return CurrentDbContextTransaction;
        }

        public async Task CommitAsync(CancellationToken cancellationToken = default)
        {
            await _dbContext.SaveChangesAsync();
            await _dbContext.Database.CommitTransactionAsync();
            isCommit = true;
            CurrentDbContextTransaction = null;
        }
        public void Commit()
        {
            _dbContext.Database.CommitTransaction();
            isCommit = true;
            CurrentDbContextTransaction = null;
        }

        public async Task RollbackAsync(CancellationToken cancellationToken = default)
        {
            await _dbContext.Database.RollbackTransactionAsync(cancellationToken);
            isRollback = true;
            CurrentDbContextTransaction = null;
        }
        public void Dispose()
        {
            if(CurrentDbContextTransaction != null)
            {
                if(!isCommit && !isRollback)
                {
                    Commit();
                }
                CurrentDbContextTransaction.Dispose();
            }
        }

        public async ValueTask DisposeAsync()
        {
            if(CurrentDbContextTransaction != null)
            {
                if (!isCommit && !isRollback)
                {
                    await CommitAsync();
                }
                await CurrentDbContextTransaction.DisposeAsync();
            }
        }

    }
}

DbTransaction負責操作開啟事務,提交事務以及回滾事務。
實現UnitOfWork:

namespace Wheel.Uow
{
    public interface IUnitOfWork : IScopeDependency, IDisposable, IAsyncDisposable
    {
        Task<int> SaveChangesAsync(CancellationToken cancellationToken = default); 
        Task<IDbTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
        Task CommitAsync(CancellationToken cancellationToken = default);
        Task RollbackAsync(CancellationToken cancellationToken = default);
    }
    public class UnitOfWork : IUnitOfWork
    {
        private readonly WheelDbContext _dbContext;
        private IDbTransaction? Transaction = null;

        public UnitOfWork(WheelDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
        {
            return await _dbContext.SaveChangesAsync(cancellationToken);
        }
        public async Task<IDbTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)
        {
            Transaction = new DbTransaction(_dbContext);
            await Transaction.BeginTransactionAsync(cancellationToken);
            return Transaction;
        }
        public async Task CommitAsync(CancellationToken cancellationToken = default)
        {
            if(Transaction == null) 
            {
                throw new Exception("Transaction is null, Please BeginTransaction");
            }
            await Transaction.CommitAsync(cancellationToken);
        }

        public async Task RollbackAsync(CancellationToken cancellationToken = default)
        {
            if (Transaction == null)
            {
                throw new Exception("Transaction is null, Please BeginTransaction");
            }
            await Transaction.RollbackAsync(cancellationToken);
        }
        public void Dispose()
        {
            if(Transaction != null)
                Transaction.Dispose();
            _dbContext.Dispose();
        }

        public async ValueTask DisposeAsync()
        {
            if (Transaction != null)
                await Transaction.DisposeAsync();
            await _dbContext.DisposeAsync();
        }
    }
}

UnitOfWork負責控制DbTransaction的操作以及資料庫SaveChanges。

EF攔截器

在資料庫操作中,我們經常有一些資料是希望可以自動記錄的,如插入資料自動根據當前時間給建立時間欄位賦值,修改時自動根據當前時間修改最近更新時間欄位。亦或者當需要軟刪除操作時,我們正常呼叫Delete方法,實際是修改表資料,而不是從表中物理刪除資料。
新增軟刪除,建立時間以及更新時間介面:

public interface ISoftDelete
{
    /// <summary>
    /// 是否刪除
    /// </summary>
    public bool IsDeleted { get; set; }
}
public interface IHasUpdateTime
{
    /// <summary>
    /// 最近修改時間
    /// </summary>
    DateTimeOffset UpdateTime { get; set; }
}
public interface IHasCreationTime
{
    /// <summary>
    /// 建立時間
    /// </summary>
    DateTimeOffset CreationTime { get; set; }
}

實現WheelEFCoreInterceptor,繼承SaveChangesInterceptor,當呼叫SaveChanges方法是就會執行攔截器的邏輯操作。

namespace Wheel.EntityFrameworkCore
{
    /// <summary>
    /// EF攔截器
    /// </summary>
    public sealed class WheelEFCoreInterceptor : SaveChangesInterceptor
    {
        public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
        {
            OnSavingChanges(eventData);
            return base.SavingChanges(eventData, result);
        }

        public static void OnSavingChanges(DbContextEventData eventData)
        {
            ArgumentNullException.ThrowIfNull(eventData.Context);
            eventData.Context.ChangeTracker.DetectChanges();
            foreach (var entityEntry in eventData.Context.ChangeTracker.Entries())
            {
                if (entityEntry is { State: EntityState.Deleted, Entity: ISoftDelete softDeleteEntity })
                {
                    softDeleteEntity.IsDeleted = true;
                    entityEntry.State = EntityState.Modified;
                }
                if (entityEntry is { State: EntityState.Modified, Entity: IHasUpdateTime hasUpdateTimeEntity })
                {
                    hasUpdateTimeEntity.UpdateTime = DateTimeOffset.Now;
                }
                if (entityEntry is { State: EntityState.Added, Entity: IHasCreationTime hasCreationTimeEntity })
                {
                    hasCreationTimeEntity.CreationTime = DateTimeOffset.Now;
                }
            }
        }
    }
}

在AddDbContext新增我們的攔截器:

builder.Services.AddDbContext<WheelDbContext>(options =>
    options.UseSqlite(connectionString)
        .AddInterceptors(new WheelEFCoreInterceptor())
        .UseLazyLoadingProxies()
);

這樣就完成了我們ORM的整合了。

輪子倉庫地址https://github.com/Wheel-Framework/Wheel
歡迎進群催更。

image.png

相關文章