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

Posted ABP爱好者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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/abpcqrsexample


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

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

引入 Abp.Cqrs 类库

ABP CQRS 实现案例:基于 MediatR 实现core项目中安装cqrs包,并且添加模块依赖ABP CQRS 实现案例:基于 MediatR 实现

在命令或事件处理类的项目中,注册cqrs处理ABP CQRS 实现案例:基于 MediatR 实现

添加电话簿实体类

 
   
   
 
  1. /// <summary>

  2.    /// </summary>

  3.    public class TelephoneBook : FullAuditedAggregateRoot<Guid>

  4.    {

  5.        /// <summary>

  6.        /// 初始化<see cref="TelephoneBook"/>实例

  7.        /// </summary>

  8.        public TelephoneBook()

  9.        {

  10.        }

  11.        /// <summary>

  12.        /// 初始化<see cref="TelephoneBook"/>实例

  13.        /// </summary>

  14.        public TelephoneBook([NotNull]string name, string emailAddress, string tel)

  15.        {

  16.            this.Name = name;

  17.            this.Tel = tel;

  18.        }

  19.        /// <summary>

  20.        /// 姓名

  21.        /// </summary>

  22.        public string Name { get; protected set; }

  23.        /// <summary>

  24.        /// 邮箱

  25.        /// </summary>

  26.        /// <summary>

  27.        /// </summary>

  28.        public string Tel { get; protected set; }

  29.        /// <summary>

  30.        /// 修改联系方式

  31.        /// </summary>

  32.        /// <param name="emailAddress"></param>

  33.        /// <param name="tel"></param>

  34.        public void Change(string emailAddress,string tel)

  35.        {

  36.            this.Tel = tel;

  37.        }

  38.        /// <summary>

  39.        /// 修改姓名

  40.        /// </summary>

  41.        /// <param name="name"></param>

  42.        public void ChangeName(string name)

  43.        {

  44.            this.Name = name;

  45.        }

  46.    }

更新ef脚本

AddressBookDbContext中添加一下代码

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

执行脚本 add-migrationAdd_TelephoneBookupdate-database

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

 
   
   
 
  1.    /// <summary>

  2.    /// </summary>

  3.    public class CreateTelephoneBookCommand:Command

  4.    {

  5.        public TelephoneBookDto TelephoneBook { get;private set; }

  6.        public CreateTelephoneBookCommand(TelephoneBookDto book)

  7.        {

  8.            this.TelephoneBook = book;

  9.        }

  10.    }

  11.    /// <summary>

  12.    /// </summary>

  13.    public class UpdateTelephoneBookCommand : Command

  14.    {

  15.        public TelephoneBookDto TelephoneBook { get; private set; }

  16.        public UpdateTelephoneBookCommand(TelephoneBookDto book)

  17.        {

  18.            this.TelephoneBook = book;

  19.        }

  20.    }

  21.    /// <summary>

  22.    /// </summary>

  23.    public class DeleteTelephoneBookCommand : Command

  24.    {

  25.        public EntityDto<Guid> TelephoneBookId { get; private set; }

  26.        public DeleteTelephoneBookCommand(EntityDto<Guid> id)

  27.        {

  28.            this.TelephoneBookId = id;

  29.        }

  30.    }

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

