29 | 定義倉儲:使用EF Core實現倉儲層
首先定義倉儲層的接口,以及倉儲層實現的基類,抽象類
倉儲層的接口
namespace GeekTime.Infrastructure.Core
{
/// <summary>
/// 包含普通實體的倉儲
/// 約束 TEntity 必須是繼承 Entity 的基類,必須實現聚合根 IAggregateRoot
/// 也就是說倉儲里面存儲的對象必須是一個聚合根對象
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public interface IRepository<TEntity> where TEntity : Entity, IAggregateRoot
{
IUnitOfWork UnitOfWork { get; }
TEntity Add(TEntity entity);
Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default);
TEntity Update(TEntity entity);
Task<TEntity> UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);
bool Remove(Entity entity);// 由於沒有指定主鍵,只能根據當前實體進行刪除操作
Task<bool> RemoveAsync(Entity entity);
}
/// <summary>
/// 包含指定主鍵的類型的實體的倉儲
/// 繼承了上面的接口 IRepository<TEntity>,也就是說擁有了上面定義的所有方法
/// 另外一個,它實現了幾個跟 Id 相關的操作的方法
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TKey"></typeparam>
public interface IRepository<TEntity, TKey> : IRepository<TEntity> where TEntity : Entity<TKey>, IAggregateRoot
{
bool Delete(TKey id);
Task<bool> DeleteAsync(TKey id, CancellationToken cancellationToken = default);
TEntity Get(TKey id);
Task<TEntity> GetAsync(TKey id, CancellationToken cancellationToken = default);
}
}
具體抽象類的實現
namespace GeekTime.Infrastructure.Core
{
/// <summary>
/// 定義普通實體的倉儲
/// 定義約束 TDbContext 必須是 EFContext,也就是倉儲必須依賴於 EFContext 及其子類
/// 將來就可以把自己定義的比如 DomainContext 作為泛型參數傳入 Repository,就可以很快捷地定義出來自己的倉儲
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TDbContext"></typeparam>
public abstract class Repository<TEntity, TDbContext> : IRepository<TEntity> where TEntity : Entity, IAggregateRoot where TDbContext : EFContext
{
// 具體實現需要依賴 DbContext
protected virtual TDbContext DbContext { get; set; }
public Repository(TDbContext context)
{
this.DbContext = context;
}
public virtual IUnitOfWork UnitOfWork => DbContext;// 因為 DbContext, EFContext 實際上實現了 IUnitOfWork,所以直接返回
// 下面這些方法都是 EntityFramework 提供的能力,所以就能通過簡單的幾行代碼來實現基本的倉儲操作
public virtual TEntity Add(TEntity entity)
{
return DbContext.Add(entity).Entity;
}
public virtual Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default)
{
return Task.FromResult(Add(entity));
}
public virtual TEntity Update(TEntity entity)
{
return DbContext.Update(entity).Entity;
}
public virtual Task<TEntity> UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
{
return Task.FromResult(Update(entity));
}
public virtual bool Remove(Entity entity)
{
DbContext.Remove(entity);
return true;
}
public virtual Task<bool> RemoveAsync(Entity entity)
{
return Task.FromResult(Remove(entity));
}
}
/// <summary>
/// 定義主鍵的實體的倉儲
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TDbContext"></typeparam>
public abstract class Repository<TEntity, TKey, TDbContext> : Repository<TEntity, TDbContext>, IRepository<TEntity, TKey> where TEntity : Entity<TKey>, IAggregateRoot where TDbContext : EFContext
{
public Repository(TDbContext context) : base(context)
{
}
/// <summary>
/// 根據 Id 從 DbContext 獲取 Entity,然后再 Remove
/// 這樣的好處是可以跟蹤對象的狀態
/// 壞處是任意的刪除都需要先去數據庫里面做查詢
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public virtual bool Delete(TKey id)
{
var entity = DbContext.Find<TEntity>(id);
if (entity == null)
{
return false;
}
DbContext.Remove(entity);
return true;
}
public virtual async Task<bool> DeleteAsync(TKey id, CancellationToken cancellationToken = default)
{
var entity = await DbContext.FindAsync<TEntity>(id, cancellationToken);
if (entity == null)
{
return false;
}
DbContext.Remove(entity);
return true;
}
public virtual TEntity Get(TKey id)
{
return DbContext.Find<TEntity>(id);
}
public virtual async Task<TEntity> GetAsync(TKey id, CancellationToken cancellationToken = default)
{
return await DbContext.FindAsync<TEntity>(id, cancellationToken);
}
}
}
實現自己的 DbContext
DomainContext
namespace GeekTime.Infrastructure
{
public class DomainContext : EFContext
{
public DomainContext(DbContextOptions options, IMediator mediator, ICapPublisher capBus) : base(options, mediator, capBus)
{
}
public DbSet<Order> Orders { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region 注冊領域模型與數據庫的映射關系
modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new UserEntityTypeConfiguration());
#endregion
base.OnModelCreating(modelBuilder);
}
}
}
映射關系,針對每一個領域模型創建一個 EntityTypeConfiguration
OrderEntityTypeConfiguration
namespace GeekTime.Infrastructure.EntityConfigurations
{
class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
// 定義主鍵
builder.HasKey(p => p.Id);
//builder.ToTable("order");
//builder.Property(p => p.UserId).HasMaxLength(20);
//builder.Property(p => p.UserName).HasMaxLength(30);
// 定義導航屬性
builder.OwnsOne(o => o.Address, a =>
{
a.WithOwner();
//a.Property(p => p.City).HasMaxLength(20);
//a.Property(p => p.Street).HasMaxLength(50);
//a.Property(p => p.ZipCode).HasMaxLength(10);
});
}
}
}
UserEntityTypeConfiguration
namespace GeekTime.Infrastructure.EntityConfigurations
{
class UserEntityTypeConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasKey(p => p.Id);
}
}
}
事務處理
要實現對 DomainContext 的事務處理的話,僅僅需要創建一個類 DomainContextTransactionBehavior
namespace GeekTime.Infrastructure
{
public class DomainContextTransactionBehavior<TRequest, TResponse> : TransactionBehavior<DomainContext, TRequest, TResponse>
{
public DomainContextTransactionBehavior(DomainContext dbContext, ICapPublisher capBus, ILogger<DomainContextTransactionBehavior<TRequest, TResponse>> logger) : base(dbContext, capBus, logger)
{
}
}
}
為了演示效果,在應用程序啟動時,添加一行代碼
Startup
// 這一行代碼的作用是創建一個 Scope,在這個范圍內創建 DomainContext
using (var scope = app.ApplicationServices.CreateScope())
{
var dc = scope.ServiceProvider.GetService<DomainContext>();
// 確定數據庫已經創建,如果數據庫沒有創建,這個時候會執行數據庫的自動創建過程,根據模型創建數據庫
dc.Database.EnsureCreated();
}
數據庫的注冊部分
ServiceCollectionExtensions
/// <summary>
/// 這個定義就是將連接字符串配置到 dDomainContext
/// </summary>
/// <param name="services"></param>
/// <param name="connectionString"></param>
/// <returns></returns>
public static IServiceCollection AddMySqlDomainContext(this IServiceCollection services, string connectionString)
{
return services.AddDomainContext(builder =>
{
builder.UseMySql(connectionString);
});
}
這一行代碼的調用位置是在 ConfigureServices 里面
// 從配置中獲取字符串
services.AddMySqlDomainContext(Configuration.GetValue<string>("Mysql"));
啟動程序,運行過程中 EF 框架會根據定義的實體映射關系生成數據庫,可在 Mysql 數據庫中查看生成結果
接着豐富一下 Order 的映射關系
namespace GeekTime.Infrastructure.EntityConfigurations
{
class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
// 定義主鍵
builder.HasKey(p => p.Id);
builder.ToTable("order");// 修改表名為 order,不帶 s
builder.Property(p => p.UserId).HasMaxLength(20);// 修改字段長度
builder.Property(p => p.UserName).HasMaxLength(30);
// 定義導航屬性
// OwnsOne 的方式可以將 Address 這個值類型作為同一個表的字段來設置
builder.OwnsOne(o => o.Address, a =>
{
a.WithOwner();
a.Property(p => p.City).HasMaxLength(20);
a.Property(p => p.Street).HasMaxLength(50);
a.Property(p => p.ZipCode).HasMaxLength(10);
});
}
}
}
啟動程序,可以看到數據庫修改結果
這說明可以在倉儲層定義領域模型與數據庫的映射關系,這個映射關系可以組織為一個目錄,為每一個領域模型設置一個類型來定義,並且這個過程是強類型的,這樣的結構,便於后期維護
另外倉儲層的話,定義了一個 IOrderRepository,僅僅實現了 IRepository 泛型接口,引進 Order,由於 Order 實際上有一個主鍵是 long,所以這里把主鍵類型也傳給 IRepository
namespace GeekTime.Infrastructure.Repositories
{
public interface IOrderRepository : IRepository<Order, long>
{
}
}
Order
public class Order : Entity<long>, IAggregateRoot
這樣子,Order 的倉儲就定義完畢
那么 Order 倉儲的實現也非常簡單,僅僅需要繼承 Repository,把 Order,long,DomainContext 傳入泛型 Repository 即可,這里還實現了 IOrderRepository
namespace GeekTime.Infrastructure.Repositories
{
public class OrderRepository : Repository<Order, long, DomainContext>, IOrderRepository
{
public OrderRepository(DomainContext context) : base(context)
{
}
}
}
通過這樣簡單的繼承,可以復用之前定義的代碼,快速實現倉儲層的定義
可以通過代碼提升看到倉儲層是有 Add,Update,Remove,Delete 方法,還有 UnitOfWork 的屬性
這樣一來就完成了倉儲層的定義,可以看到倉儲層的代碼非常的薄,僅僅包含了一些接口的定義和類的繼承,需要自定義一些方法的時候,可以在倉儲層定義一些特殊方法,比如 AddABC 等特殊的邏輯都可以在這里去實現
namespace GeekTime.Infrastructure.Repositories
{
public class OrderRepository : Repository<Order, long, DomainContext>, IOrderRepository
{
public OrderRepository(DomainContext context) : base(context)
{
}
}
public void AddABC()
{
}
}
另外一個在組織領域模型和數據庫的關系的時候,可以很清晰的看到,是在 EntityConfiguration 這個目錄下面,為每一個模型定義一個映射類,當領域模型越來越復雜,數據庫的結構越來越復雜的時候,這樣的組織結構會非常的清晰
GitHub源碼鏈接:
https://github.com/witskeeper/geektime
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。
如有任何疑問,請與我聯系 (MingsonZheng@outlook.com) 。