ABP CQRS 實現案例: 基於 MediatR 實現

介紹

CQRS(命令查詢職責分離模式) 從業務上分離修改 (Command,增,刪,改,會對系統狀態進行修改) 和查詢(Query,查,不會對系統狀態進行修改) 的行爲。從而使得邏輯更加清晰,便於對不同部分進行鍼對性的優化。

CQRS 基本思想在於,任何一個對象的方法可以分爲兩大類

本文主要介紹如何使用基於 MediatR實現的 Abp.Cqrs類庫,以及如何從讀寫分離模式來思考問題. 本文旨在探索 cqrs 如果落地,目前僅支持單機模式,不支持分佈式。 本文案例主要介紹了命令的使用方式,分離寫的職責,對 event 沒有過多的介紹和使用。

源碼:

  1. https://github.com/ZhaoRd/abp_cqrs

  2. https://github.com/ZhaoRd/abpcqrsexample

項目案例 -- 電話簿 (後端)

本案例後端使用 abp 官方模板,可在 https://aspnetboilerplate.com/Templates 創建項目,前端使用的是 ng-alain 模板。

引入 Abp.Cqrs 類庫

core項目中安裝 cqrs 包,並且添加模塊依賴

在命令或事件處理類的項目中,註冊 cqrs 處理

添加電話簿實體類

電話簿實體類代碼,代碼封裝了簡單的業務 修改聯繫方式修改姓名,封裝這兩個業務,主要是爲了命令服務,注意實體類的屬性可訪問性是 protected,這意味着該實體具有不可變性,如果要修改實體屬性 (修改對象狀態),只能通過實體提供的業務方法進行修改。

/// <summary>
    /// 電話簿
    /// </summary>
    public class TelephoneBook : FullAuditedAggregateRoot<Guid>
    {
        /// <summary>
        /// 初始化<see cref="TelephoneBook"/>實例
        /// </summary>
        public TelephoneBook()
        {
        }
        /// <summary>
        /// 初始化<see cref="TelephoneBook"/>實例
        /// </summary>
        public TelephoneBook([NotNull]string name, string emailAddress, string tel)
        {
            this.Name = name;
            this.EmailAddress = emailAddress;
            this.Tel = tel;
        }
        /// <summary>
        /// 姓名
        /// </summary>
        public string Name { get; protected set; }
        /// <summary>
        /// 郵箱
        /// </summary>
        public string EmailAddress { get; protected set; }
        /// <summary>
        /// 電話號碼
        /// </summary>
        public string Tel { get; protected set; }
        /// <summary>
        /// 修改聯繫方式
        /// </summary>
        /// <param ></param>
        /// <param ></param>
        public void Change(string emailAddress,string tel)
        {
            this.EmailAddress = emailAddress;
            this.Tel = tel;
        }
        /// <summary>
        /// 修改姓名
        /// </summary>
        /// <param ></param>
        public void ChangeName(string name)
        {
            this.Name = name;
        }
    }

更新 ef 腳本

AddressBookDbContext中添加一下代碼

public
 
DbSet
<
TelephoneBook
>
 
TelephoneBooks
 
{
 
get
;
 
set
;
 
}

執行腳本 add-migrationAdd_TelephoneBookupdate-database

定義 創建、更新、刪除命令

    /// <summary>
    /// 創建電話簿命令
    /// </summary>
    public class CreateTelephoneBookCommand:Command
    {
        public TelephoneBookDto TelephoneBook { get;private set; }
        public CreateTelephoneBookCommand(TelephoneBookDto book)
        {
            this.TelephoneBook = book;
        }
    }
    /// <summary>
    /// 更新電話命令
    /// </summary>
    public class UpdateTelephoneBookCommand : Command
    {
        public TelephoneBookDto TelephoneBook { get; private set; }
        public UpdateTelephoneBookCommand(TelephoneBookDto book)
        {
            this.TelephoneBook = book;
        }
    }
    /// <summary>
    /// 刪除電話簿命令
    /// </summary>
    public class DeleteTelephoneBookCommand : Command
    {
        public EntityDto<Guid> TelephoneBookId { get; private set; }
        public DeleteTelephoneBookCommand(EntityDto<Guid> id)
        {
            this.TelephoneBookId = id;
        }
    }

命令代碼很簡單,只要提供命令需要的數據即可

命令處理類