命令处理类

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

 
   
   
 
  1.    /// <summary>

  2.    /// </summary>

  3.    public class UpdateTelephoneBookCommandHandler : ICommandHandler<UpdateTelephoneBookCommand>

  4.    {

  5.        private readonly IRepository<TelephoneBook, Guid> _telephoneBookRepository;

  6.        public UpdateTelephoneBookCommandHandler(IRepository<TelephoneBook, Guid> telephoneBookRepository)

  7.        {

  8.            this._telephoneBookRepository = telephoneBookRepository;

  9.        }

  10.        public async Task<Unit> Handle(UpdateTelephoneBookCommand request, CancellationToken cancellationToken)

  11.        {

  12.            var tenphoneBook = await this._telephoneBookRepository.GetAsync(request.TelephoneBook.Id.Value);

  13.            return Unit.Value;

  14.        }

  15.    }

  16.    /// <summary>

  17.    /// </summary>

  18.    public class DeleteTelephoneBookCommandHandler : ICommandHandler<DeleteTelephoneBookCommand>

  19.    {

  20.        private readonly IRepository<TelephoneBook, Guid> _telephoneBookRepository;

  21.        public DeleteTelephoneBookCommandHandler(

  22.            IRepository<TelephoneBook, Guid> telephoneBookRepository)

  23.        {

  24.            this._telephoneBookRepository = telephoneBookRepository;

  25.        }

  26.        public async Task<Unit> Handle(DeleteTelephoneBookCommand request, CancellationToken cancellationToken)

  27.        {

  28.            await this._telephoneBookRepository.DeleteAsync(request.TelephoneBookId.Id);

  29.            return Unit.Value;

  30.        }

  31.    }

  32.    /// <summary>

  33.    /// </summary>

  34.    public class CreateTelephoneBookCommandHandler : ICommandHandler<CreateTelephoneBookCommand>

  35.    {

  36.        private readonly IRepository<TelephoneBook, Guid> _telephoneBookRepository;

  37.        public CreateTelephoneBookCommandHandler(IRepository<TelephoneBook, Guid> telephoneBookRepository)

  38.        {

  39.            this._telephoneBookRepository = telephoneBookRepository;

  40.        }

  41.        public async Task<Unit> Handle(CreateTelephoneBookCommand request, CancellationToken cancellationToken)

  42.        {

  43.            await this._telephoneBookRepository.InsertAsync(telephoneBook);

  44.            return Unit.Value;

  45.        }

  46.    }

DTO类定义

DTO负责和前端交互数据

 
   
   
 
  1.    [AutoMap(typeof(TelephoneBook))]

  2.    public class TelephoneBookDto : EntityDto<Guid?>

  3.    {

  4.        /// <summary>

  5.        /// 姓名

  6.        /// </summary>

  7.        public string Name { get;  set; }

  8.        /// <summary>

  9.        /// 邮箱

  10.        /// </summary>

  11.        /// <summary>

  12.        /// </summary>

  13.        public string Tel { get;  set; }

  14.    }

  15.    [AutoMap(typeof(TelephoneBook))]

  16.    public class TelephoneBookListDto : FullAuditedEntityDto<Guid>

  17.    {

  18.        /// <summary>

  19.        /// 姓名

  20.        /// </summary>

  21.        public string Name { get; set; }

  22.        /// <summary>

  23.        /// 邮箱

  24.        /// </summary>

  25.        /// <summary>

  26.        /// </summary>

  27.        public string Tel { get; set; }

  28.    }

实现应用层

