IT教程 ·

你肯定看得懂的 DDD+CQRS+EDA+ES 中心头脑与极简可运转代码示例

进阶之路 | 奇妙的IPC之旅

媒介

跟着分布式架构微效劳的鼓起,DDD(范畴驱动设想)、CQRS(敕令查询职责星散)、EDA(事宜驱动架构)、ES(事宜溯源)等观点也一并成为时下的炽热观点,我也在早些时刻浏览了一些大佬的剖析文,进修相干观点,不过一向有种若明若暗、似懂非懂的以为。经由一段时间的进修和研讨大佬的代码后,自身设想完成了一套我消化明白后的代码。为了突出重点,防止遭到大批完成细节的滋扰,固然也是懒(这才是主要原因),个中的一切基础设施都运用了现成的库。所完成的研讨成果也做成了傻瓜式一键体验(我对对着黑框框敲敕令没什么兴致,能点两下鼠标搞定的事我绝不在键盘上敲又臭又长的敕令,敲敕令能敲出优越感的人我以为应当是抖M)。

正文

DDD(范畴驱动设想)

这一定是最群丑跳梁的一个观点,每一个大佬都能讲出一大篇演讲稿,但都或多或少存在差别或不合,在我初看 DDD 时,我就被整懵了,这究竟是咋回事?

如今回过头来看,DDD 现实上是一个高阶头脑观点,并不能指点开发者怎样敲键盘,是指点人怎样思索范畴问题,而不是指点人思索出详细的范畴的。恰是因为中心隔了一层虚幻漂渺的观点,致使差别的人得出了差别的结论。还好 DDD 存在一些比较详细轻易落实的观点,如今就来说下我对这些罕见基础观点的明白和我编码时的基础原则,愿望大家能在看大佬的文章时不必一脸懵逼,也举行下心得交换。

Entity(实体)

实体是一个存储数据的类,假如类中包括自身的合法性考证划定规矩之类的要领,平常称之为充血模子,相对的纯真保存数据的则称为血虚模子(偶然也叫做 POCO 类)。实体有一个主要性子,相称性是由标识属性决议的,这个标识可所以一个简朴的 int 型的 Id,也可所以多个内部数据的某种组合(类似数据库表的复合字段主键)。除标识外的其他东西均不对两个实体对象的相称性发作影响。而且实体的数据属性是可更改的。

有许多大佬以为实体应当是充血的,但在我看来,血虚的好像更好,因为需求的不稳固性也许致使这些划定规矩并不稳固,或划定规矩自身并不唯一,在差别场所也许须要差别划定规矩。这时刻候充血模子不管怎么办都很别扭,假如把划定规矩定义和校验交给外部组件,这些需求就很轻易满足,比方运用 FluentValidate 为一种实体定义多套划定规矩或对内部的划定规矩条目按状况重新组合。

ValueObject(值对象)

值对象也是用来存储数据的类。与实体相对,值对象没有标识属性,其相称性由一切内部属性决议,当且仅当两个值对象实例的一切属性逐一相称时,这两个值对象相称。而且值对象的一切属性为只读,仅能在组织函数中举行唯逐一次设置,假如愿望修正某个值对象的某一属性,唯一的方法是运用新的值对象替换旧的值对象。而且值对象常常作为实体的属性存在。

这个观点看起来和实体迥殊类似,都是用来存储数据的,但也有些性子上的基础差别。网上的大佬一般会为值对象编写基类,但我以为,值对象和实体在代码完成上并没有这么大的区分。能够看做整数和小数在计算机中表现为差别的数据范例,但在数学观点上他们没有区分,仅仅只是因为离散的计算机体系没法圆满示意一连的数学数字而发作的缝合怪。我倾向于依据类的代码定义所表现出来的性子与谁相符就将其视为谁,而不是看完成的接口或继承的基类。因为需求的不确定性会致使他们也许会发作转换,依据代码举行自我形貌来推断能够防止许多潜伏的贫苦。

Aggregate,Aggregate Root(聚合及聚合根)

聚合根示意一个范畴所操纵的顶级实体范例,其他隶属数据都是聚合根的内部属性,聚合根和其所属的其他实体的组合称为聚合。这是一个纯观点性的东西。对范畴实体的操纵必需从聚合根入手下手,也就是说确保数据完全性的基础单位是聚合。大佬的代码中常常会用一个空接口来示意聚合根,假如某个实体完成了这个接口,就示意这个实体可所以一个聚合根。请注意,聚合根不一定必需是顶级范例,也可所以其他实体的一个属性。这示意一个实体在,某些状况下是聚合根,而其他状况下是另一个聚合根的内部属性。也就是说实体之间并不是严厉的树状关联,而是平常有向图状关联。

我以为定义如许的空接口现实意不大,反而也许形成一些误解。假如某个实体因为需求更改致使不再见成为聚合根,那这个实表现实大将不再是聚合根,但人是会出错的,极也许遗忘去掉聚合根接口,这时刻代码与现实将发作矛盾。所以我以为聚合根应当基于现实而不是代码。当一个实体不再见作为聚合根运用时,将相干代码删除,就同时示意它不再是聚合根,浏览代码的人也因为看不到相干代码而自动以为它不是聚合根。在代码中的表现体式格局与下一个的观点有关。