cqrs 中,是通過命令修改實體屬性的,所以命令處理類需要依賴相關倉儲。 關注更新命令處理,可以看到不是直接修改實體屬性,而是通過實體提供的業務方法修改實體屬性。

    /// <summary>
    /// 更新電話簿命令處理
    /// </summary>
    public class UpdateTelephoneBookCommandHandler : ICommandHandler<UpdateTelephoneBookCommand>
    {
        private readonly IRepository<TelephoneBook, Guid> _telephoneBookRepository;
        public UpdateTelephoneBookCommandHandler(IRepository<TelephoneBook, Guid> telephoneBookRepository)
        {
            this._telephoneBookRepository = telephoneBookRepository;
        }
        public async Task<Unit> Handle(UpdateTelephoneBookCommand request, CancellationToken cancellationToken)
        {
            var tenphoneBook = await this._telephoneBookRepository.GetAsync(request.TelephoneBook.Id.Value);
            tenphoneBook.Change(request.TelephoneBook.EmailAddress,request.TelephoneBook.Tel);
            return Unit.Value;
        }
    }
    /// <summary>
    /// 刪除電話簿命令
    /// </summary>
    public class DeleteTelephoneBookCommandHandler : ICommandHandler<DeleteTelephoneBookCommand>
    {
        private readonly IRepository<TelephoneBook, Guid> _telephoneBookRepository;
        public DeleteTelephoneBookCommandHandler(
            IRepository<TelephoneBook, Guid> telephoneBookRepository)
        {
            this._telephoneBookRepository = telephoneBookRepository;
        }
        public async Task<Unit> Handle(DeleteTelephoneBookCommand request, CancellationToken cancellationToken)
        {
            await this._telephoneBookRepository.DeleteAsync(request.TelephoneBookId.Id);
            return Unit.Value;
        }
    }
    /// <summary>
    /// 創建電話簿命令
    /// </summary>
    public class CreateTelephoneBookCommandHandler : ICommandHandler<CreateTelephoneBookCommand>
    {
        private readonly IRepository<TelephoneBook, Guid> _telephoneBookRepository;
        public CreateTelephoneBookCommandHandler(IRepository<TelephoneBook, Guid> telephoneBookRepository)
        {
            this._telephoneBookRepository = telephoneBookRepository;
        }
        public async Task<Unit> Handle(CreateTelephoneBookCommand request, CancellationToken cancellationToken)
        {
            var telephoneBook = new TelephoneBook(request.TelephoneBook.Name, request.TelephoneBook.EmailAddress, request.TelephoneBook.Tel);
            await this._telephoneBookRepository.InsertAsync(telephoneBook);
            return Unit.Value;
        }
    }

DTO 類定義

DTO 負責和前端交互數據

    [AutoMap(typeof(TelephoneBook))]
    public class TelephoneBookDto : EntityDto<Guid?>
    {
        /// <summary>
        /// 姓名
        /// </summary>
        public string Name { get;  set; }
        /// <summary>
        /// 郵箱
        /// </summary>
        public string EmailAddress { get;  set; }
        /// <summary>
        /// 電話號碼
        /// </summary>
        public string Tel { get;  set; }
    }
    [AutoMap(typeof(TelephoneBook))]
    public class TelephoneBookListDto : FullAuditedEntityDto<Guid>
    {
        /// <summary>
        /// 姓名
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 郵箱
        /// </summary>
        public string EmailAddress { get; set; }
        /// <summary>
        /// 電話號碼
        /// </summary>
        public string Tel { get; set; }
    }

實現應用層

應用層需要依賴兩個內容