应用层需要依赖两个内容

  • 命令总线负责发送命令

  • 仓储负责查询功能

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

 
   
   
 
  1.    public class TelephoneBookAppServiceTests : AddressBookTestBase

  2.    {

  3.        private readonly ITelephoneBookAppService _service;

  4.        public TelephoneBookAppServiceTests()

  5.        {

  6.            _service = Resolve<ITelephoneBookAppService>();

  7.        }

  8.        /// <summary>

  9.        /// 获取所有看通讯录

  10.        /// </summary>

  11.        /// <returns></returns>

  12.        [Fact]

  13.        public async Task GetAllTelephoneBookList_Test()

  14.        {

  15.            // Act

  16.            var output = await _service.GetAllTelephoneBookList();

  17.            // Assert

  18.            output.Count().ShouldBe(0);

  19.        }

  20.        /// <summary>

  21.        /// 创建通讯录

  22.        /// </summary>

  23.        /// <returns></returns>

  24.        [Fact]

  25.        public async Task CreateTelephoneBook_Test()

  26.        {

  27.            // Act

  28.            await _service.CreateOrUpdate(

  29.                new TelephoneBookDto()

  30.                    {

  31.                        Name = "赵云",

  32.                        Tel="12345678901"

  33.                    });

  34.            await UsingDbContextAsync(async context =>

  35.                {

  36.                    var zhaoyun = await context

  37.                                           .TelephoneBooks

  38.                                           .FirstOrDefaultAsync(u => u.Name == "赵云");

  39.                    zhaoyun.ShouldNotBeNull();

  40.                });

  41.        }

  42.        /// <summary>

  43.        /// 更新通讯录

  44.        /// </summary>

  45.        /// <returns></returns>

  46.        [Fact]

  47.        public async Task UpdateTelephoneBook_Test()

  48.        {

  49.            // Act

  50.            await _service.CreateOrUpdate(

  51.                new TelephoneBookDto()

  52.                    {

  53.                        Name = "赵云",

  54.                        Tel = "12345678901"

  55.                    });

  56.            var zhaoyunToUpdate = await UsingDbContextAsync(async context =>

  57.                {

  58.                    var zhaoyun = await context

  59.                                           .TelephoneBooks

  60.                                           .FirstOrDefaultAsync(u => u.Name == "赵云");

  61.                    return zhaoyun;

  62.                });

  63.            zhaoyunToUpdate.ShouldNotBeNull();

  64.            await _service.CreateOrUpdate(

  65.                new TelephoneBookDto()

  66.                    {

  67.                        Id = zhaoyunToUpdate.Id,

  68.                        Name = "赵云",

  69.                        Tel = "12345678901"

  70.                    });

  71.            await UsingDbContextAsync(async context =>

  72.                {

  73.                    var zhaoyun = await context

  74.                                      .TelephoneBooks

  75.                                      .FirstOrDefaultAsync(u => u.Name == "赵云");

  76.                    zhaoyun.ShouldNotBeNull();

  77.                });

  78.        }

  79.        /// <summary>

  80.        /// 删除通讯录

  81.        /// </summary>

  82.        /// <returns></returns>

  83.        [Fact]

  84.        public async Task DeleteTelephoneBook_Test()

  85.        {

  86.            // Act

  87.            await _service.CreateOrUpdate(

  88.                new TelephoneBookDto()

  89.                    {

  90.                        Name = "赵云",

  91.                        Tel = "12345678901"

  92.                    });

  93.            var zhaoyunToDelete = await UsingDbContextAsync(async context =>

  94.                {

  95.                    var zhaoyun = await context

  96.                                      .TelephoneBooks

  97.                                      .FirstOrDefaultAsync(u => u.Name == "赵云");

  98.                    return zhaoyun;

  99.                });

  100.            zhaoyunToDelete.ShouldNotBeNull();

  101.            await _service.Delete(

  102.                new EntityDto<Guid>()

  103.                    {

  104.                        Id = zhaoyunToDelete.Id

  105.                    });

  106.            await UsingDbContextAsync(async context =>

  107.                {

  108.                    var zhaoyun = await context

  109.                                      .TelephoneBooks

  110.                                      .Where(c=>c.IsDeleted == false)

  111.                                      .FirstOrDefaultAsync(u => u.Name == "赵云");

  112.                    zhaoyun.ShouldBeNull();

  113.                });

  114.        }

  115.    }

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


项目案例 -- 电话簿 (前端 ng-alain 项目)

使用ng-alain实现前端项目

界面预览

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

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