Repository(仓储)

仓储示意对聚合根的耐久化的笼统,在代码上可表现为声清楚明了增删查改的相干要领的接口,而仓储的完成类担任详细处置惩罚怎样对聚合根实体举行增删查改。比方在仓储内部运用数据库完成详细工作。

假如一个仓储担任治理一个聚合根实体的耐久化或者说存取,那这个实体就是一个现实上的聚合根。那末在这里,就能够在代码操纵大将看到某个实体被仓储治理等价为这个实体是聚合根,反之就不是。也就是说,假如将某个实体的仓储的末了一个现实运用代码删除,这个实体就在现实上不再是聚合根,此时代码表现与现实将圆满同步,不再见发作矛盾。至于因为没看到某个实体的仓储而将实体误以为不是聚合根,这实在并没有任何问题。这申明在你所关注的范畴中这个实体确切不是聚合根,而这个实体也许作为聚合根运用的范畴你基础不体贴,所以看不到,那这个实体是不是在其他范畴作为聚合根运用对你而言现实上是无所谓的。

Domain Service(范畴效劳)

这就涉及到营业代码的编写了。假如一个营业须要由多个聚合根合营完成,也就是须要多个仓储,那末就应当将这些对仓储的挪用封装进一个效劳,一致对外暴露供应效劳。

假如这些仓储操纵须要具有事件性,也能够在这里举行谐和治理。假如某个营业只须要一个仓储介入,要不要特地封装一个效劳就看你愉快了。

CQRS(敕令查询职责星散)

CQRS 本质上是一种指点头脑,指点开发者怎样设想一个低耦合高可扩大架构的头脑。传统的 CURD 将对数据的操纵分为 读、写、改、删,将他们封装在一起致使他们将严密耦合在雷同的数据源中,不利于扩大。CQRS 则将对数据的操纵分为会转变数据源的和不会转变数据源的,前者称为敕令,后者称为查询。将他们离别封装能让他们各自运用差别的数据源,进步可扩大性。

个中敕令是一个会转变数据源,但不返回任何值的要领;查询是会返回值,但绝不会转变数据源的要领。但是在我的编码中,敕令是能够返回值的,至于要返回什么,依据现实状况调解。比方最简朴的返回一个 bool 示意操纵是不是胜利以决议接下来的营业流程该走向何方,这是很罕见的状况。所以在我的观点里,一个要领是敕令照样查询现实上只看这个要领是不是会转变数据源,要封装在一起照样离别封装都无所谓。提议离开封装到差别的仓储中,经由历程仓储关联到详细的数据源,敕令和查询的仓储关联到差别的数据源的时刻,天然就完成了读写星散。经由历程起名来昭示要领的目标应当能够轻松区分一个要领属于敕令照样查询。只需脑子里有这个观点,要完成扩大方法多的是。

事宜驱动架构(EDA)

能够说一切图形界面(Gui)编程都是清一色的事宜驱动架构,这东西一点也不希奇。说白了,EDA 就是一种被动架构,经由历程某些事变的发作来触发某些操纵的实行,不然体系就随时待命,按兵不动。

EDA 的完成须要一个中介才完成,在 Windows 中,这个东西叫做 Windows 音讯行列(音讯轮回)和事宜处置惩罚器。一样的,在非 Gui 编程中也须要这俩东西,但一般被称为音讯总线和音讯花费者。在分布式体系中,这个中介将不与体系在统一历程以至不在统一装备中,称为分布式音讯总线。如许在开发时能够分红两拨,一拨担任写生产并发送事宜的代码,一拨担任写吸收事宜信息并举行处置惩罚的代码。他们之间的沟通仅限于交换体贴的事宜叫什么以及事宜携带了什么信息。至于发作的音讯是怎样送到准确的花费端并触发花费处置惩罚器的,那是音讯总线的事。假如一个音讯总线须要这两拨人相识中心的历程以至须要自身去完成,那这个音讯总线是个成品,也起不到什么解耦的效果,以至是个拖后腿的东西。

EDA + CQRS

当他们连系在一起,就发作了敕令或查询的提议和现实处置惩罚完成能够星散的效果。敕令的提议方向敕令总线发送一条敕令音讯并带上必要参数,花费方收到音讯后猎取参数完成任务并返回效果。敕令能够看做一种特别的事宜,敕令只由一个敕令处置惩罚器处置惩罚,并可向发送方返回一个处置惩罚效果;事宜由一切对同种事宜感兴致的事宜处置惩罚器处置惩罚,不向事宜发送方返回任何效果。

事宜处置惩罚器的实行次序是不确定的,所以任何事宜处置惩罚器都必需自力完成事宜处置惩罚。假如两个事宜处置惩罚之间存在因果依靠,应当在前置事宜处置惩罚后由事宜处置惩罚器宣布新事宜,并由后置事宜处置惩罚器去处置惩罚前置事宜发作的新事宜,而不是让它们处置惩罚统一事宜。

ES(事宜溯源)

