ABP CQRS 实现案例:基于 MediatR 实现

介绍

CQRS(命令查询职责分离模式)从业务上分离修改 (Command,增,删,改,会对系统状态进行修改)和查询(Query,查,不会对系统状态进行修改)的行为。从而使得逻辑更加清晰,便于对不同部分进行针对性的优化。

CQRS基本思想在于,任何一个对象的方法可以分为两大类

  • 命令(Command):不返回任何结果(void),但会改变对象的状态。
  • 查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。

本文主要介绍如何使用基于MediatR实现的Abp.Cqrs类库,以及如何从读写分离模式来思考问题.
本文旨在探索cqrs如果落地,目前仅支持单机模式,不支持分布式。
本文案例主要介绍了命令的使用方式,分离写的职责,对event没有过多的介绍和使用。

架构图

源码:

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

项目案例 -- 电话簿 (后端)

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

引入 Abp.Cqrs 类库

abp.cqrs

core项目中安装cqrs包,并且添加模块依赖
模块依赖

在命令或事件处理类的项目中,注册cqrs处理


注册处理类

添加电话簿实体类

电话簿实体类代码,代码封装了简单的业务修改联系方式修改姓名,封装这两个业务,主要是为了命令服务,注意实体类的属性可访问性是protected,这意味着该实体具有不可变性,如果要修改实体属性(修改对象状态),只能通过实体提供的业务方法进行修改。

/// 
    /// 电话簿
    /// 
    public class TelephoneBook : FullAuditedAggregateRoot
    {
        /// 
        /// 初始化实例
        /// 
        public TelephoneBook()
        {

        }

        /// 
        /// 初始化实例
        /// 
        public TelephoneBook([NotNull]string name, string emailAddress, string tel)
        {
            this.Name = name;
            this.EmailAddress = emailAddress;
            this.Tel = tel;
        }

        /// 
        /// 姓名
        /// 
        public string Name { get; protected set; }

        /// 
        /// 邮箱
        /// 
        public string EmailAddress { get; protected set; }

        /// 
        /// 电话号码
        /// 
        public string Tel { get; protected set; }

        /// 
        /// 修改联系方式
        /// 
        /// 
        /// 
        public void Change(string emailAddress,string tel)
        {
            this.EmailAddress = emailAddress;
            this.Tel = tel;
        }

        /// 
        /// 修改姓名
        /// 
        /// 
        public void ChangeName(string name)
        {
            this.Name = name;
        }
    }

更新ef脚本

AddressBookDbContext中添加一下代码

public DbSet TelephoneBooks { get; set; }

执行脚本add-migration Add_TelephoneBookupdate-database

定义 创建、更新、删除命令


    /// 
    /// 创建电话簿命令
    /// 
    public class CreateTelephoneBookCommand:Command
    {
        public TelephoneBookDto TelephoneBook { get;private set; }

        public CreateTelephoneBookCommand(TelephoneBookDto book)
        {
            this.TelephoneBook = book;
        }
    }

    /// 
    /// 更新电话命令
    /// 
    public class UpdateTelephoneBookCommand : Command
    {
        public TelephoneBookDto TelephoneBook { get; private set; }

        public UpdateTelephoneBookCommand(TelephoneBookDto book)
        {
            this.TelephoneBook = book;
        }
    }

    /// 
    /// 删除电话簿命令
    /// 
    public class DeleteTelephoneBookCommand : Command
    {
        public EntityDto TelephoneBookId { get; private set; }

        public DeleteTelephoneBookCommand(EntityDto id)
        {
            this.TelephoneBookId = id;
        }
    }
    

命令代码很简单,只要提供命令需要的数据即可

命令处理类