列表界面代码

 
   
   
 
  1. import { Component, OnInit, Injector } from '@angular/core';

  2. import { _HttpClient, ModalHelper } from '@delon/theme';

  3. import { SimpleTableColumn, SimpleTableComponent } from '@delon/abc';

  4. import { SFSchema } from '@delon/form';

  5. import { finalize } from 'rxjs/operators';

  6. import { AppComponentBase } from '@shared/app-component-base';

  7. import { TelephoneBookServiceProxy, TelephoneBookDto, TelephoneBookListDto } from '@shared/service-proxies/service-proxies';

  8. import { BooksCreateComponent } from './../create/create.component'

  9. import { BooksEditComponent } from './../edit/edit.component'

  10. @Component({

  11.  selector: 'books-list',

  12.  templateUrl: './list.component.html',

  13. })

  14. export class BooksListComponent extends AppComponentBase  implements OnInit {

  15.    params: any = {};

  16.    list = [];

  17.    loading = false;

  18.    constructor( injector: Injector,private http: _HttpClient, private modal: ModalHelper,

  19.      private _telephoneBookService:TelephoneBookServiceProxy) {

  20.        super(injector);

  21.      }

  22.    ngOnInit() {

  23.      this.loading = true;

  24.      this._telephoneBookService

  25.          .getAllTelephoneBookList()

  26.          .pipe(finalize(

  27.            ()=>{

  28.              this.loading = false;

  29.            }

  30.          ))

  31.          .subscribe(res=>{

  32.            this.list = res;

  33.          })

  34.          ;

  35.    }

  36.    edit(id: string): void {

  37.      this.modal.static(BooksEditComponent, {

  38.        bookId: id

  39.      }).subscribe(res => {

  40.        this.ngOnInit();

  41.      });

  42.    }

  43.    add() {

  44.       this.modal

  45.         .static(BooksCreateComponent, { id: null })

  46.         .subscribe(() => this.ngOnInit());

  47.    }

  48.    delete(book: TelephoneBookListDto): void {

  49.      abp.message.confirm(

  50.        "删除通讯录 '" + book.name + "'?"

  51.      ).then((result: boolean) => {

  52.        console.log(result);

  53.        if (result) {

  54.          this._telephoneBookService.delete(book.id)

  55.            .pipe(finalize(() => {

  56.              abp.notify.info("删除通讯录: " + book.name);

  57.              this.ngOnInit();

  58.            }))

  59.            .subscribe(() => { });

  60.        }

  61.      });

  62.    }

  63. }

 
   
   
 
  1. <page-header></page-header>

  2. <nz-card>

  3.  <!--

  4.  <sf mode="search" [schema]="searchSchema" [formData]="params" (formSubmit)="st.reset($event)" (formReset)="st.reset(params)"></sf>

  5.  -->

  6.  <div class="my-sm">

  7.    <button (click)="add()" nz-button nzType="primary">新建</button>

  8.  </div>

  9. <nz-table #tenantListTable

  10. [nzData]="list"

  11. [nzLoading]="loading"

  12. >

  13. <thead nz-thead>

  14.    <tr>

  15.        <th nz-th>

  16.            <span>序号</span>

  17.        </th>

  18.        <th nz-th>

  19.            <span>姓名</span>

  20.        </th>

  21.        <th nz-th>

  22.            <span>邮箱</span>

  23.        </th>

  24.        <th nz-th>

  25.      </th>

  26.        <th nz-th>

  27.            <span>{{l('Actions')}}</span>

  28.        </th>

  29.    </tr>

  30. </thead>

  31. <tbody nz-tbody>

  32.    <tr nz-tbody-tr *ngFor="let data of tenantListTable.data;let i=index;">

  33.        <td nz-td>

  34.            <span>

  35.              {{(i+1)}}

  36.            </span>

  37.        </td>

  38.        <td nz-td>

  39.            <span>

  40.                {{data.name}}

  41.            </span>

  42.        </td>

  43.        <td nz-td>

  44.            <span>

  45.                {{data.emailAddress}}

  46.            </span>

  47.        </td>

  48.        <td nz-td>

  49.          <span>

  50.              {{data.tel}}

  51.          </span>

  52.      </td>

  53.        <td nz-td>

  54.          <nz-dropdown>

  55.              <a class="ant-dropdown-link" nz-dropdown>

  56.                  <i class="anticon anticon-setting"></i>

  57.                  操作

  58.                  <i class="anticon anticon-down"></i>

  59.              </a>

  60.              <ul nz-menu>

  61.                  <li nz-menu-item (click)="edit(data.id)">修改</li>

  62.                  <li nz-menu-item (click)="delete(data)">

  63.                      删除

  64.                  </li>

  65.              </ul>

  66.          </nz-dropdown>

  67.      </td>

  68.    </tr>

  69. </tbody>

  70. </nz-table>

  71.  <!--

  72.  <simple-table #st [data]="url" [columns]="columns" [extraParams]="params"></simple-table>

  73.  -->

  74. </nz-card>

