使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求
需求
需求很简单:如何创建新的TodoList和TodoItem并持久化。
初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法;实用传入的请求参数new一个数据库实体对象;调用IRepository<T>完成数据库的写入,最多会在中间加一层Service。这个做法本身没有问题,也是需要从初学阶段开始扎实地掌握开发技能的必经之路,有助于帮助理解逻辑调用的过程。
对于稍微正式一些的项目,.NET工程上习惯的实现是通过使用一些比较成熟的类库框架,有效地对业务逻辑进行分类管理、消除冗余代码,以达到业务逻辑职责清晰简洁的目的。在这个阶段我们经常使用的两个类库分别是AutoMapper和MediatR,本文结合POST请求,先介绍关于MediatR部分,下一篇关于GET请求,会涉及AutoMapper的部分。
目标
合理组织并使用MediatR,完成POST请求。
原理与思路
首先来简单地介绍一下这个类库。
关于CQRS模式、中介者模式和MediatR
CQRS模式
CQRS模式全称是“Command Query Responsibility Segregation”,正如字面意思,CQRS模式的目的在于将读取操作和写入操作的指责区分开,并使用不同的Model去表示。从CRUD的角度来说,就是把R和CUD区分开来对待。如下图所示:

这个模式可以有效地应用到具有主从分离的数据库架构中,当需要获取数据时,从只读数据库(一般是从库)中读取数据,当需要写入或更新数据时,向主库进行操作。
CQRS模式旨在解决的问题是:为了屏蔽数据库层面“写优先”还是“读优先”的优化设计策略,在业务逻辑侧进行解耦。
任何设计模式都是对解决特定问题的一个Trade off,自然也带来了一些缺点,首先就是服务内部的组件复杂度上升了,因为需要创建额外的类来实现CQRS模式;其次如果数据层是分离的,那么可能会有数据的状态不一致问题。
中介者Mediator模式
这是23种基本设计模式中的一个,属于行为型设计模式,它给出了组件之间交互的一种解耦的方式。简单参考下图,具体内容就不过多解释了,任何一篇介绍设计模式的文章都有介绍。

