Asp.Net Core6.0中MediatR的应用CQRS


1、前言

  对于简单的系统而言模型与数据可以进行直接的映射,比如说三层模型就足够支撑项目的需求了。对于这种简单的系统我们过度设计说白了无异于增加成本,因为对于一般的CRUD来说我们不用特别区分查询和增删改的程序结构。高射炮打蚊子那就有点大材小用了。但是我们的系统具有一定复杂性的时候,可能源于访问频次、数据量或者数据模型这个时候我们的查询跟增删改的需求差距就逐渐变大。所以CQRS(Command Query Responsibility Segregation)命令查询的责任分离就出现了。CQRS本质上是一种读写分离设计思想,这种框架设计模式将命令型业务和查询型业务分开单独处理。我们运用MediatR就可以轻松的实现CQRS。

2、中介者模式

  中介者模式定义了一个中介对象来封装一系列对象之间的交互关系,中介者使各个对象之间不需要显式地相互引用,从而降低耦合性。也符合符合迪米特原则。MediatR本质就是中介者模式,实现命令的构造和命令的处理分开来。

3、MediatR简介

  MediatR是一个跨平台通过一种进程内消息传递机制,进行请求/响应、命令、查询、通知和事件的消息传递,并通过C#泛型来支持消息的智能调度,其目的是消息发送和消息处理的解耦。它支持以单播和多播形式使用同步或异步的模式来发布消息,创建和侦听事件。

4、主要的几个对象

a.IMediator:主要提供Send与Publish方法,需要执行的命令都是通过这两个方法实现

b.IRequest、IRequest命令查询 | 处理类所继承的接口,一个有返回类型,一个无返回类型,一个查询对应一个处理类,程序集只认第一个扫描到的类。

c.IRequestHandler(实现Handle方法) :命令处理接口。命令查询 | 处理类继承它,也可以继承AsyncRequestHandler(实现抽象Handle方法)、RequestHandler(实现抽象Handle方法)接口

d.INotification:命令查询 | 处理类所继承的接口这个没有返回,与IRequest不通的是可以对于多个处理类。

e.INotificationHandler:与IRequestHandler一样的只不过这是INotification的处理接口

5、IRequest栗子

a.IRequest:有返回值的类

  说了那么多干巴巴的直接上代码看。我这里是Core6.0控制台应用程序,安装nuget包 MediatR与扩展包MediatR.Extensions.Microsoft.DependencyInjection。也可以通过命令行添加dotnet add package MediatR dotnet add package MediatR.Extensions.Microsoft.DependencyInjection先看命令的查询处理

这里我习惯性的将两个类放在一个文件里面方便查看,命名这里做查询就写XXXQuery  处理类的命名也是XXXQueryHandler

using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Mrdiator.Query
{
    /// 
    /// 查询信息命令类
    /// 
    internal class GetInfoQuery:IRequest
    {
        /// 
        /// 构造函数--就是查询的条件说白了
        /// 
        /// 
        /// 
        /// 
        internal GetInfoQuery(int age, string name, DateTime nowTime)
        {
            Age = age;
            Name = name;
            NowTime = nowTime;
        }
        
        public  int Age { get; set; }
        public  string Name { get; set; }
        public  DateTime NowTime { get; set; }
    }
    /// 
    /// 查询命令的处理类
    /// 
    internal class GetInfoQueryHandller : IRequestHandler
    {
        public Task Handle(GetInfoQuery request, CancellationToken cancellationToken)
        {
            Console.WriteLine("GetObjCommandHandller");
            object ret = new
            {
                request.Name,
                request.NowTime,
                request.Age,
            };
            var result = new Result()
            {
                Code = 200,
                Message="Success",
                Data = ret
            };
            return Task.FromResult(result);
        }
    }
}

来看一下调用

using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Mrdiator.Query;
using Newtonsoft.Json;
using System.Net.Http;
using System.Reflection;