新增界面代码

 
   
   
 
  1. <div class="modal-header">

  2.    <div class="modal-title">创建通讯录</div>

  3.  </div>

  4.  <nz-tabset>

  5.    <nz-tab nzTitle="基本信息">

  6.      <div nz-row>

  7.        <div nz-col class="mt-sm">

  8.          姓名

  9.        </div>

  10.        <div ng-col class="mt-sm">

  11.          <input nz-input [(ngModel)]="book.name" />

  12.        </div>

  13.      </div>

  14.      <div nz-row>

  15.        <div nz-col class="mt-sm">

  16.          邮箱

  17.        </div>

  18.        <div ng-col class="mt-sm">

  19.          <input nz-input [(ngModel)]="book.emailAddress" />

  20.        </div>

  21.      </div>

  22.      <div nz-row>

  23.          <div nz-col class="mt-sm">

  24.          </div>

  25.          <div ng-col class="mt-sm">

  26.            <input nz-input [(ngModel)]="book.tel" />

  27.          </div>

  28.        </div>

  29.    </nz-tab>

  30.  </nz-tabset>

  31.  <div class="modal-footer">

  32.    <button nz-button [nzType]="'default'" [nzSize]="'large'" (click)="close()">

  33.      取消

  34.    </button>

  35.    <button nz-button [nzType]="'primary'" [nzSize]="'large'" (click)="save()">

  36.      保存

  37.    </button>

  38.  </div>

 
   
   
 
  1. import { Component, OnInit,Injector } from '@angular/core';

  2.  import { NzModalRef, NzMessageService } from 'ng-zorro-antd';

  3.  import { _HttpClient } from '@delon/theme';

  4.  import { TelephoneBookServiceProxy, TelephoneBookDto, TelephoneBookListDto } from '@shared/service-proxies/service-proxies';

  5. import { AppComponentBase } from '@shared/app-component-base';

  6. import * as _ from 'lodash';

  7. import { finalize } from 'rxjs/operators';

  8.  @Component({

  9.    selector: 'books-create',

  10.    templateUrl: './create.component.html',

  11.  })

  12.  export class BooksCreateComponent extends AppComponentBase implements OnInit {

  13.    book: TelephoneBookDto = null;

  14.    saving: boolean = false;

  15.    constructor(injector: Injector,

  16.      private _telephoneBookService:TelephoneBookServiceProxy,

  17.      private modal: NzModalRef,

  18.      public msgSrv: NzMessageService,

  19.      private subject: NzModalRef,

  20.      public http: _HttpClient

  21.    ) {

  22.      super(injector);

  23.    }

  24.    ngOnInit(): void {

  25.      this.book = new TelephoneBookDto();

  26.      // this.http.get(`/user/${this.record.id}`).subscribe(res => this.i = res);

  27.    }

  28.    save(): void {

  29.      this.saving = true;

  30.      this._telephoneBookService.createOrUpdate(this.book)

  31.        .pipe(finalize(() => {

  32.          this.saving = false;

  33.        }))

  34.        .subscribe((res) => {

  35.          this.notify.info(this.l('SavedSuccessfully'));

  36.          this.close();

  37.        });

  38.    }

  39.    close() {

  40.      this.subject.destroy();

  41.    }

  42.  }

