美文网首页ABPABP.NET
ABP CQRS 实现案例:基于 MediatR 实现

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

作者: 诸葛_小亮 | 来源:发表于2018-08-14 07:06 被阅读44次

介绍

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,这意味着该实体具有不可变性,如果要修改实体属性(修改对象状态),只能通过实体提供的业务方法进行修改。

/// <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 name="emailAddress"></param>
        /// <param name="tel"></param>
        public void Change(string emailAddress,string tel)
        {
            this.EmailAddress = emailAddress;
            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-migration Add_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实现前端项目

界面预览

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

}

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


参考资料


我的公众号

我的公众号

相关文章

网友评论

  • aabc4e4a8159:没理解到 return Unit.Value
    诸葛_小亮:ICommandHandler 要求返回Unit,这个是 media的要求,所以需要返回 Unit.Value

本文标题:ABP CQRS 实现案例:基于 MediatR 实现

本文链接:https://www.haomeiwen.com/subject/ldvubftx.html