这种设计模式实际上是一种采用依赖倒置(Inversion of Control, IoC)的方式,实现了图中蓝色组件的松耦合。
MediatR
这是在开发中被广泛采用的实现以上两种设计模式的类库,更准确的说法是,它通过应用中介者模式,实现了进程内CQRS。基本思想是所有来自API接口和数据存储之间的逻辑,都需要通过MediatR来组织(即所谓的“中介者”)。
从实现上看,MediatR提供了几组用于不同场景的接口,我们在本文中处理的比较多的是IRequest<T>/IRequestHandler<T>以及INotification<T>/INotificationHander<T>两组接口,更多的请参考官方文档和例子。
实现
所有需要使用MediatR的地方都集中在Application项目中。
引入MediatR
$ dotnet add src/TodoList.Application/TodoList.Application.csproj package MediatR.Extensions.Microsoft.DependencyInjection
为了适配CQRS的模式,我们在Application项目中的TodoLists和TodoItems下相同地创建几个文件夹:
Commands:用于组织CUD相关的业务逻辑;Queries:用于组织R相关的业务逻辑;EventHandlers:用于组织领域事件处理的相关业务逻辑。
在Application根目录下同样创建DependencyInjection.cs用于该项目的依赖注入:
DependencyInjection.cs
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace TodoList.Application;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddMediatR(Assembly.GetExecutingAssembly());
return services;
}
}
并在Api项目中使用:
// 省略其他...
// 添加应用层配置
builder.Services.AddApplication();
// 添加基础设施配置
builder.Services.AddInfrastructure(builder.Configuration);
实现Post请求
在本章中我们只实现TodoList和TodoItem的Create接口(POST),剩下的接口后面的文章中逐步涉及。
POST TodoList
在Application/TodoLists/Commands/下新建一个目录CreateTodoList用于存放创建一个TodoList相关的所有逻辑:
CreateTodoListCommand.cs
using MediatR;
using TodoList.Application.Common.Interfaces;
namespace TodoList.Application.TodoLists.Commands.CreateTodoList;
public class CreateTodoListCommand : IRequest<Guid>
{
public string? Title { get; set; }
}
public class CreateTodoListCommandHandler : IRequestHandler<CreateTodoListCommand, Guid>
{
private readonly IRepository<Domain.Entities.TodoList> _repository;
public CreateTodoListCommandHandler(IRepository<Domain.Entities.TodoList> repository)
{
_repository = repository;
}
public async Task<Guid> Handle(CreateTodoListCommand request, CancellationToken cancellationToken)
{
var entity = new Domain.Entities.TodoList
{
Title = request.Title
};
await _repository.AddAsync(entity, cancellationToken);
return entity.Id;
}
}
有一些实践是将Request和RequestHandler分开两个文件,我更倾向于像这样将他俩放在一起,一是保持简洁,二是当你需要顺着一个Command去寻找它对应的Handler时,不需要更多的跳转。
接下来在TodoListController里实现对应的POST方法,
using MediatR;
using Microsoft.AspNetCore.Mvc;
using TodoList.Application.TodoLists.Commands.CreateTodoList;
namespace TodoList.Api.Controllers;
[ApiController]
[Route("/todo-list")]
public class TodoListController : ControllerBase
{
private readonly IMediator _mediator;
// 注入MediatR
public TodoListController(IMediator mediator)
=> _mediator = mediator;
[HttpPost]
public async Task<Guid> Create([FromBody] CreateTodoListCommand command)
{
var createdTodoList = await _mediator.Send(command);
// 出于演示的目的,这里只返回创建出来的TodoList的Id,
// 实际使用中可能会选择IActionResult作为返回的类型并返回CreatedAtRoute对象,
// 因为我们还没有去写GET方法,返回CreatedAtRoute会报错(找不到对应的Route),等讲完GET后会在那里更新
return createdTodoList.Id;
}
}
POST TodoItem
类似TodoListController和CreateTodoListCommand的实现,这里我直接把代码贴出来了。
CreateTodoItemCommand.cs
using MediatR;
using TodoList.Application.Common.Interfaces;
using TodoList.Domain.Entities;
using TodoList.Domain.Events;
namespace TodoList.Application.TodoItems.Commands.CreateTodoItem;
public class CreateTodoItemCommand : IRequest<Guid>
{
public Guid ListId { get; set; }
public string? Title { get; set; }
}
public class CreateTodoItemCommandHandler : IRequestHandler<CreateTodoItemCommand, Guid>
{
private readonly IRepository<TodoItem> _repository;
public CreateTodoItemCommandHandler(IRepository<TodoItem> repository)
{
_repository = repository;
}
public async Task<Guid> Handle(CreateTodoItemCommand request, CancellationToken cancellationToken)
{
var entity = new TodoItem
{
// 这个ListId在前文中的代码里漏掉了,需要添加到Domain.Entities.TodoItem实体上
ListId = request.ListId,
Title = request.Title,
Done = false
};
await _repository.AddAsync(entity, cancellationToken);
return entity.Id;
}
}
TodoItemController.cs
using MediatR;
using Microsoft.AspNetCore.Mvc;
using TodoList.Application.TodoItems.Commands.CreateTodoItem;
namespace TodoList.Api.Controllers;
[ApiController]
[Route("/todo-item")]
public class TodoItemController : ControllerBase
{
private readonly IMediator _mediator;
// 注入MediatR
public TodoItemController(IMediator mediator)
=> _mediator = mediator;
[HttpPost]
public async Task<Guid> Create([FromBody] CreateTodoItemCommand command)
{
var createdTodoItem = await _mediator.Send(command);
// 处于演示的目的,这里只返回创建出来的TodoItem的Id,理由同前
return createdTodoItem.Id;
}
}
验证
运行Api项目,通过Hoppscotch发送对应接口请求:
创建TodoList验证
请求

