CQRS輕量級框架【CQRSlite】學習使用小記

丶Pz發表於2018-05-24

前言

  這幾天在研究DDD和CQRS。快把我繞暈了。發現國外的好文質量還是挺高的。之所以先體驗CQRSlite這個小框架,是因為看了一位大神寫的文章:https://www.codeproject.com/articles/991648/cqrs-a-cross-examination-of-how-it-works 。於是乎,下載框架體驗一下。

什麼是CQRS?

  Command Query Responsibility Segregation 的簡稱。翻譯過來就是命令查詢職責分離模式。在具體的也就不由我這個小菜鳥去闡述了。根據我的理解,在專案中,我們通常做一些資料儲存的工作,但是資料查詢的時候可能需要聯合查詢多張表,為了優化查詢速度會通過一些冗餘欄位或者快取在或者其他方式去優化。而查詢的一個流程基本和新增修改刪除沒有太大的關係。在平日的開發過程中,基本上,查詢和增刪改都放在同一個服務中。而CQRS要做的就是讓他們分離。命令是命令,查詢是查詢。而他們之間是如何互動的呢,這就要用到 Pub/Sub 機制了。(不過框架裡的實現貌似是通過反射註冊一些Handler實現的)下面這個圖或許可以幫你理解一下。(圖片來源於上文的文章中)

CQRSlite

  其他的就不瞎扯了,我也只是通過這個輕量級框架去理解CQRS的實現。所以下面就簡單介紹這個框架以及我的學習過程。

  首先看一下自帶Demo,Demo很簡單,就是一個增加,修改和展示。

   

  

  是不是超簡單的Demo。下面我們看一下具體程式碼。

  首先,新增這個動作屬於一個命令,那麼我們就建立一個Create的命令。然後通過CommandBus傳送命令。