在應用層中不在直接修改實體屬性 觀察 創建 編輯 刪除 業務,可以看到這些業務都在做意見事情:發佈命令.

    public class TelephoneBookAppServiceTests : AddressBookTestBase
    {
        private readonly ITelephoneBookAppService _service;
        public TelephoneBookAppServiceTests()
        {
            _service = Resolve<ITelephoneBookAppService>();
        }
        /// <summary>
        /// 獲取所有看通訊錄
        /// </summary>
        /// <returns></returns>
        [Fact]
        public async Task GetAllTelephoneBookList_Test()
        {
            // Act
            var output = await _service.GetAllTelephoneBookList();
            // Assert
            output.Count().ShouldBe(0);
        }
        /// <summary>
        /// 創建通訊錄
        /// </summary>
        /// <returns></returns>
        [Fact]
        public async Task CreateTelephoneBook_Test()
        {
            // Act
            await _service.CreateOrUpdate(
                new TelephoneBookDto()
                    {
                        EmailAddress = "yun.zhao@qq.com",
                        Name = "趙雲",
                        Tel="12345678901"
                    });
            await UsingDbContextAsync(async context =>
                {
                    var zhaoyun = await context
                                           .TelephoneBooks
                                           .FirstOrDefaultAsync(u => u.Name == "趙雲");
                    zhaoyun.ShouldNotBeNull();
                });
        }
        /// <summary>
        /// 更新通訊錄
        /// </summary>
        /// <returns></returns>
        [Fact]
        public async Task UpdateTelephoneBook_Test()
        {
            // Act
            await _service.CreateOrUpdate(
                new TelephoneBookDto()
                    {
                        EmailAddress = "yun.zhao@qq.com",
                        Name = "趙雲",
                        Tel = "12345678901"
                    });
            var zhaoyunToUpdate = await UsingDbContextAsync(async context =>
                {
                    var zhaoyun = await context
                                           .TelephoneBooks
                                           .FirstOrDefaultAsync(u => u.Name == "趙雲");
                    return zhaoyun;
                });
            zhaoyunToUpdate.ShouldNotBeNull();
            await _service.CreateOrUpdate(
                new TelephoneBookDto()
                    {
                        Id = zhaoyunToUpdate.Id,
                        EmailAddress = "yun.zhao@sina.com",
                        Name = "趙雲",
                        Tel = "12345678901"
                    });
            await UsingDbContextAsync(async context =>
                {
                    var zhaoyun = await context
                                      .TelephoneBooks
                                      .FirstOrDefaultAsync(u => u.Name == "趙雲");
                    zhaoyun.ShouldNotBeNull();
                    zhaoyun.EmailAddress.ShouldBe("yun.zhao@sina.com");
                });
        }
        /// <summary>
        /// 刪除通訊錄
        /// </summary>
        /// <returns></returns>
        [Fact]
        public async Task DeleteTelephoneBook_Test()
        {
            // Act
            await _service.CreateOrUpdate(
                new TelephoneBookDto()
                    {
                        EmailAddress = "yun.zhao@qq.com",
                        Name = "趙雲",
                        Tel = "12345678901"
                    });
            var zhaoyunToDelete = await UsingDbContextAsync(async context =>
                {
                    var zhaoyun = await context
                                      .TelephoneBooks
                                      .FirstOrDefaultAsync(u => u.Name == "趙雲");
                    return zhaoyun;
                });
            zhaoyunToDelete.ShouldNotBeNull();
            await _service.Delete(
                new EntityDto<Guid>()
                    {
                        Id = zhaoyunToDelete.Id
                    });
            await UsingDbContextAsync(async context =>
                {
                    var zhaoyun = await context
                                      .TelephoneBooks
                                      .Where(c=>c.IsDeleted == false)
                                      .FirstOrDefaultAsync(u => u.Name == "趙雲");
                    zhaoyun.ShouldBeNull();
                });
        }
    }

項目案例 -- 電話簿 (前端 ng-alain 項目)

使用 ng-alain 實現前端項目

界面預覽

列表界面代碼