//实例化一个ServiceCollection
IServiceCollection services = new ServiceCollection();
//添加当前的程序集MediatR会扫描当前的程序集
//services.AddMediatR(typeof(Program).Assembly);    
services.AddMediatR(Assembly.GetExecutingAssembly());
//构建一个serviceProvider
var serviceProvider = services.BuildServiceProvider();
//从容器中获取mediator
var mediator = serviceProvider.GetService();
//执行命令
var result =await mediator.Send(new GetInfoQuery(18,"wyy",DateTime.Now));

同样我们启动程序也打印了我们的输出。

 b.IRequest:无返回值的栗子

using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Mrdiator.Query
{
    /// 
    /// 命令查询类--无返回值
    /// 
    internal class GetInfoQuery2 : IRequest
    {
        public GetInfoQuery2(int age, string name, DateTime nowTime)
        {
            Age = age;
            Name = name;
            NowTime = nowTime;
        }

        public int Age { get; set; }
        public string Name { get; set; }
        public DateTime NowTime { get; set; }
    }
    /// 
    /// 命令处理类1-----继承AsyncRequestHandler 实现抽象方法 Handle
    /// 
    internal class GetInfoQuery2_2Handller : AsyncRequestHandler
    {
        protected override Task Handle(GetInfoQuery2 request, CancellationToken cancellationToken)
        {
            Console.WriteLine("GetInfoQuery2_2Handller");
            return Task.CompletedTask;
        }
    }
    /// 
    /// 命令处理类2-----IRequestHandler 实现接口方法 Handle
    /// 
    internal class GetInfoQuery2Handller : IRequestHandler
    {
        public Task Handle(GetInfoQuery2 request, CancellationToken cancellationToken)
        {
            Console.WriteLine("GetInfoQuery2Handller");

            return Task.FromResult(new Unit());
        }
    }
   
}
var result2 =await mediator.Send(new GetInfoQuery2(18,"wyy",DateTime.Now));

  我们写了一个GetInfoQuery2,下面有两个类都在泛型里实现了,可以看到程序是只执行了GetInfoQuery2_2Handller就可以看出IRequest命令类跟处理类失忆对一的关系。我们只是通过Mediator的send将GetInfoQuery2 作为参数传进去程序就能执行到GetInfoQuery2_2Handller里面的Handle方法这就是MediatR的好处。

     /// 
    /// 命令处理类-----继承RequestHandler 实现抽象方法 Handle
    /// 
    internal class GetInfoQuery3Handller : RequestHandler
    {
        protected override Result Handle(GetInfoQuery3 request)
        {
            Console.WriteLine("GetInfoQuery3Handller");
            return new Result();
        }
    }

  这样写也可以调用到 ,这就是上面写的 继承不同的类或者接口,一般大多数我都是继承IRequestHandler。

6、INotification栗子

  这里我新建了一个Core6.0的WebAPI的工程来演示INotification的运用。同样的nuget安装MediatR与扩展包MediatR.Extensions.Microsoft.DependencyInjection。在Program.cs里添加。这里如果你的命令处理类跟项目在同一个程序集里面就用第二个也可以,如果你是分开的另外建了一个类库写命令查询的直接引用里面随便一个类获取程序集就可以了

//获取该类下的程序集
builder.Services.AddMediatR(typeof(Program).Assembly);
//获取当前程序集
//builder.Services.AddMediatR(Assembly.GetExecutingAssembly());

   这里我们注册了处理多个事件、每个都执行到了。

using MediatApi.Helper;
using MediatApi.Model;
using MediatR;
using Newtonsoft.Json;

