使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求


系列导航

需求

需求很简单:如何创建新的TodoListTodoItem并持久化。

初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法;实用传入的请求参数new一个数据库实体对象;调用IRepository<T>完成数据库的写入,最多会在中间加一层Service。这个做法本身没有问题,也是需要从初学阶段开始扎实地掌握开发技能的必经之路,有助于帮助理解逻辑调用的过程。

对于稍微正式一些的项目,.NET工程上习惯的实现是通过使用一些比较成熟的类库框架,有效地对业务逻辑进行分类管理、消除冗余代码,以达到业务逻辑职责清晰简洁的目的。在这个阶段我们经常使用的两个类库分别是AutoMapperMediatR,本文结合POST请求,先介绍关于MediatR部分,下一篇关于GET请求,会涉及AutoMapper的部分。

目标

合理组织并使用MediatR,完成POST请求。

原理与思路

首先来简单地介绍一下这个类库。

关于CQRS模式、中介者模式和MediatR

CQRS模式

CQRS模式全称是“Command Query Responsibility Segregation”,正如字面意思,CQRS模式的目的在于将读取操作和写入操作的指责区分开,并使用不同的Model去表示。从CRUD的角度来说,就是把RCUD区分开来对待。如下图所示:

image

这个模式可以有效地应用到具有主从分离的数据库架构中,当需要获取数据时,从只读数据库(一般是从库)中读取数据,当需要写入或更新数据时,向主库进行操作。

CQRS模式旨在解决的问题是:为了屏蔽数据库层面“写优先”还是“读优先”的优化设计策略,在业务逻辑侧进行解耦。

任何设计模式都是对解决特定问题的一个Trade off,自然也带来了一些缺点,首先就是服务内部的组件复杂度上升了,因为需要创建额外的类来实现CQRS模式;其次如果数据层是分离的,那么可能会有数据的状态不一致问题。

中介者Mediator模式

这是23种基本设计模式中的一个,属于行为型设计模式,它给出了组件之间交互的一种解耦的方式。简单参考下图,具体内容就不过多解释了,任何一篇介绍设计模式的文章都有介绍。

image

这种设计模式实际上是一种采用依赖倒置(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项目中的TodoListsTodoItems下相同地创建几个文件夹:

  • 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请求

在本章中我们只实现TodoListTodoItemCreate接口(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;
    }
}

有一些实践是将RequestRequestHandler分开两个文件,我更倾向于像这样将他俩放在一起,一是保持简洁,二是当你需要顺着一个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

类似TodoListControllerCreateTodoListCommand的实现,这里我直接把代码贴出来了。

  • 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验证

  • 请求
    image

  • 返回
    image

  • 数据库
    image
    第一条数据是种子数据,第二条是我们刚才创建的。

创建TodoItem验证

继续拿刚才创建的这个TodoList的Id来创建新的TodoItem

  • 请求
    image

  • 返回
    image

  • 数据库
    image
    最后一条是我们新创建的,其余是种子数据。

总结

我们已经通过演示在POST请求中实现MediatR库带来的CQRS模式,在这篇文章里我留了一个坑。就是领域事件的Handler并没有任何演示,只是创建了一个文件夹,结合在这篇文章中留下来的发布领域事件的坑,会在DELETE的文章中填完。

看起来使用CQRS模式使得我们的代码结构变得更加复杂了,但是对于一些再复杂一些的实际项目中,正确使用CQRS模式有助于你分析和整理业务需求,并将相关的业务需求以及相关模型梳理到统一的位置进行管理,包括在后续的文章里我们会陆续向其中加入诸如入参校验、出参类型转换等逻辑。认真思考并运用习惯之后,大家可以自行体会这样做的“权衡”。

参考资料

  1. MediatR
  2. Mediator
文章来源:https://www.cnblogs.com/code4nothing/p/build-todolist-6.html

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
管理员
上一篇:[WPF] 实现 WPF 的 Inner Shadow
下一篇:关于二元插值问题的探讨
评论列表

发表评论

评论内容
昵称:
关联文章

使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求
使用.NET 6开发TodoList应用(11)——使用FluentValidation和MediatR实现接口请求验证
使用.NET 6开发TodoList应用(9)——实现PUT请求
使用.NET 6开发TodoList应用(10)——实现DELETE请求以及HTTP请求幂等性
使用.NET 6开发TodoList应用(7)——使用AutoMapper实现GET请求
使用.NET 6开发TodoList应用(14)——实现查询过滤
使用.NET 6开发TodoList应用(16)——实现查询排序
使用.NET 6开发TodoList应用(15)——实现查询搜索
使用.NET 6开发TodoList应用(12)——实现ActionFilter
使用.NET 6开发TodoList应用(23)——实现请求限流
使用.NET 6开发TodoList应用(25)——实现RefreshToken
使用.NET 6开发TodoList应用(22)——实现缓存
使用.NET 6开发TodoList应用(19)——处理OPTION和HEAD请求
使用.NET 6开发TodoList应用(13)——实现查询分页
使用.NET 6开发TodoList应用(17)——实现数据塑形
使用.NET 6开发TodoList应用(28)——实现应用程序健康检查
使用.NET 6开发TodoList应用(30)——实现Docker打包和部署
使用.NET 6开发TodoList应用(31)——实现基于Github Actions和ACI的CI/CD
使用.NET 6开发TodoList应用(29)——实现静态字符串本地化功能
使用.NET 6开发TodoList应用(8)——实现全局异常处理

联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码