import { Component, OnInit, Injector } from '@angular/core';
import { _HttpClient, ModalHelper } from '@delon/theme';
import { SimpleTableColumn, SimpleTableComponent } from '@delon/abc';
import { SFSchema } from '@delon/form';
import { finalize } from 'rxjs/operators';
import { AppComponentBase } from '@shared/app-component-base';
import { TelephoneBookServiceProxy, TelephoneBookDto, TelephoneBookListDto } from '@shared/service-proxies/service-proxies';
import { BooksCreateComponent } from './../create/create.component'
import { BooksEditComponent } from './../edit/edit.component'
@Component({
  selector: 'books-list',
  templateUrl: './list.component.html',
})
export class BooksListComponent extends AppComponentBase  implements OnInit {
    params: any = {};
    list = [];
    loading = false;
    constructor( injector: Injector,private http: _HttpClient, private modal: ModalHelper,
      private _telephoneBookService:TelephoneBookServiceProxy) { 
        super(injector);
      }
    ngOnInit() { 
      this.loading = true;
      this._telephoneBookService
          .getAllTelephoneBookList()
          .pipe(finalize(
            ()=>{
              this.loading = false;
            }
          ))
          .subscribe(res=>{
            this.list = res;
          })
          ;
    }
    edit(id: string): void {
      this.modal.static(BooksEditComponent, {
        bookId: id
      }).subscribe(res => {
        this.ngOnInit();
      });
    }
    add() {
       this.modal
         .static(BooksCreateComponent, { id: null })
         .subscribe(() => this.ngOnInit());
    }
    delete(book: TelephoneBookListDto): void {
      abp.message.confirm(
        "刪除通訊錄 '" + book.name + "'?"
      ).then((result: boolean) => {
        console.log(result);
        if (result) {
          this._telephoneBookService.delete(book.id)
            .pipe(finalize(() => {
              abp.notify.info("刪除通訊錄: " + book.name);
              this.ngOnInit();
            }))
            .subscribe(() => { });
        }
      });
    }
}
<page-header></page-header>
<nz-card>
  <!--
  <sf mode="search" [schema]="searchSchema" [formData]="params" (formSubmit)="st.reset($event)" (formReset)="st.reset(params)"></sf>
  -->
  <div class="my-sm">
    <button (click)="add()" nz-button nzType="primary">新建</button>
  </div>
<nz-table #tenantListTable 
[nzData]="list" 
[nzLoading]="loading" 
>
<thead nz-thead>
    <tr>
        <th nz-th>
            <span>序號</span>
        </th>
        <th nz-th>
            <span>姓名</span>
        </th>
        <th nz-th>
            <span>郵箱</span>
        </th>
        <th nz-th>
          <span>電話</span>
      </th>
        <th nz-th>
            <span>{{l('Actions')}}</span>
        </th>
    </tr>
</thead>
<tbody nz-tbody>
    <tr nz-tbody-tr *ngFor="let data of tenantListTable.data;let i=index;">
        <td nz-td>
            <span>
              {{(i+1)}}
            </span>
        </td>
        <td nz-td>
            <span>
                {{data.name}}
            </span>
        </td>
        <td nz-td>
            <span>
                {{data.emailAddress}}
            </span>
        </td>
        <td nz-td>
          <span>
              {{data.tel}}
          </span>
      </td>
        <td nz-td>
          <nz-dropdown>
              <a class="ant-dropdown-link" nz-dropdown>
                  <i class="anticon anticon-setting"></i>
                  操作
                  <i class="anticon anticon-down"></i>
              </a>
              <ul nz-menu>
                  <li nz-menu-item (click)="edit(data.id)">修改</li>
                  <li nz-menu-item (click)="delete(data)">
                      刪除
                  </li>
              </ul>
          </nz-dropdown>
      </td>
    </tr>
</tbody>
</nz-table>
  <!--
  <simple-table #st [data]="url" [columns]="columns" [extraParams]="params"></simple-table>
  -->
</nz-card>

新增界面代碼

<div class="modal-header">
    <div class="modal-title">創建通訊錄</div>
  </div>
  <nz-tabset>
    <nz-tab nzTitle="基本信息">
      <div nz-row>
        <div nz-col class="mt-sm">
          姓名
        </div>
        <div ng-col class="mt-sm">
          <input nz-input [(ngModel)]="book.name" />
        </div>
      </div>
      <div nz-row>
        <div nz-col class="mt-sm">
          郵箱
        </div>
        <div ng-col class="mt-sm">
          <input nz-input [(ngModel)]="book.emailAddress" />
        </div>
      </div>
      <div nz-row>
          <div nz-col class="mt-sm">
            電話號碼
          </div>
          <div ng-col class="mt-sm">
            <input nz-input [(ngModel)]="book.tel" />
          </div>
        </div>
    </nz-tab>
  </nz-tabset>
  <div class="modal-footer">
    <button nz-button [nzType]="'default'" [nzSize]="'large'" (click)="close()">
      取消
    </button>
    <button nz-button [nzType]="'primary'" [nzSize]="'large'" (click)="save()">
      保存
    </button>
  </div>
import { Component, OnInit,Injector } from '@angular/core';
  import { NzModalRef, NzMessageService } from 'ng-zorro-antd';
  import { _HttpClient } from '@delon/theme';
  import { TelephoneBookServiceProxy, TelephoneBookDto, TelephoneBookListDto } from '@shared/service-proxies/service-proxies';
