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

2022-11-11 18:02:42

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