编辑页面代码

 
   
   
 
  1. <div class="modal-header">

  2.    <div class="modal-title">编辑通讯录</div>

  3.  </div>

  4.  <nz-tabset>

  5.    <nz-tab nzTitle="基本信息">

  6.      <div nz-row>

  7.        <div nz-col class="mt-sm">

  8.          姓名:{{book.name}}

  9.        </div>

  10.      </div>

  11.      <div nz-row>

  12.        <div nz-col class="mt-sm">

  13.          邮箱

  14.        </div>

  15.        <div ng-col class="mt-sm">

  16.          <input nz-input [(ngModel)]="book.emailAddress" />

  17.        </div>

  18.      </div>

  19.      <div nz-row>

  20.          <div nz-col class="mt-sm">

  21.          </div>

  22.          <div ng-col class="mt-sm">

  23.            <input nz-input [(ngModel)]="book.tel" />

  24.          </div>

  25.        </div>

  26.    </nz-tab>

  27.  </nz-tabset>

  28.  <div class="modal-footer">

  29.    <button nz-button [nzType]="'default'" [nzSize]="'large'" (click)="close()">

  30.      取消

  31.    </button>

  32.    <button nz-button [nzType]="'primary'" [nzSize]="'large'" (click)="save()">

  33.      保存

  34.    </button>

  35.  </div>

 
   
   
 
  1. import { Component, OnInit,Injector,Input } from '@angular/core';

  2.  import { NzModalRef, NzMessageService } from 'ng-zorro-antd';

  3.  import { _HttpClient } from '@delon/theme';

  4.  import { TelephoneBookServiceProxy, TelephoneBookDto, TelephoneBookListDto } from '@shared/service-proxies/service-proxies';

  5. import { AppComponentBase } from '@shared/app-component-base';

  6. import * as _ from 'lodash';

  7. import { finalize } from 'rxjs/operators';

  8.  @Component({

  9.    selector: 'books-edit',

  10.    templateUrl: './edit.component.html',

  11.  })

  12.  export class BooksEditComponent extends AppComponentBase  implements OnInit {

  13.    book: TelephoneBookDto = null;

  14.    @Input()

  15.    bookId:string = null;

  16.    saving: boolean = false;

  17.    constructor(injector: Injector,

  18.      private _telephoneBookService:TelephoneBookServiceProxy,

  19.      private modal: NzModalRef,

  20.      public msgSrv: NzMessageService,

  21.      private subject: NzModalRef,

  22.      public http: _HttpClient

  23.    ) {

  24.      super(injector);

  25.      this.book = new TelephoneBookDto();

  26.    }

  27.    ngOnInit(): void {

  28.     // this.book = new TelephoneBookDto();

  29.     this._telephoneBookService.getForEdit(this.bookId)

  30.     .subscribe(

  31.     (result) => {

  32.         this.book = result;

  33.     });

  34.      // this.http.get(`/user/${this.record.id}`).subscribe(res => this.i = res);

  35.    }

  36.    save(): void {

  37.      this.saving = true;

  38.      this._telephoneBookService.createOrUpdate(this.book)

  39.        .pipe(finalize(() => {

  40.          this.saving = false;

  41.        }))

  42.        .subscribe((res) => {

  43.          this.notify.info(this.l('SavedSuccessfully'));

  44.          this.close();

  45.        });

  46.    }

  47.    close() {

  48.      this.subject.destroy();

  49.    }

  50.  }


参考资料

  • 浅谈命令查询职责分离(CQRS)模式

  • 团队开发框架实战—CQRS架构

  • DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器)

  • DDD CQRS架构和传统架构的优缺点比较

  • CQRS Journey


我的公众号


以上是关于ABP CQRS 实现案例:基于 MediatR 实现的主要内容,如果未能解决你的问题,请参考以下文章

MediatR 和 CQRS 测试。如何验证调用了该处理程序?

MediatR CQRS模式解决将消息发送与消息处理进行了解耦,他同时支持异步和同步来发送和监听消息.

MediatR CQRS模式解决将消息发送与消息处理进行了解耦,他同时支持异步和同步来发送和监听消息.

MediatR CQRS - 如何处理不存在的资源(asp.net core web api)

使用 MediatR 链接处理程序

基于CQRS的架构在答题PK小游戏中的实践案例