事宜溯源示意能清查一个事宜的泉源,以至与之相干的其他事宜的观点,说句大白话就是刨祖坟。ES 对汗青状况回溯的需求有着天然的支撑,最罕见的如打消重做。而 ES 平常会合营 EDA 运用,ES 保存 EDA 发作的事宜信息,而且这些信息有只读性和因果连贯性。这趁便能让我们对体系中的实体究竟是怎样一步一步变成如今这个模样有一个清楚的相识。毕竟实体具有可变性,实体信息一旦转变,旧的信息就会丧失,ES 恰好弥补了这个缺点。

代码展示申明

此处的事宜音讯中介运用 MediatR 完成。

接口

DDD 相干

实体

定义一个实体的基础要素,完成接口的类就是实体,值对象没有接口或基类,只看代码所展示的性子是不是相符值对象的定义,聚合根没有接口或基类,只看实体是不是被仓储运用,范畴效劳说白了就是个打包封装,依据状况来决议,比方重构时提取要领即可视为封装效劳。在此处可简朴以为没有完成实体接口的数据类是值对象:

 1 /// <summary>
 2 /// 实体接口
 3 /// </summary>
 4 public interface IEntity {}
 5 
 6 /// <summary>
 7 /// 泛型实体接口,束缚Id属性
 8 /// </summary>
 9 public interface IEntity<TKey> : IEntity
10     where TKey : IEquatable<TKey>
11 {
12     TKey Id { get; set; }
13 }

仓储接口

仓储接口细分为可读仓储和可写仓储,可写仓储有一个分支为可批量提交仓储,示意修正操纵会在挪用提交保存要领后批量保存,也就是事件(就是用来替换操纵单位的,这东西就有一个提交操纵,名字也稀里糊涂,我曾一向没法明白这东西是干吗的),接口声明参考 EF Core,示例完成也基于 EF Core。因为已公开了查询接口范例的 Set 属性,运用者能够恣意自定义查询。

 1     public interface IBulkOperableVariableRepository<TResult, TVariableRepository, TEntity>
 2         where TEntity : IEntity
 3         where TVariableRepository : IVariableRepository<TEntity>
 4     {
 5         TResult SaveChanges();
 6         Task<TResult> SaveChangesAsync(CancellationToken cancellationToken);
 7     }
 8 
 9     public interface IBulkOperableVariableRepository<TVariableRepository, TEntity>
10         where TEntity : IEntity
11         where TVariableRepository : IVariableRepository<TEntity>
12     {
13         void SaveChanges();
14         Task SaveChangesAsync(CancellationToken cancellationToken);
15     }
16 
17     public interface IReadOnlyRepository<TEntity>
18         where TEntity : IEntity
19     {
20         IQueryable<TEntity> Set { get; }
21         TEntity Find(TEntity entity, bool ignoreNullValue);
22         Task<TEntity> FindAsync(TEntity entity, bool ignoreNullValue);
23 
24     }
25     public interface IReadOnlyRepository<TEntity, TKey> : IReadOnlyRepository<TEntity>
26         where TEntity : IEntity<TKey>
27         where TKey : IEquatable<TKey>
28     {
29         TEntity Find(TKey key);
30         Task<TEntity> FindAsync(TKey key);
31         IQueryable<TEntity> Find(IEnumerable<TKey> keys);
32     }
33 
34     public interface IVariableRepository<TEntity>
35         where TEntity : IEntity
36     {
37         void Add(TEntity entity);
38         Task AddAsync(TEntity entity, CancellationToken cancellationToken);
39         void Update(TEntity entity);
40         Task UpdateAsync(TEntity entity, CancellationToken cancellationToken);
41         void Delete(TEntity entity, bool isSoftDelete);
42         Task DeleteAsync(TEntity entity, bool isSoftDelete, CancellationToken cancellationToken);
43         void AddRange(IEnumerable<TEntity> entities);
44         Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken);
45         void UpdateRange(IEnumerable<TEntity> entities);
46         Task UpdateRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken);
47         void DeleteRange(IEnumerable<TEntity> entities, bool isSoftDelete);
48         Task DeleteRangeAsync(IEnumerable<TEntity> entities, bool isSoftDelete, CancellationToken cancellationToken);
49     }
50     public interface IVariableRepository<TEntity, TKey> : IVariableRepository<TEntity>
51         where TEntity : IEntity<TKey>
52         where TKey : IEquatable<TKey>
53     {
54         void Delete(TKey key, bool isSoftDelete);
55         Task DeleteAsync(TKey key, bool isSoftDelete, CancellationToken cancellationToken);
56         void DeleteRange(IEnumerable<TKey> keys, bool isSoftDelete);
57         Task DeleteRangeAsync(IEnumerable<TKey> keys, bool isSoftDelete, CancellationToken cancellationToken);
58     }
59 
60     public interface IRepository<TEntity> : IVariableRepository<TEntity>, IReadOnlyRepository<TEntity>
61         where TEntity : IEntity
62     {
63     }
64 
65     public interface IRepository<TEntity, TKey> : IRepository<TEntity>, IVariableRepository<TEntity, TKey>, IReadOnlyRepository<TEntity, TKey>
66         where TEntity : IEntity<TKey>
67         where TKey : IEquatable<TKey>
68     {
69     }