import { AppComponentBase } from '@shared/app-component-base';
import * as _ from 'lodash';
import { finalize } from 'rxjs/operators';
  @Component({
    selector: 'books-create',
    templateUrl: './create.component.html',
  })
  export class BooksCreateComponent extends AppComponentBase implements OnInit {
    book: TelephoneBookDto = null;
    saving: boolean = false;
    constructor(injector: Injector,
      private _telephoneBookService:TelephoneBookServiceProxy,
      private modal: NzModalRef,
      public msgSrv: NzMessageService,
      private subject: NzModalRef,
      public http: _HttpClient
    ) { 
      super(injector);
    }
    ngOnInit(): void {
      this.book = new TelephoneBookDto();
      // this.http.get(`/user/${this.record.id}`).subscribe(res => this.i = res);
    }
    save(): void {
      this.saving = true;
      this._telephoneBookService.createOrUpdate(this.book)
        .pipe(finalize(() => {
          this.saving = false;
        }))
        .subscribe((res) => {
          this.notify.info(this.l('SavedSuccessfully'));
          this.close();
        });
    }
    close() {
      this.subject.destroy();
    }
  }

編輯頁面代碼

<div class="modal-header">
    <div class="modal-title">編輯通訊錄</div>
  </div>
  <nz-tabset>
    <nz-tab nzTitle="基本信息">
      <div nz-row>
        <div nz-col class="mt-sm">
          姓名:{{book.name}}
        </div>
      </div>
      <div nz-row>
        <div nz-col class="mt-sm">
          郵箱
        </div>
        <div ng-col class="mt-sm">
          <input nz-input [(ngModel)]="book.emailAddress" />
        </div>
      </div>
      <div nz-row>
          <div nz-col class="mt-sm">
            電話號碼
          </div>
          <div ng-col class="mt-sm">
            <input nz-input [(ngModel)]="book.tel" />
          </div>
        </div>
    </nz-tab>
  </nz-tabset>
  <div class="modal-footer">
    <button nz-button [nzType]="'default'" [nzSize]="'large'" (click)="close()">
      取消
    </button>
    <button nz-button [nzType]="'primary'" [nzSize]="'large'" (click)="save()">
      保存
    </button>
  </div>
import { Component, OnInit,Injector,Input } from '@angular/core';
  import { NzModalRef, NzMessageService } from 'ng-zorro-antd';
  import { _HttpClient } from '@delon/theme';
  import { TelephoneBookServiceProxy, TelephoneBookDto, TelephoneBookListDto } from '@shared/service-proxies/service-proxies';
import { AppComponentBase } from '@shared/app-component-base';
import * as _ from 'lodash';
import { finalize } from 'rxjs/operators';
  @Component({
    selector: 'books-edit',
    templateUrl: './edit.component.html',
  })
  export class BooksEditComponent extends AppComponentBase  implements OnInit {
    book: TelephoneBookDto = null;
    @Input()
    bookId:string = null;
    saving: boolean = false;
    constructor(injector: Injector,
      private _telephoneBookService:TelephoneBookServiceProxy,
      private modal: NzModalRef,
      public msgSrv: NzMessageService,
      private subject: NzModalRef,
      public http: _HttpClient
    ) { 
      super(injector);
      this.book = new TelephoneBookDto();
    }
    ngOnInit(): void {
     // this.book = new TelephoneBookDto();
     this._telephoneBookService.getForEdit(this.bookId)
     .subscribe(
     (result) => {
         this.book = result;
     });
      // this.http.get(`/user/${this.record.id}`).subscribe(res => this.i = res);
    }
    save(): void {
      this.saving = true;
      this._telephoneBookService.createOrUpdate(this.book)
        .pipe(finalize(() => {
          this.saving = false;
        }))
        .subscribe((res) => {
          this.notify.info(this.l('SavedSuccessfully'));
          this.close();
        });
    }
    close() {
      this.subject.destroy();
    }
  }

參考資料

  • 淺談命令查詢職責分離 (CQRS) 模式

  • 團隊開發框架實戰—CQRS 架構

  • DDD 領域驅動設計學習(四)- 架構(CQRS/EDA / 管道和過濾器)

  • DDD CQRS 架構和傳統架構的優缺點比較

  • CQRS Journey

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/U1gKVSj7jpqcfOJooin5Xg