ABP CQRS 實現案例: 基於 MediatR 實現
介紹
CQRS(命令查詢職責分離模式) 從業務上分離修改 (Command,增,刪,改,會對系統狀態進行修改) 和查詢(Query,查,不會對系統狀態進行修改) 的行爲。從而使得邏輯更加清晰,便於對不同部分進行鍼對性的優化。
CQRS 基本思想在於,任何一個對象的方法可以分爲兩大類
-
命令 (Command): 不返回任何結果 (void),但會改變對象的狀態。
-
查詢 (Query): 返回結果,但是不會改變對象的狀態,對系統沒有副作用。
本文主要介紹如何使用基於 MediatR
實現的 Abp.Cqrs
類庫,以及如何從讀寫分離模式來思考問題. 本文旨在探索 cqrs 如果落地,目前僅支持單機模式,不支持分佈式。 本文案例主要介紹了命令的使用方式,分離寫的職責,對 event 沒有過多的介紹和使用。
源碼:
-
https://github.com/ZhaoRd/abp_cqrs
-
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_TelephoneBook
和 update-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