EF Core 专用特化版仓储接口

 1     public interface IEFCoreRepository<TEntity, TDbContext> : IReadOnlyRepository<TEntity>, IVariableRepository<TEntity>, IBulkOperableVariableRepository<int, IEFCoreRepository<TEntity, TDbContext>, TEntity>
 2         where TEntity : class, IEntity
 3         where TDbContext : DbContext
 4     { }
 5 
 6     public interface IEFCoreRepository<TEntity, TKey, TDbContext> : IEFCoreRepository<TEntity, TDbContext>, IReadOnlyRepository<TEntity, TKey>, IVariableRepository<TEntity, TKey>
 7         where TEntity : class, IEntity<TKey>
 8         where TKey : IEquatable<TKey>
 9         where TDbContext : DbContext
10     { }

 

CQRS+EDA 相干:

敕令接口

分为带返回值敕令和无返回值敕令

1 public interface ICommand<out TResult> : ICommand
2 {
3 }
4 
5 public interface ICommand : IMessage
6 {
7 }

敕令总线接口

一样分为带返回值和无返回值

 1 public interface ICommandBus<in TCommand>
 2     where TCommand : ICommand
 3 {
 4     Task SendCommandAsync(TCommand command, CancellationToken cancellationToken);
 5 }
 6 
 7 public interface ICommandBus<in TCommand, TResult> : ICommandBus<TCommand>
 8     where TCommand : ICommand<TResult>
 9 {
10     new Task<TResult> SendCommandAsync(TCommand command, CancellationToken cancellationToken);
11 }

敕令处置惩罚器接口

同上

 1 public interface ICommandHandler<in TCommand>
 2     where TCommand : ICommand
 3 {
 4     Task Handle(TCommand command, CancellationToken cancellationToken);
 5 }
 6 
 7 public interface ICommandHandler<in TCommand, TResult> : ICommandHandler<TCommand>
 8     where TCommand : ICommand<TResult>
 9 {
10     new Task<TResult> Handle(TCommand command, CancellationToken cancellationToken);
11 }

敕令存储接口

可用于汗青敕令追溯,返回值可用于返回存储是不是胜利或其他必要信息

 1 public interface ICommandStore
 2 {
 3     void Save(ICommand command);
 4 
 5     Task SaveAsync(ICommand command, CancellationToken cancellationToken);
 6 }
 7 
 8 public interface ICommandStore<TResult> : ICommandStore
 9 {
10     new TResult Save(ICommand command);
11 
12     new Task<TResult> SaveAsync(ICommand command, CancellationToken cancellationToken);
13 }

事宜接口

没有返回值

1 public interface IEvent : IMessage
2 {
3 }

事宜总线接口

同上

 1 public interface IEventBus
 2 {
 3     void PublishEvent(IEvent @event);
 4 
 5     Task PublishEventAsync(IEvent @event, CancellationToken cancellationToken);
 6 }
 7 
 8 public interface IEventBus<TResult> : IEventBus
 9 {
10     new TResult PublishEvent(IEvent @event);
11 
12     new Task<TResult> PublishEventAsync(IEvent @event, CancellationToken cancellationToken);
13 }

事宜处置惩罚器接口

同上

1 public interface IEventHandler<in TEvent>
2     where TEvent : IEvent
3 {
4     Task Handle(TEvent @event, CancellationToken cancellationToken);
5 }

事宜存储接口

同敕令存储接口

 1 public interface IEventStore
 2 {
 3     void Save(IEvent @event);
 4 
 5     Task SaveAsync(IEvent @event, CancellationToken cancellationToken = default);
 6 }
 7 
 8 public interface IEventStore<TResult> : IEventStore
 9 {
10     new TResult Save(IEvent @event);
11 
12     new Task<TResult> SaveAsync(IEvent @event, CancellationToken cancellationToken = default);
13 }

(敕令、事宜)音讯基础接口

1 public interface IMessage
2 {
3     Guid Id { get; }
4 
5     DateTimeOffset Timestamp { get; }
6 }

相干接口定义终了。

完成

EF Core 泛型仓储

