你一定看得懂的 DDD+CQRS+EDA+ES 核心思想與極簡可運行代碼示例

来源:https://www.cnblogs.com/coredx/archive/2020/02/28/12364960.html
-Advertisement-
Play Games

前言 隨著分散式架構微服務的興起,DDD(領域驅動設計)、CQRS(命令查詢職責分離)、EDA(事件驅動架構)、ES(事件溯源)等概念也一併成為時下的火熱概念,我也在早些時候閱讀了一些大佬的分析文,學習相關概念,不過一直有種霧裡看花、似懂非懂的感覺。經過一段時間的學習和研究大佬的代碼後,自己設計實現 ...


前言

       隨著分散式架構微服務的興起,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 	   

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

-Advertisement-
Play Games
更多相關文章
  • 計算屬性關鍵詞: computed。 計算屬性在處理一些複雜邏輯時是很有用的。 下麵是字元串反轉的一些寫法,看起來有點複雜 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>demo</title> </head> <body> ...
  • 迴圈使用 v-for 指令。 v-for 指令需要以 site in sites 形式的特殊語法, sites 是源數據數組並且 site 是數組元素迭代的別名。 v-for 可以綁定數據到數組來渲染一個列表: <!DOCTYPE html> <html> <head> <meta charset= ...
  • 條件判斷使用 v-if 指令: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>demo</title> </head> <body> <script src="https://cdn.staticfile.org/vue/2. ...
  • 通過實例來看下 Vue 構造器中需要哪些內容 測試時這段代碼我直接寫在index.html中 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Vue 測試實例 - 菜鳥教程(runoob.com)</title> <script ...
  • 給項目的入口文件做點小改變: 【補充:編輯器建議使用vscode,我還沒裝,暫時先用phpstorm】 打開 APP.vue 文件,代碼如下(解釋在註釋中) <!-- 展示模板 --> <template> <div id="app"> <img src="./assets/logo.png"> < ...
  • 我之前是有安裝過npm的 使用淘寶 NPM 鏡像 $ npm install -g cnpm --registry=https://registry.npm.taobao.org 查看nmp版本 $ npm -v 使用 NPM 安裝vue $ cnpm install vue 在命令行中快速搭建大型 ...
  • 為什麼要用flex 基於css3簡單方便,更優雅的實現,瀏覽器相容性好,傳統的css實現一個div居中佈局要寫一堆代碼,而現在幾行代碼就搞定了,沒有理由不用flex。 相容性: Base Browsers: IE8.0+, Firefox40.0+, Chrome40.0+, iOS8.0+, An ...
  • 視頻線上率統計——基於驅動匯流排設備的領域驅動設計方法落地 [toc] 1.應用背景 本司智能信息箱產品是管控攝像頭電源,監控攝像頭視頻線上率的一個有效運維工具。因為統計視頻線上率是業主十分關心的問題,所以如何有效地統計視頻線上率是工程師需要努力解決的問題。 2.各視頻線上率統計方法比較 |方案|是否 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...