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<T>:命令查詢 | 處理類所繼承的接口,一個有返回類型,一個無返回類型,一個查詢對應一個處理類,程序集只認第一個掃描到的類。

  c.IRequestHandler<in TRequest,TResponse>(實現Handle方法) :命令處理接口。命令查詢 | 處理類繼承它,也可以繼承AsyncRequestHandler(實現抽象Handle方法)、RequestHandler(實現抽象Handle方法)接口

  d.INotification:命令查詢 | 處理類所繼承的接口這個沒有返回,與IRequest不通的是可以對於多個處理類。

  e.INotificationHandler<in TNotification>:與IRequestHandler一樣的只不過這是INotification的處理接口

5、IRequest栗子

a.IRequest<T>:有返回值的類

  說了那麼多幹巴巴的直接上代碼看。我這裏是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
{
    /// <summary>
    /// 查詢信息命令類
    /// </summary>
    internal class GetInfoQuery:IRequest<Result>
    {
        /// <summary>
        /// 構造函數--就是查詢的條件說白了
        /// </summary>
        /// <param name="age"></param>
        /// <param name="name"></param>
        /// <param name="nowTime"></param>
        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; }
    }
    /// <summary>
    /// 查詢命令的處理類
    /// </summary>
    internal class GetInfoQueryHandller : IRequestHandler<GetInfoQuery, Result>
    {
        public Task<Result> 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<IMediator>();
//執行命令
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
{
    /// <summary>
    /// 命令查詢類--無返回值
    /// </summary>
    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; }
    }
    /// <summary>
    /// 命令處理類1-----繼承AsyncRequestHandler 實現抽象方法 Handle
    /// </summary>
    internal class GetInfoQuery2_2Handller : AsyncRequestHandler<GetInfoQuery2>
    {
        protected override Task Handle(GetInfoQuery2 request, CancellationToken cancellationToken)
        {
            Console.WriteLine("GetInfoQuery2_2Handller");
            return Task.CompletedTask;
        }
    }
    /// <summary>
    /// 命令處理類2-----IRequestHandler 實現接口方法 Handle
    /// </summary>
    internal class GetInfoQuery2Handller : IRequestHandler<GetInfoQuery2>
    {
        public Task<Unit> 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的好處。

     /// <summary>
    /// 命令處理類-----繼承RequestHandler 實現抽象方法 Handle
    /// </summary>
    internal class GetInfoQuery3Handller : RequestHandler<GetInfoQuery3, Result>
    {
        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
{
    /// <summary>
    /// 創建訂單
    /// </summary>
    public class OrderCreateCommand:INotification
    {
        /// <summary>
        /// Id
        /// </summary>
        public long Id { get; set; }
        /// <summary>
        /// 訂單號
        /// </summary>
        public string? OrderNum { get; set; }
        /// <summary>
        /// 訂單類型
        /// </summary>
        public string? OrderType { get; set; }
        /// <summary>
        /// 創建時間
        /// </summary>
        public DateTime? CreatTime { get; set; }

    }
    /// <summary>
    /// 創建訂單處理1
    /// </summary>
    public class OrderCreateCommandHandler : INotificationHandler<OrderCreateCommand>
    {
        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);
        }
    }
    /// <summary>
    /// 創建訂單後處理步驟2
    /// </summary>
    public class OrderCreateTwoHandler : INotificationHandler<OrderCreateCommand>
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("22222——————————————扣錢成功了");
            return Task.CompletedTask;
        }
    }
    /// <summary>
    ///  創建訂單後處理步驟3
    /// </summary>
    public class OrderCreateThreeHandler : INotificationHandler<OrderCreateCommand>
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("333333333——————————————訂單入庫啦!");
            return Task.CompletedTask;
        }
    }
    /// <summary>
    ///  創建訂單後處理步驟4
    /// </summary>
    public class OrderCreateFoureHandler : INotificationHandler<OrderCreateCommand>
    {
        public Task Handle(OrderCreateCommand notification, CancellationToken cancellationToken)
        {
            Console.WriteLine("4444444——————————————第四個操作呢!");
            return Task.CompletedTask;
        }
    }
    /// <summary>
    ///  創建訂單後處理步驟5
    /// </summary>
    public class OrderCreateFiveHandler : INotificationHandler<OrderCreateCommand>
    {
        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<TDBContext, TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> 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<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> 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 : IRequestHandler<CusCreateCommand, int>
    {
        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

 /// <summary>
        /// 創建用戶_mediator
        /// </summary>
        /// <param name="cmd"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<int> CusCreateMediator([FromBody] CusCreateCommand cmd)=> await _mediator.Send(cmd,HttpContext.RequestAborted);
        /// <summary>
        /// 創建用戶 Service
        /// </summary>
        /// <returns></returns>

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

   運行可以發現 傳統的Service就不會執行。MediatR 中具有與此類似的管線機制,可通過泛型接口 IPipelineBehavior<,>註冊,使得我們在 Handle 真正執行前或後可以額外做一些事情:記錄日誌、對消息做校驗、對數據做預處理數據庫事務、記錄性能較差的Handler 等等。

8、總結

  MediatR的用法

  a.IRequest、IRequest<T> 只有一個單獨的Handler執行

  b.Notification,用於多個Handler。

  對於每個 request 類型,都有相應的 handler 接口:

  • IRequestHandler<T, U> 實現該接口並返回 Task<U>
  • RequestHandler<T, U> 繼承該類並返回 U
  • IRequestHandler<T> 實現該接口並返回 Task<Unit>
  • AsyncRequestHandler<T> 繼承該類並返回 Task
  • RequestHandler<T> 繼承該類不返回

  Notification

  Notification 就是通知,調用者發出一次,然後可以有多個處理者參與處理。

  Notification 消息的定義很簡單,只需要讓你的類繼承一個空接口 INotification 即可。而處理程序則實現 INotificationHandler<T> 接口的 Handle 方法

 

  PASS:如果你想成爲一個成功的人,那麼請爲自己加油,讓積極打敗消極,讓高尚打敗鄙陋,讓真誠打敗虛僞,讓寬容打敗褊狹,讓快樂打敗憂鬱,讓勤奮打敗懶惰,讓堅強打敗脆弱,只要你願意,你完全可以做最好的自己。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章