未知主键的实体运用实体对象为前提查找时,运用动态生成表达式的要领

  1     public class EFCoreRepository<TEntity, TKey, TDbContext> : EFCoreRepository<TEntity, TDbContext>, IEFCoreRepository<TEntity, TKey, TDbContext>
  2         where TEntity : class, IEntity<TKey>
  3         where TKey : IEquatable<TKey>
  4         where TDbContext : DbContext
  5     {
  6         public EFCoreRepository(TDbContext dbContext) : base(dbContext)
  7         {
  8         }
  9 
 10         public virtual void Delete(TKey key, bool isSoftDelete)
 11         {
 12             var entity = Find(key);
 13             Delete(entity, isSoftDelete);
 14         }
 15 
 16         public virtual Task DeleteAsync(TKey key, bool isSoftDelete, CancellationToken cancellationToken = default)
 17         {
 18             Delete(key, isSoftDelete);
 19             return Task.CompletedTask;
 20         }
 21 
 22         public virtual void DeleteRange(IEnumerable<TKey> keys, bool isSoftDelete)
 23         {
 24             var entities = Find(keys).ToArray();
 25             dbSet.AttachRange(entities);
 26             DeleteRange(entities, isSoftDelete);
 27         }
 28 
 29         public virtual Task DeleteRangeAsync(IEnumerable<TKey> keys, bool isSoftDelete, CancellationToken cancellationToken = default)
 30         {
 31             DeleteRange(keys, isSoftDelete);
 32             return Task.CompletedTask;
 33         }
 34 
 35         public virtual TEntity Find(TKey key)
 36         {
 37             return Set.SingleOrDefault(x => x.Id.Equals(key));
 38         }
 39 
 40         public virtual IQueryable<TEntity> Find(IEnumerable<TKey> keys)
 41         {
 42             return Set.Where(x => keys.Contains(x.Id));
 43         }
 44 
 45         public override TEntity Find(TEntity entity, bool ignoreNullValue)
 46         {
 47             return base.Find(entity, ignoreNullValue);
 48         }
 49 
 50         public virtual Task<TEntity> FindAsync(TKey key)
 51         {
 52             return Set.SingleOrDefaultAsync(x => x.Id.Equals(key));
 53         }
 54 
 55         public override Task<TEntity> FindAsync(TEntity entity, bool ignoreNullValue)
 56         {
 57             return base.FindAsync(entity, ignoreNullValue);
 58         }
 59     }
 60 
 61     public class EFCoreRepository<TEntity, TDbContext> : IEFCoreRepository<TEntity, TDbContext>
 62         where TEntity : class, IEntity
 63         where TDbContext : DbContext
 64     {
 65         protected readonly TDbContext dbContext;
 66         protected readonly DbSet<TEntity> dbSet;
 67 
 68         protected virtual void ProcessChangedEntity()
 69         {
 70             var changedEntities = dbContext.ChangeTracker.Entries()
 71                 .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);
 72             foreach (var entity in changedEntities)
 73             {
 74                 (entity as IOptimisticConcurrencySupported)?.GenerateNewConcurrencyStamp();
 75             }
 76 
 77             var changedEntitiesGroups = changedEntities.GroupBy(x => x.State);
 78             foreach (var group in changedEntitiesGroups)
 79             {
 80                 switch (group)
 81                 {
 82                     case var entities when entities.Key == EntityState.Added:
 83                         foreach (var entity in entities)
 84                         {
 85                             if (entity is IActiveControllable)
 86                             {
 87                                 (entity as IActiveControllable).Active ??= true;
 88                             }
 89                         }
 90                         break;
 91                     case var entities when entities.Key == EntityState.Modified:
 92                         foreach (var entity in entities)
 93                         {
 94                             (entity as IEntity)?.ProcessCreationInfoWhenModified(dbContext);
 95 
 96                             if (entity is IActiveControllable && (entity as IActiveControllable).Active == null)
 97                             {
 98                                 entity.Property(nameof(IActiveControllable.Active)).IsModified = false;
 99                             }
100                         }
101                         break;
102                     default:
103                         break;
104                 }
105             }
106         }
107 
108         protected virtual void ResetDeletedMark(params TEntity[] entities)
109         {
110             foreach (var entity in entities)
111             {
112                 if (entity is ILogicallyDeletable)
113                 {
114                     (entity as ILogicallyDeletable).IsDeleted = false;
115                 }
116             }
117         }
118 
119         public EFCoreRepository(TDbContext dbContext)
120         {
121             this.dbContext = dbContext;
122             dbSet = this.dbContext.Set<TEntity>();
123         }
124 
125         public virtual void Add(TEntity entity)
126         {
127             dbSet.Add(entity);
128         }
129 
130         public virtual Task AddAsync(TEntity entity, CancellationToken cancellationToken = default)
131         {
132             return dbSet.AddAsync(entity, cancellationToken).AsTask();
133         }
134 
135         public virtual void AddRange(IEnumerable<TEntity> entities)
136         {
137             dbSet.AddRange(entities);
138         }
139 
140         public virtual Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default)
141         {
142             return dbSet.AddRangeAsync(entities, cancellationToken);
143         }
144 
145         public virtual void Delete(TEntity entity, bool isSoftDelete)
146         {
147             dbSet.Attach(entity);
148             if (isSoftDelete)
149             {
150                 if (entity is ILogicallyDeletable)
151                 {
152                     (entity as ILogicallyDeletable).IsDeleted = true;
153                 }
154                 else
155                 {
156                     throw new InvalidOperationException($"请求软删除的实体不完成{nameof(ILogicallyDeletable)}接口。");
157                 }
158             }
159             else
160             {
161                 dbSet.Remove(entity);
162             }
163         }
164 
165         public virtual Task DeleteAsync(TEntity entity, bool isSoftDelete, CancellationToken cancellationToken = default)
166         {
167             Delete(entity, isSoftDelete);
168             return Task.CompletedTask;
169         }
170 
171         public virtual void DeleteRange(IEnumerable<TEntity> entities, bool isSoftDelete)
172         {
173             dbSet.AttachRange(entities);
174             foreach (var entity in entities)
175             {
176                 Delete(entity, isSoftDelete);
177             }
178         }
179 
180         public virtual Task DeleteRangeAsync(IEnumerable<TEntity> entities, bool isSoftDelete, CancellationToken cancellationToken = default)
181         {
182             DeleteRange(entities, isSoftDelete);
183             return Task.CompletedTask;
184         }
185 
186         public virtual TEntity Find(TEntity entity, bool ignoreNullValue)
187         {
188             var exp = GenerateWhere(dbContext, entity, ignoreNullValue);
189 
190             return Set.SingleOrDefault(exp);
191         }
192 
193         public virtual Task<TEntity> FindAsync(TEntity entity, bool ignoreNullValue)
194         {
195             var exp = GenerateWhere(dbContext, entity, ignoreNullValue);
196 
197             return Set.SingleOrDefaultAsync(exp);
198         }
199 
200         public virtual int SaveChanges()
201         {
202             ProcessChangedEntity();
203             return dbContext.SaveChanges();
204         }
205 
206         public virtual Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
207         {
208             ProcessChangedEntity();
209             return dbContext.SaveChangesAsync(cancellationToken);
210         }
211 
212         public virtual IQueryable<TEntity> Set => dbSet.AsNoTracking();
213 
214         public virtual void Update(TEntity entity)
215         {
216             ResetDeletedMark(entity);
217             dbSet.Update(entity);
218         }
219 
220         public virtual Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
221         {
222             Update(entity);
223             return Task.CompletedTask;
224         }
225 
226         public virtual void UpdateRange(IEnumerable<TEntity> entities)
227         {
228             ResetDeletedMark(entities.ToArray());
229             dbSet.UpdateRange(entities);
230         }
231 
232         public virtual Task UpdateRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default)
233         {
234             UpdateRange(entities);
235             return Task.CompletedTask;
236         }
237 
238         static private Expression<Func<TEntity, bool>> GenerateWhere(TDbContext dbContext, TEntity entity, bool ignoreNullValue)
239         {
240             //查找实体范例主键
241             var model = dbContext.Model.FindEntityType(typeof(TEntity));
242             var key = model.FindPrimaryKey();
243 
244             //查找一切主键属性,假如没有主键就运用一切实体属性
245             IEnumerable<PropertyInfo> props;
246             if (key != null)
247             {
248                 props = key.Properties.Select(x => x.PropertyInfo);
249             }
250             else
251             {
252                 props = model.GetProperties().Select(x => x.PropertyInfo);
253             }
254 
255             //生成表达式参数
256             ParameterExpression parameter = Expression.Parameter(typeof(TEntity), "x");
257 
258             //初始化提取实体范例一切属性信息生成属性接见表达式并包装备用
259             var keyValues = props.Select(x => new { key = x, value = x.GetValue(entity), propExp = Expression.Property(parameter, x) });
260             //初始化存储由基础范例构成的属性信息(只需个空鸠合,现实数据在后面的轮回中添补)
261             var primitiveKeyValues = keyValues.Take(0).Where(x => IsPrimitiveType(x.key.PropertyType));
262             //初始化基础范例属性的相称比较表达式存储鸠合(只需个空鸠合,现实数据在后面的轮回中添补)
263             var equals = primitiveKeyValues.Take(0).Select(x => Expression.Equal(x.propExp, Expression.Constant(x.value)));
264             //初始化庞杂范例属性存储鸠合
265             var notPrimitiveKeyValues = primitiveKeyValues;
266 
267             //假如另有元素,申明上次用于提取信息的庞杂属性内部还存在庞杂属性,接下来用提取到的基础范例属性信息生成相称比较表达式并合并到存储鸠合然后继承提取剩下的庞杂范例属性的内部属性
268             while (keyValues.Count() > 0)
269             {
270                 if (ignoreNullValue)
271                 {
272                     keyValues = keyValues.Where(x => x.value != null);
273                 }
274                 //提取由基础范例构成的属性信息
275                 primitiveKeyValues = keyValues.Where(x => IsPrimitiveType(x.key.PropertyType));
276                 //生成基础范例属性的相称比较表达式
277                 equals = equals.Concat(primitiveKeyValues.Select(x => Expression.Equal(x.propExp, Expression.Constant(x.value))));
278                 //提取庞杂范例属性
279                 notPrimitiveKeyValues = keyValues.Except(primitiveKeyValues);
280                 //离别提取各个庞杂范例属性内部的属性信息继承生成内部属性接见表达式
281                 keyValues =
282                     from kv in notPrimitiveKeyValues
283                     from propInfo in kv.value.GetType().GetProperties()
284                     select new { key = propInfo, value = propInfo.GetValue(kv.value), propExp = Expression.Property(kv.propExp, propInfo) };
285             }
286 
287             //假如相称比较表达式有多个,将一切相称比较表达式用 && 运算连接起来
288             var and = equals.First();
289             foreach (var eq in equals.Skip(1))
290             {
291                 and = Expression.AndAlso(and, eq);
292             }
293 
294             //生成完全的过滤前提表达式,形如:  (TEntity x) => { return x.a == ? && x.b == ? && x.obj1.m == ? && x.obj1.n == ? && x.obj2.u.v == ?; }
295             var exp = Expression.Lambda<Func<TEntity, bool>>(and, parameter);
296 
297             //推断某个范例是不是是基础数据范例
298             static bool IsPrimitiveType(Type type)
299             {
300                 var primitiveTypes = new[] {
301                     typeof(sbyte)
302                     ,typeof(byte)
303                     ,typeof(short)
304                     ,typeof(ushort)
305                     ,typeof(int)
306                     ,typeof(uint)
307                     ,typeof(long)
308                     ,typeof(ulong)
309                     ,typeof(float)
310                     ,typeof(double)
311                     ,typeof(decimal)
312                     ,typeof(char)
313                     ,typeof(string)
314                     ,typeof(bool)
315                     ,typeof(DateTime)
316                     ,typeof(DateTimeOffset)
317                     //,typeof(Enum)
318                     ,typeof(Guid)};
319 
320                 var tmp =
321                     type.IsDerivedFrom(typeof(Nullable<>))
322                     ? Nullable.GetUnderlyingType(type)
323                     : type;
324 
325                 return tmp.IsEnum || primitiveTypes.Contains(tmp);
326             }
327 
328             return exp;
329         }
330     }