cqrs中,是通过命令修改实体属性的,所以命令处理类需要依赖相关仓储。
关注更新命令处理,可以看到不是直接修改实体属性,而是通过实体提供的业务方法修改实体属性。


    /// 
    /// 更新电话簿命令处理
    /// 
    public class UpdateTelephoneBookCommandHandler : ICommandHandler
    {
        
        private readonly IRepository _telephoneBookRepository;

        public UpdateTelephoneBookCommandHandler(IRepository telephoneBookRepository)
        {
            this._telephoneBookRepository = telephoneBookRepository;
        }

        public async Task 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;
        }
    }

    /// 
    /// 删除电话簿命令
    /// 
    public class DeleteTelephoneBookCommandHandler : ICommandHandler
    {
        private readonly IRepository _telephoneBookRepository;
        public DeleteTelephoneBookCommandHandler(
            IRepository telephoneBookRepository)
        {
            this._telephoneBookRepository = telephoneBookRepository;
        }

        public async Task Handle(DeleteTelephoneBookCommand request, CancellationToken cancellationToken)
        {
            await this._telephoneBookRepository.DeleteAsync(request.TelephoneBookId.Id);

            return Unit.Value;
        }
    }

    /// 
    /// 创建电话簿命令
    /// 
    public class CreateTelephoneBookCommandHandler : ICommandHandler
    {
        private readonly IRepository _telephoneBookRepository;
        
        public CreateTelephoneBookCommandHandler(IRepository telephoneBookRepository)
        {
            this._telephoneBookRepository = telephoneBookRepository;
        }

        public async Task 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
    {

        /// 
        /// 姓名
        /// 
        public string Name { get;  set; }

        /// 
        /// 邮箱
        /// 
        public string EmailAddress { get;  set; }

        /// 
        /// 电话号码
        /// 
        public string Tel { get;  set; }

    }

    [AutoMap(typeof(TelephoneBook))]
    public class TelephoneBookListDto : FullAuditedEntityDto
    {

        /// 
        /// 姓名
        /// 
        public string Name { get; set; }

        /// 
        /// 邮箱
        /// 
        public string EmailAddress { get; set; }

        /// 
        /// 电话号码
        /// 
        public string Tel { get; set; }

    }

实现应用层

应用层需要依赖两个内容

  • 命令总线负责发送命令
  • 仓储负责查询功能

在应用层中不在直接修改实体属性
观察创建 编辑 删除 业务,可以看到这些业务都在做意见事情:发布命令.


    public class TelephoneBookAppServiceTests : AddressBookTestBase
    {
        private readonly ITelephoneBookAppService _service;

        public TelephoneBookAppServiceTests()
        {
            _service = Resolve();
        }

        /// 
        /// 获取所有看通讯录
        /// 
        /// 
        [Fact]
        public async Task GetAllTelephoneBookList_Test()
        {
            // Act
            var output = await _service.GetAllTelephoneBookList();

            // Assert
            output.Count().ShouldBe(0);
        }

        /// 
        /// 创建通讯录
        /// 
        /// 
        [Fact]
        public async Task CreateTelephoneBook_Test()
        {
            // Act
            await _service.CreateOrUpdate(
                new TelephoneBookDto()
                    {
                        EmailAddress = "[email protected]",
                        Name = "赵云",
                        Tel="12345678901"
                    });

            await UsingDbContextAsync(async context =>
                {
                    var zhaoyun = await context
                                           .TelephoneBooks
                                           .FirstOrDefaultAsync(u => u.Name == "赵云");
                    zhaoyun.ShouldNotBeNull();
                });
        }

        /// 
        /// 更新通讯录
        /// 
        /// 
        [Fact]
        public async Task UpdateTelephoneBook_Test()
        {
            // Act
            await _service.CreateOrUpdate(
                new TelephoneBookDto()
                    {
                        EmailAddress = "[email protected]",
                        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 = "[email protected]",
                        Name = "赵云",
                        Tel = "12345678901"
                    });

            await UsingDbContextAsync(async context =>
                {
                    var zhaoyun = await context
                                      .TelephoneBooks
                                      .FirstOrDefaultAsync(u => u.Name == "赵云");
                    zhaoyun.ShouldNotBeNull();
                    zhaoyun.EmailAddress.ShouldBe("[email protected]");
                });

        }

        /// 
        /// 删除通讯录
        /// 
        /// 
        [Fact]
        public async Task DeleteTelephoneBook_Test()
        {
            // Act
            await _service.CreateOrUpdate(
                new TelephoneBookDto()
                    {
                        EmailAddress = "[email protected]",
                        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()
                    {
                        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实现前端项目

界面预览

列表.png
新增
修改

列表界面代码

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(() => { });
        }
      });
    }

}



  
  
序号 姓名 邮箱 电话 {{l('Actions')}} {{(i+1)}} {{data.name}} {{data.emailAddress}} {{data.tel}} 操作
  • 修改
  • 删除

新增界面代码


  
  
  
    
  
      
姓名
邮箱
电话号码
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();
    }
  }

编辑页面代码


  
  
  
    
  
      
姓名:{{book.name}}
邮箱
电话号码
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 实现)