namespace MediatApi.Application.Command
{
    /// 
    /// 创建订单
    /// 
    public class OrderCreateCommand:INotification
    {
        /// 
        /// Id
        /// 
        public long Id { get; set; }
        /// 
        /// 订单号
        /// 
        public string? OrderNum { get; set; }
        /// 
        /// 订单类型
        /// 
        public string? OrderType { get; set; }
        /// 
        /// 创建时间
        /// 
        public DateTime? CreatTime { get; set; }

    }
    /// 
    /// 创建订单处理1
    /// 
    public class OrderCreateCommandHandler : INotificationHandler
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Order model = new(notification.Id, notification.OrderNum, notification.OrderType, notification.CreatTime);
            //数据库操作省略
            Result ret = new()
            {
                Code=200,
                Message="",
                Data=model
            };
            string retJson=JsonConvert.SerializeObject(ret);
            Console.WriteLine("11111——————————————订单创建啦!");
            return Task.FromResult(retJson);
        }
    }
    /// 
    /// 创建订单后处理步骤2
    /// 
    public class OrderCreateTwoHandler : INotificationHandler
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("22222——————————————扣钱成功了");
            return Task.CompletedTask;
        }
    }
    /// 
    ///  创建订单后处理步骤3
    /// 
    public class OrderCreateThreeHandler : INotificationHandler
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("333333333——————————————订单入库啦!");
            return Task.CompletedTask;
        }
    }
    /// 
    ///  创建订单后处理步骤4
    /// 
    public class OrderCreateFoureHandler : INotificationHandler
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("4444444——————————————第四个操作呢!");
            return Task.CompletedTask;
        }
    }
    /// 
    ///  创建订单后处理步骤5
    /// 
    public class OrderCreateFiveHandler : INotificationHandler
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("55555——————————————接着奏乐接着舞!");
            return Task.CompletedTask;
        }
    }

}

 注意:这里是用mediator的publish方法的实现的,命令查询类继承INotification就要用publish方法,继承IRequest就要用Send方法,项目目录也在左侧这样别人看着也清晰点

7、IPipelineBehavior

  这个接口的作用就是在我们命令处理之前或者之后插入逻辑,类似我们的中间件,我新建一个TransactionBehavior来处理保存数据库之前之后的操作,这里的代码只判断了是否为空之前写的是判断事务是否为空,代码多就随便写了意思意思。

 然后新建一个DBTransactionBehavior这里对操作数据库新增演示一下

using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace MediatApi.Helper
{
    public class TransactionBehavior : IPipelineBehavior where TRequest : IRequest where TDBContext : WyyDbContext
    {
        ILogger _logger;
        WyyDbContext _dbContext;

        public TransactionBehavior(ILogger logger, WyyDbContext dbContext)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }

        public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
        {
            var response = default(TResponse);
            try
            {
                Console.WriteLine("执行前逻辑++++++++++++++++++++++++++++++++++");
                Console.WriteLine();
                if (request != null)
                 return await next();

                Console.WriteLine("逻辑不对处理++++++++++++++++++++++++++++++++");
                Console.WriteLine();
                var strategy = _dbContext.Database.CreateExecutionStrategy();

                await strategy.ExecuteAsync(async () =>
                {
                    Guid transactionId;
                    using (var transaction = await _dbContext.Database.BeginTransactionAsync())
                    using (_logger.BeginScope("TransactionContext:{TransactionId}", transaction.TransactionId))
                    {
                        _logger.LogInformation("----- 开始事务 {TransactionId} 请求{request}", transaction.TransactionId, request);

                        response = await next();

                        _logger.LogInformation("----- 提交事务 {TransactionId}}", transaction.TransactionId);

                        await _dbContext.CommitTransactionAsync(transaction);

                        transactionId = transaction.TransactionId;
                    }
                });

                return response;

            }
            catch(Exception ex)
            {
                _logger.LogError(ex, "处理事务出错{@Command}", request);

                throw;
            }
        }
    }
}
using MediatApi.Entity;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;

namespace MediatApi.Helper
{
    public class WyyDbContext: testContext
    {
        private IDbContextTransaction _currentTransaction;

        public IDbContextTransaction GetCurrentTransaction() => _currentTransaction;
        public bool HasActiveTransaction => _currentTransaction != null;
        