敕令

敕令基类

 1     public abstract class MediatRCommand : MediatRCommand<Unit>, ICommand, IRequest
 2     {
 3     }
 4 
 5     public abstract class MediatRCommand<TResult> : ICommand<TResult>, IRequest<TResult>
 6     {
 7         public Guid Id { get; }
 8 
 9         public DateTimeOffset Timestamp { get; }
10 
11         public MediatRCommand()
12         {
13             Id = Guid.NewGuid();
14             Timestamp = DateTimeOffset.Now;
15         }
16     }

示例详细敕令,敕令只包括参数信息,怎样运用参数信息完成任务是敕令处置惩罚器的事

 1     public class ListUserCommand : MediatRCommand<IPagedList<ApplicationUser>>
 2     {
 3         public PageInfo PageInfo { get; }
 4         public QueryFilter QueryFilter { get; }
 5         public ListUserCommand(PageInfo pageInfo, QueryFilter queryFilter)
 6         {
 7             PageInfo = pageInfo;
 8             QueryFilter = queryFilter;
 9         }
10     }

敕令总线

 1     public class MediatRCommandBus<TCommand, TResult> : ICommandBus<TCommand, TResult>
 2         where TCommand : MediatRCommand<TResult>
 3     {
 4         private readonly IMediator mediator;
 5         private readonly ICommandStore commandStore;
 6 
 7         public MediatRCommandBus(IMediator mediator, ICommandStore commandStore)
 8         {
 9             this.mediator = mediator;
10             this.commandStore = commandStore;
11         }
12 
13         public virtual Task<TResult> SendCommandAsync(TCommand command, CancellationToken cancellationToken = default)
14         {
15             commandStore?.SaveAsync(command, cancellationToken);
16             return mediator.Send(command, cancellationToken);
17         }
18 
19         Task ICommandBus<TCommand>.SendCommandAsync(TCommand command, CancellationToken cancellationToken)
20         {
21             return SendCommandAsync(command, cancellationToken);
22         }
23     }
24 
25     public class MediatRCommandBus<TCommand> : MediatRCommandBus<MediatRCommand<Unit>, Unit>
26         where TCommand : MediatRCommand<Unit>
27     {
28         public MediatRCommandBus(IMediator mediator, ICommandStore commandStore) : base(mediator, commandStore)
29         {
30         }
31     }

