ABP CQRS 实现案例:基于 MediatR 实现
Posted ABP爱好者
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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处理
添加电话簿实体类
/// <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.Tel = tel;
}
/// <summary>
/// 姓名
/// </summary>
public string Name { get; protected set; }
/// <summary>
/// 邮箱
/// </summary>
/// <summary>
/// </summary>
public string Tel { get; protected set; }
/// <summary>
/// 修改联系方式
/// </summary>
/// <param name="emailAddress"></param>
/// <param name="tel"></param>
public void Change(string emailAddress,string tel)
{
this.Tel = tel;
}
/// <summary>
/// 修改姓名
/// </summary>
/// <param name="name"></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);
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)
{
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>
/// <summary>
/// </summary>
public string Tel { get; set; }
}
[AutoMap(typeof(TelephoneBook))]
public class TelephoneBookListDto : FullAuditedEntityDto<Guid>
{
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 邮箱
/// </summary>
/// <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()
{
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()
{
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,
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 DeleteTelephoneBook_Test()
{
// Act
await _service.CreateOrUpdate(
new TelephoneBookDto()
{
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>
</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
我的公众号
以上是关于ABP CQRS 实现案例:基于 MediatR 实现的主要内容,如果未能解决你的问题,请参考以下文章
MediatR 和 CQRS 测试。如何验证调用了该处理程序?
MediatR CQRS模式解决将消息发送与消息处理进行了解耦,他同时支持异步和同步来发送和监听消息.
MediatR CQRS模式解决将消息发送与消息处理进行了解耦,他同时支持异步和同步来发送和监听消息.