        public async Task CommitTransactionAsync(IDbContextTransaction transaction)
        {
            if (transaction == null) throw new ArgumentNullException(nameof(transaction));
            if (transaction != _currentTransaction) throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not current");
            try
            {
                await SaveChangesAsync();
                transaction.Commit();
            }
            catch
            {
                RollbackTransaction();
                throw;
            }
            finally
            {
                if (_currentTransaction != null)
                {
                    _currentTransaction.Dispose();
                    _currentTransaction = null;
                }
            }
        }
        public void RollbackTransaction()
        {
            try
            {
                _currentTransaction?.Rollback();
            }
            finally
            {
                if (_currentTransaction != null)
                {
                    _currentTransaction.Dispose();
                    _currentTransaction = null;
                }
            }
        }
    }
}

 在Program里面注册服务

builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(DBTransactionBehavior<,>));

这里链接数据库我们做一个新增Command里面的testContext就是数据库上下文我这里是从数据库直接生成的 WyyDbContext继承testContext

using MediatApi.Entity;
using MediatR;

namespace MediatApi.Application.Command
{
    public class CusCreateCommand:IRequest<int>
    {
        public string? Name { get; set; }
        public int? Age { get; set; }
    }
    public class CusCreateCommandHandler : IRequestHandlerint>
    {
        private readonly testContext _db;

        public CusCreateCommandHandler(testContext db)
        {
            _db = db;
        }

        public async Task<int> Handle(CusCreateCommand request, CancellationToken cancellationToken)
        {
            Cu c = new()
            {
                Name = request.Name,
                Age = request.Age,

            };
            _db.Cus.Add(c);
            Console.WriteLine("执行处理++++++++++++++++++++++++++++++++++");
            return await _db.SaveChangesAsync();
        }
    }
}

 为了增加对比性 我也新建了一个传统的services来新增Cus

using MediatApi.Entity;

namespace MediatApi.services
{
   
    public interface ICusService
    {
        Task<int> AddAsync();
    }
    public class CusService : ICusService
    {
        private readonly testContext _db;

        public CusService(testContext db)
        {
            _db = db;
        }

        public async Task<int> AddAsync()
        {
            Cu c = new()
            {
                Name = "wyy",
                Age = 18

            };
            _db.Cus.Add(c);
      
return await _db.SaveChangesAsync(); } } }

控制器里面两个新增 一个走MediatRy个走传统的Service

 /// 
        /// 创建用户_mediator
        /// 
        /// 
        /// 
        [HttpPost]
        public async Task<int> CusCreateMediator([FromBody] CusCreateCommand cmd)=> await _mediator.Send(cmd,HttpContext.RequestAborted);
        /// 
        /// 创建用户 Service
        /// 
        /// 

        [HttpPost]
        public async Task<int> CusCreateService() => await _cusService.AddAsync();

 运行可以发现 传统的Service就不会执行。MediatR 中具有与此类似的管线机制,可通过泛型接口 IPipelineBehavior<,>注册,使得我们在 Handle 真正执行前或后可以额外做一些事情:记录日志、对消息做校验、对数据做预处理数据库事务、记录性能较差的Handler 等等。

8、总结

  MediatR的用法

  a.IRequest、IRequest 只有一个单独的Handler执行

  b.Notification,用于多个Handler。

  对于每个 request 类型,都有相应的 handler 接口:

  • IRequestHandler 实现该接口并返回 Task
  • RequestHandler 继承该类并返回 U
  • IRequestHandler 实现该接口并返回 Task
  • AsyncRequestHandler 继承该类并返回 Task
  • RequestHandler 继承该类不返回

  Notification

  Notification 就是通知,调用者发出一次,然后可以有多个处理者参与处理。

  Notification 消息的定义很简单,只需要让你的类继承一个空接口 INotification 即可。而处理程序则实现 INotificationHandler 接口的 Handle 方法

 

  PASS:如果你想成为一个成功的人,那么请为自己加油,让积极打败消极,让高尚打败鄙陋,让真诚打败虚伪,让宽容打败褊狭,让快乐打败忧郁,让勤奋打败懒惰,让坚强打败脆弱,只要你愿意,你完全可以做最好的自己。