敕令处置惩罚器

敕令处置惩罚器基类

 1     public abstract class MediatRCommandHandler<TCommand, TResult> : ICommandHandler<TCommand, TResult>, IRequestHandler<TCommand, TResult>
 2     where TCommand : MediatRCommand<TResult>
 3     {
 4         public abstract Task<TResult> Handle(TCommand command, CancellationToken cancellationToken = default);
 5 
 6         Task ICommandHandler<TCommand>.Handle(TCommand command, CancellationToken cancellationToken)
 7         {
 8             return Handle(command, cancellationToken);
 9         }
10     }
11 
12     public abstract class MediatRCommandHandler<TCommand> : MediatRCommandHandler<TCommand, Unit>
13         where TCommand : MediatRCommand
14     {
15     }

详细敕令处置惩罚器示例,运用注入的仓储查询数据,ApplicationUser 在这里就是现实上的聚合根实体

 1     public class ListUserCommandHandler : MediatRCommandHandler<ListUserCommand, IPagedList<ApplicationUser>>
 2     {
 3         private IEFCoreRepository<ApplicationUser, int, ApplicationIdentityDbContext> repository;
 4 
 5         public ListUserCommandHandler(IEFCoreRepository<ApplicationUser, int, ApplicationIdentityDbContext> repository)
 6         {
 7             this.repository = repository;
 8         }
 9 
10         public override Task<IPagedList<ApplicationUser>> Handle(ListUserCommand command, CancellationToken cancellationToken = default)
11         {
12             return repository.Set
13                 .OrderBy(x => x.Id)
14                 .ToPagedListAsync(command.PageInfo.PageNumber, command.PageInfo.PageSize);
15         }
16     }

敕令存储