返回

数据库

第一条数据是种子数据,第二条是我们刚才创建的。
创建TodoItem验证
继续拿刚才创建的这个TodoList的Id来创建新的TodoItem:
请求

返回

数据库

最后一条是我们新创建的,其余是种子数据。
总结
我们已经通过演示在POST请求中实现MediatR库带来的CQRS模式,在这篇文章里我留了一个坑。就是领域事件的Handler并没有任何演示,只是创建了一个文件夹,结合在这篇文章中留下来的发布领域事件的坑,会在DELETE的文章中填完。
看起来使用CQRS模式使得我们的代码结构变得更加复杂了,但是对于一些再复杂一些的实际项目中,正确使用CQRS模式有助于你分析和整理业务需求,并将相关的业务需求以及相关模型梳理到统一的位置进行管理,包括在后续的文章里我们会陆续向其中加入诸如入参校验、出参类型转换等逻辑。认真思考并运用习惯之后,大家可以自行体会这样做的“权衡”。
参考资料
系列导航
- 使用.NET 6开发TodoList应用(1)——系列背景
- 使用.NET 6开发TodoList应用(2)——项目结构搭建
- 使用.NET 6开发TodoList应用(3)——引入第三方日志
- 使用.NET 6开发TodoList应用(4)——引入数据存储
- 使用.NET 6开发TodoList应用(5)——领域实体创建
- 使用.NET 6开发TodoList应用(5.1)——实现Repository模式
- 使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求
- 使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求
- 使用.NET 6开发TodoList应用(8)——实现全局异常处理
- 使用.NET 6开发TodoList应用(9)——实现PUT请求
- 使用.NET 6开发TodoList应用(10)——实现PATCH请求
- 使用.NET 6开发TodoList应用(11)——HTTP请求幂等性的考虑
- 使用.NET 6开发TodoList应用(12)——实现接口请求验证
- 使用.NET 6开发TodoList应用(13)——实现ActionFilter
- 使用.NET 6开发TodoList应用(14)——实现查询分页
- 使用.NET 6开发TodoList应用(15)——实现查询过滤
- 使用.NET 6开发TodoList应用(16)——实现查询搜索
- 使用.NET 6开发TodoList应用(17)——实现查询排序
- 使用.NET 6开发TodoList应用(18)——实现数据塑形
- 使用.NET 6开发TodoList应用(19)——实现HATEAOS支持
- 使用.NET 6开发TodoList应用(20)——处理OPTION和HEAD请求
- 使用.NET 6开发TodoList应用(21)——实现Root Document
- 使用.NET 6开发TodoList应用(22)——实现API版本控制
- 使用.NET 6开发TodoList应用(23)——实现缓存
- 使用.NET 6开发TodoList应用(24)——实现请求限流和阈值控制
- 使用.NET 6开发TodoList应用(25)——实现基于JWT的Identity功能
- 使用.NET 6开发TodoList应用(26)——实现RefreshToken
- 使用.NET 6开发TodoList应用(27)——实现Configuration和Option的强类型绑定
- 使用.NET 6开发TodoList应用(28)——实现API的Swagger文档化
- 使用.NET 6开发TodoList应用(29)——实现应用程序健康检查
- 使用.NET 6开发TodoList应用(30)——实现本地化功能
- 使用.NET 6开发TodoList应用(31)——实现Docker打包和部署
- 使用.NET 6开发TodoList应用(32)——实现基于Github Actions和ACI的CI/CD
使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求的更多相关文章
- 使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求
系列导航 使用.NET 6开发TodoList应用文章索引 需求 需求很简单:实现GET请求获取业务数据.在这个阶段我们经常使用的类库是AutoMapper. 目标 合理组织并使用AutoMapper ...
- 使用.NET 6开发TodoList应用(19)——处理OPTION和HEAD请求
系列导航及源代码 使用.NET 6开发TodoList应用文章索引 需求 在HTTP请求中,我们还剩下两类不常使用的请求没有讲到,本文就来实现以下关于OPTIONS和HEAD请求.OPTIONS请求用 ...
- 使用.NET 6开发TodoList应用文章索引
系列导航 使用.NET 6开发TodoList应用(1)--系列背景 使用.NET 6开发TodoList应用(2)--项目结构搭建 使用.NET 6开发TodoList应用(3)--引入第三方日志 ...
- 使用.NET 6开发TodoList应用(3)——引入第三方日志库
需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选 ...
- 使用.NET 6开发TodoList应用(1)——系列背景
前言 想到要写这样一个系列博客,初衷有两个:一是希望通过一个实践项目,将.NET 6 WebAPI开发的基础知识串联起来,帮助那些想要入门.NET 6服务端开发的朋友们快速上手,对使用.NET 6开发 ...
- 使用.NET 6开发TodoList应用(2)——项目结构搭建
为了不影响阅读的体验,我把系列导航放到文章最后了,有需要的小伙伴可以直接通过导航跳转到对应的文章 : P TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能 ...
- 使用.NET 6开发TodoList应用(4)——引入数据存储
需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储.目前的需求很简单: 需要能持久化TodoList对象并对其 ...
- 使用.NET 6开发TodoList应用(5)——领域实体创建
需求 上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发. 首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作? 长文 ...
- 使用.NET 6开发TodoList应用(5.1)——实现Repository模式
需求 经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景.有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是 ...
随机推荐
- BehaviorTree.CPP.行为树XML格式(六)
The XML format XML模式的基础 在第一个教程中,介绍了这个简单的树. <root main_tree_to_execute = "MainTree" > ...
- Java培训机构如何选择才能避免被骗?
近年来,随着IT行业的快速崛起,各类互联网人才供不应求,而Java工程师作为目前最为火爆的岗位之一,更是以高薪+高新技术的标签受到了人们的广泛关注.许多年轻人也看到了这个行业的发展前景,决定报名培训机 ...
- P6072 『MdOI R1』Path
考虑我们有这样操作. 我们只要维护两点在子树内和两点在子树外的异或和即可. 前者可以类似于线段树合并的trie树合并. 后者有两种做法: 一种是把dfn序翻倍:然后子树补变成了一个区间最大异或问题,可 ...
- 【数据库】本地KEGG数据库如何拆分子库?
目录 KEGG本地库文件 按物种拆分KEGG数据库 1.获得物种分类信息 2.获得物种分类的序列信息并建库 3.获得物种分类的K-ko对应文件 根据相似性原理,序列相似,功能相似,所有功能注释无非是用 ...
- 【机器学习与R语言】4-决策树
目录 1.决策树原理 2.决策树应用示例 2.1)收集数据 2.2)探索和准备数据 2.3)训练模型 2.4)评估模型性能 2.5)提高模型性能 通过自适应增强算法(boosting) 将惩罚因子分配 ...
- R语言与医学统计图形-【18】ggplot2几何对象汇总
ggplot2绘图系统--几何对象汇总 前面介绍了常见的几种基本的几何对象,并且介绍了scale.stat等其他要素.后续将介绍position.themes.coord和faceting等函数. 这 ...
- zabbix 集成cloud alert
1. 了解 Cloud Alert 通过应用,接入监控系统/平台的告警,集中管理您的告警,统一分派通知,统一分析.这个平台最先了解和使用是在 2017 年下半年,之前的名称叫 oneits ...
- Java的那些小事
一,JDK和JRE有什么区别? JRE:Java Runtime Environment(java运行时环境).即java程序的运行时环境,包含了java虚拟机,java基础类库. JDK:Java ...
- 巩固javaweb第十一天
巩固内容: HTML <script> 元素 <script>标签用于加载脚本文件,如: JavaScript. <script> 元素在以后的章节中会详细描述. ...
- 日常Java 2021/10/13
Java枚举 values(), ordinal()和valueOf()方法位于java.lang.Enum类中: values()返回枚举类中所有的值 ordinal()方法可以找到每个枚举常量的索 ...