/// <summary>
    /// 【建立一個新項】命令
    /// </summary>
    public class CreateInventoryItem : ICommand 
    {
        public readonly string Name;
        
        public CreateInventoryItem(Guid id, string name)
        {
            Id = id;
            Name = name;
        }

        public Guid Id { get; set; }
        public int ExpectedVersion { get; set; }
    }

  Controller層只要負責傳送命令即可

 [HttpPost]
        public async Task<ActionResult> Add(string name, CancellationToken cancellationToken)
        {
            await _commandSender.Send(new CreateInventoryItem(Guid.NewGuid(), name), cancellationToken);
            return RedirectToAction("Index");
        }

  當 CreateInventoryItem 這個命令傳送出去之後,框架就去找匹配的命令處理器。程式碼如下:

    public class InventoryCommandHandlers : ICommandHandler<CreateInventoryItem>
    {
        private readonly ISession _session;

        public InventoryCommandHandlers(ISession session)
        {
            _session = session;
        }

        public async Task Handle(CreateInventoryItem message)
        {
            var item = new InventoryItem(message.Id, message.Name);
            await _session.Add(item);
            await _session.Commit();
        }

  然後通過Handle方法去處理這個命令。可以看到,Handle中呼叫了session.Add 和Commit方法。

  Add方法,就是將這個Aggregate新增到記憶體快取中。用於後期版本追蹤。

  _trackedAggregates = new Dictionary<Guid, AggregateDescriptor>();

  然後,Commit方法又呼叫了CacheRepository,CacheRepository又呼叫了EventStore的Save方法,看到EventStore這個詞就要提起EventSourcing。其實我理解的事件朔源就是說,通過一定順序的事件序列可以重新得到當前聚合狀態。上文中的CacheRepository和EventStore都是CQRSlite框架中的實現。

 
//Session.cs
public async Task Commit(CancellationToken cancellationToken = default(CancellationToken)) { var tasks = new Task[_trackedAggregates.Count]; var i = 0; foreach (var descriptor in _trackedAggregates.Values) { //這個_repository 是cacheRepository tasks[i] = _repository.Save(descriptor.Aggregate, descriptor.Version, cancellationToken); i++; } await Task.WhenAll(tasks).ConfigureAwait(false); _trackedAggregates.Clear(); }
//CacheRepository.cs
public
async Task Save<T>(T aggregate, int? expectedVersion = null, CancellationToken cancellationToken = default(CancellationToken)) where T : AggregateRoot { var @lock = _locks.GetOrAdd(aggregate.Id, CreateLock); await @lock.WaitAsync(cancellationToken).ConfigureAwait(false); try { if (aggregate.Id != Guid.Empty && !await _cache.IsTracked(aggregate.Id).ConfigureAwait(false)) { await _cache.Set(aggregate.Id, aggregate).ConfigureAwait(false); } //這裡的_repository是Domain.Repository await _repository.Save(aggregate, expectedVersion, cancellationToken).ConfigureAwait(false); } catch (Exception) { await _cache.Remove(aggregate.Id).ConfigureAwait(false); throw; } finally { @lock.Release(); } }
 
//Repository.cs
public async Task Save<T>(T aggregate, int? expectedVersion = null, CancellationToken cancellationToken = default(CancellationToken)) where T : AggregateRoot { if (expectedVersion != null && (await _eventStore.Get(aggregate.Id, expectedVersion.Value, cancellationToken).ConfigureAwait(false)).Any()) { throw new ConcurrencyException(aggregate.Id); } var changes = aggregate.FlushUncommitedChanges(); //最後呼叫EventStore的Save方法。也就是隻儲存事件 await _eventStore.Save(changes, cancellationToken).ConfigureAwait(false); if (_publisher != null) { foreach (var @event in changes) { await _publisher.Publish(@event, cancellationToken).ConfigureAwait(false); } } }
 
//實現IEventStore介面的自定義EventStore
public async Task Save(IEnumerable<IEvent> events, CancellationToken cancellationToken = default(CancellationToken)) { foreach (var @event in events) { _inMemoryDb.TryGetValue(@event.Id, out var list); if (list == null) { list = new List<IEvent>(); _inMemoryDb.Add(@event.Id, list); } list.Add(@event); //呼叫事件釋出 await _publisher.Publish(@event, cancellationToken); } }

  在當前的這個例子中,事件是Created

public class InventoryItemCreated : IEvent 
    {
        public readonly string Name;
        public InventoryItemCreated(Guid id, string name) 
        {
            Id = id;
            Name = name;
        }

        public Guid Id { get; set; }
        public int Version { get; set; }
        public DateTimeOffset TimeStamp { get; set; }
    }

  最後呢,View層接收到事件,進行處理就OK了。

 public class InventoryItemDetailView : ICancellableEventHandler<InventoryItemCreated>,
        ICancellableEventHandler<InventoryItemDeactivated>,
        ICancellableEventHandler<InventoryItemRenamed>,
        ICancellableEventHandler<ItemsRemovedFromInventory>,
        ICancellableEventHandler<ItemsCheckedInToInventory>
    {
        public Task Handle(InventoryItemCreated message, CancellationToken token)
        {
            InMemoryDatabase.Details.Add(message.Id,
                new InventoryItemDetailsDto(message.Id, message.Name, 0, message.Version));
            return Task.CompletedTask;
        }

  相信小夥伴們讀到這裡還是一臉懵逼。沒關係,上文中的簡化版流程如下:

  

  由於這裡是同步的,所以在檢視上展示是沒有什麼問題的,但是真正使用的時候大部分檢視展示可能由於非同步處理事件更新View,所以展示上會有延遲。CQRS和ES使用上還是和普通的服務開發有些區別的。不過作為入門,我能學到的目前就這麼多,裡面肯定還有更大的空間去發掘。

總結

  本文不是一個CQRS的介紹,也不是一篇科普文章,只是一個小菜鳥的學習過程。有錯誤之處在所難免。

 

相關文章