什么都没干,现实运用时能够运用数据库保存相干信息

 1     public class InProcessCommandStore : ICommandStore<bool>
 2     {
 3         public bool Save(ICommand command)
 4         {
 5             return SaveAsync(command).Result;
 6         }
 7 
 8         public Task<bool> SaveAsync(ICommand command, CancellationToken cancellationToken = default)
 9         {
10             return Task.FromResult(true);
11         }
12 
13         void ICommandStore.Save(ICommand command)
14         {
15             Save(command);
16         }
17 
18         Task ICommandStore.SaveAsync(ICommand command, CancellationToken cancellationToken)
19         {
20             return SaveAsync(command, cancellationToken);
21         }
22     }

事宜部分和敕令基础雷同,详细代码能够到文章末端下载项目代码检察。

运用

在 Startup.ConfigureServices 要领中注册相干效劳,事宜总线和敕令总线都运用 MediatR 完成。.Net Core 内置 DI 支撑注册泛型效劳,所以某个实体在现实运用时注入泛型仓储就示意这个实体是聚合根,不必提早定义详细的聚合根实体仓储,所以删除运用代码相当于删除了仓储定义。

1  services.AddScoped(typeof(ICommandBus<>), typeof(MediatRCommandBus<>));
2  services.AddScoped(typeof(ICommandBus<,>), typeof(MediatRCommandBus<,>));
3  services.AddScoped(typeof(ICommandStore), typeof(InProcessCommandStore));
4  services.AddScoped(typeof(IEventBus), typeof(MediatREventBus));
5  services.AddScoped(typeof(IEventBus<>), typeof(MediatREventBus<>));
6  services.AddScoped(typeof(IEventStore), typeof(InProcessEventStore));
7  services.AddScoped(typeof(IEFCoreRepository<,>), typeof(EFCoreRepository<,>));
8  services.AddScoped(typeof(IEFCoreRepository<,,>), typeof(EFCoreRepository<,,>));
9  services.AddMediatR(typeof(ListUserCommandHandler).GetTypeInfo().Assembly);

示例运用比较简朴,就不定义效劳了,假如须要定义效劳,那末运用效劳的平常是敕令处置惩罚器,仓储由效劳运用。这里敕令处置惩罚器直接运用仓储。在控制器中注入敕令总线,向敕令总线发送敕令就能够猎取效果。MediatR 会自动依据发送的敕令范例查找婚配的敕令处置惩罚器去挪用。

 1     [ApiController]
 2     [Route("api/[controller]")]
 3     public class UsersController : ControllerBase
 4     {
 5         private readonly ICommandBus<ListUserCommand, IPagedList<ApplicationUser>> _commandBus;
 6         private readonly IMapper _mapper;
 7 
 8         public UsersController(ICommandBus<ListUserCommand, IPagedList<ApplicationUser>> commandBus, IMapper mapper)
 9         {
10             _commandBus = commandBus;
11             _mapper = mapper;
12         }
13 
14         /// <summary>
15         /// 猎取用户列表
16         /// </summary>
17         /// <param name="page">页码</param>
18         /// <param name="size">每页条目数</param>
19         /// <returns>用户列表</returns>
20         [HttpGet]
21         [Produces("application/json")] //声明接口响应 json 数据
22         public async Task<IActionResult> GetAsync(int? page, int? size)
23         {
24             var cmd = new ListUserCommand(new PageInfo(page ?? 1, size ?? 10), new QueryFilter());
25             var users = await _commandBus.SendCommandAsync(cmd, default);
26 
27             return new JsonResult(
28                 new
29                 {
30                     rows = users.Select(u => _mapper.Map<ApplicationUserDto>(u)),
31                     total = users.PageCount, //总页数
32                     page = users.PageNumber, //当前页码
33                     records = users.TotalItemCount //总纪录数
34                 }
35             );
36         }
37     }

运用就是这么简朴。运用者基础不须要知道敕令处置惩罚器的存在,把敕令发送到总线,等着吸收效果就能够了。

事宜平常由敕令处置惩罚器激发,能够革新敕令处置惩罚器用 DI 注入事宜总线,然后在敕令处置惩罚器中向事宜总线发送事宜,事宜总线就会自动触发响应的事宜处置惩罚器。

结语

完全的流程也许就是:控制器运用注入的效劳实行营业流程,营业效劳向敕令总线发送敕令,敕令总线触发处置惩罚器处置惩罚敕令,敕令处置惩罚器向事宜总线发送事宜,事宜总线触发事宜处置惩罚器处置惩罚事宜,事宜处置惩罚器在处置惩罚事宜后向事宜总线发送新的事宜触发后续事宜处置惩罚器继承处置惩罚新的事宜(假如须要),直到末了不发送事宜的事宜处置惩罚器完成处置惩罚。全部流程结束。在此历程当中总线会自动挪用注入的总线音讯存储来耐久化敕令和事宜,至此,一个环环相扣的极简 DDD+CQRS+EDA+ES 架构搭建完成!

想要现实体验的朋侪能够到文章末端下载项目并运转体验。启动调试后接见 /swagger 然后尝试体验挪用 api/users 接口。

 

转载请完全保存以下内容并在显眼位置标注,未经受权删除以下内容举行转载盗用的,保存追查法律责任的权益!

本文地点:

完全源代码:

内里有种种小东西,这只是个中之一,不厌弃的话能够Star一下。

图示JVM工作原理

参与评论