乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - MYSQL主從實例+Entity Framework Core實現讀寫分離之實戰演練

前言

之前寫過一篇《乘風破浪,遇見雲原生(Cloud Native)之Docker Desktop for Windows 運行MYSQL多實例並實現主從(Master-Slave)部署》,實現了MYSQL主從多實例部署,基於它我們來寫一寫怎麼在Entity Framework Core的配合下實現讀寫分離,我們通過MediatR來實現CQRS架構設計。

業務背景

某車企開展引薦活動送積分,需要提供一個服務對引薦信息進行管理,通過API接口提供引薦信息的管理能力。

image

簡單架構示意圖

涉及組件

  • MediatR
  • EntityFrameworkCore
  • Swashbuckle
  • MySqlConnector
  • Newtonsoft.Json

解決方案和分層

https://github.com/TaylorShi/HelloEfCoreMasterSlave

dotnet new sln -o HelloEfCoreMasterSlave

這裏我們將採用面向領域驅動設計(DDD)的模式,先將解決方案中項目完成分組:

  • 0.Shared 共享項目,定義業務無關的基礎代碼和接口定義
  • 1.Infrastructure 基礎層,定義倉儲、Context
  • 2.Domain 領域層,定義領域模式和領域事件
  • 3.Application 應用層,定義命令和處理程序,協調調度任務
  • 4.Api 應用入口,定義API終結點、驗證
  • 5.Test 應用測試,定義API終結點、驗證

image

共享項目

Framework.Core

這裏面放一些公共的代碼,比如全局的已知異常定義IKnowException和實現類KnowException、分頁數據PagedList<TData>

image

Framework.Domain.Abstractions

領域抽象項目,這裏定義包括:

  • IAggregateRoot 聚合根接口
  • IEntityIEntity<TKey> 實體接口
  • EntityEntity<TKey> 實體抽象類
  • IDomainEvent 領域事件接口,繼承自MediatR.INotification
  • IDomainEventHandler<TDomainEvent> 領域事件處理程序,繼承自INotificationHandler<TDomainEvent>
  • ValueObject 值對象

依賴包

dotnet add package MediatR.Extensions.Microsoft.DependencyInjection --version 8.0.0

MediatR.Extensions.Microsoft.DependencyInjection其內部包括

  • MediatR
  • Microsoft.Extensions.DependencyInjection.Abstractions

Framework.Infrastructure.Core

基礎層核心項目,這裏分組包括

  • Behaviors 行爲管理
  • Contexts 上下文管理
  • Extensions 擴展管理
  • Repositorys 倉儲管理
  • Transactions 事務管理

行爲管理組包括

  • TransactionBehavior<TDbContext, TRequest, TResponse> 事務行爲管理類,繼承自IPipelineBehavior<TRequest, TResponse>,用於命令執行前後添加事務策略。

上下文管理組包括

  • EFContext Entity Framework Core上下文

擴展管理組包括

  • GenericTypeExtensions 通用類型擴展
  • DomainEventExtension 領域事務擴展
  • QueryableExtensions LINQ查詢擴展

倉儲管理組包括

  • IRepository<TEntity> 實體接口,繼承自實體抽象類Entity和聚合根接口IAggregateRoot
  • Repository<TEntity, TDbContext> 實體抽象類,繼承自實體接口IRepository<TEntity>、實體抽象類Entity和聚合根接口IAggregateRoot

事務管理組包括

  • ITransaction 事務管理接口
  • IUnitOfWork 工作單元接口

依賴包

dotnet add package Microsoft.EntityFrameworkCore.Relational --version 3.1.0

Microsoft.EntityFrameworkCore.Relational其內部包括

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Abstractions
  • Microsoft.EntityFrameworkCore.Analyzers
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection --version 12.0.0

AutoMapper.Extensions.Microsoft.DependencyInjection其內部包括

  • AutoMapper
  • Microsoft.Extensions.Options

依賴項目

  • Framework.Domain.Abstractions
  • Framework.Core

基礎層

Referral.Infrastructure

基礎層項目,這裏分組包括

  • Contexts 上下文管理
  • EntityConfigurations 實體配置
  • Repositories 實體倉儲

上下文管理組包括

  • ReferralContextTransactionBehavior<TRequest, TResponse> 領域事務行爲管理類
  • ReferralMasterContext 業務MasterContext,繼承自Entity Framework Core上下文EFContext,代表MYSQL主實例的Context
  • ReferralSlaveContext 業務SlaveContext,繼承自Entity Framework Core上下文DbContext,代表MYSQL從實例的Context

實體配置組包括

  • ReferralCodeEntityTypeConfiguration 業務領域模型和實體類型配置類,繼承自Entity Framework Core實體配置接口IEntityTypeConfiguration<DomainModel>

實體倉儲組包括

  • IReferralCodeRepository 業務領域倉儲接口,繼承自實體接口IRepository<DomainModel, Key>
  • ReferralCodeRepository 業務領域倉儲類,繼承自實體抽象類Repository<DomainModel, Key, DomainMasterContext>和業務領域倉儲接口IReferralCodeRepository

依賴項目

  • Framework.Infrastructure.Core
  • Referral.Domain

Referral.DataContract

基礎約定項目,這裏分組包括

  • ReferralCode 業務約定模型

領域層

Referral.Domain

領域層項目,這裏分組包括

  • Events 領域事件
  • Aggregates 領域模型

領域模型組包括

  • ReferralCode 業務領域模型,繼承自實體抽象類Entity<Key>和聚合根接口IAggregateRoot

依賴項目

  • Framework.Domain.Abstractions

應用層

Referral.Application

應用層項目,這裏分組包括

  • Commands 命令和處理
  • DomainEventHandlers 領域事件處理
  • IntegrationEvents 集成事件定義
  • Queries 查詢和處理
  • Extensions 服務擴展

命令和處理組包括

  • CreateReferralCommand 創建引薦命令定義,繼承自MediatR命令請求接口IRequest<TResponse>
  • CreateReferralCommandHandler 創建引薦命令處理,繼承自MediatR命令處理接口IRequestHandler<CreateReferralCommand, TResponse>
  • DeleteReferralCommand 刪除引薦命令定義,繼承自MediatR命令請求接口IRequest<TResponse>
  • DeleteReferralCommandHandler 刪除引薦命令處理,繼承自MediatR命令處理接口IRequestHandler<DeleteReferralCommand, TResponse>
  • ModifyReferralCommand 修改引薦命令定義,繼承自MediatR命令請求接口IRequest<TResponse>
  • ModifyReferralCommandHandler 修改引薦命令處理,繼承自MediatR命令處理接口IRequestHandler<ModifyReferralCommand, TResponse>

查詢和處理組包括

  • QueryReferralCommand 查詢引薦命令定義,繼承自MediatR命令請求接口IRequest<TResponse>
  • QueryReferralCommandHandler 查詢引薦命令處理,繼承自MediatR命令處理接口IRequestHandler<QueryReferralCommand, TResponse>

服務擴展組包括

  • CommandHandlerExtensions 命令處理擴展
  • EFContextExtensions EF上下文擴展
  • IntegrationEventsExtensions 集成事件擴展
  • RepositoryExtensions 倉儲服務擴展

依賴包

dotnet add package Pomelo.EntityFrameworkCore.MySql --version 3.1.0

Pomelo.EntityFrameworkCore.MySql其內部包括

  • Microsoft.EntityFrameworkCore.Relational
  • Microsoft.EntityFrameworkCore
  • MySqlConnector
  • Pomelo.JsonObject
  • Newtonsoft.Json

依賴項目

  • Referral.Infrastructure
  • Referral.DataContract

應用入口

Referral.Api

應用入口項目,這裏分組包括

  • Controllers API終結點
  • Extensions 擴展

API終結點組包括

  • ReferralController 業務服務終結點

擴展組包括

  • ApplicationUseExtensions 應用啓用擴展
  • RoutingEndpointExtensions 路由和終結點擴展

依賴項目

  • Referral.Application

依賴包

dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer --version 5.0.0

Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer其內部包括

  • Microsoft.AspNetCore.Mvc.Versioning
dotnet add package Swashbuckle.AspNetCore --version 6.4.0

Swashbuckle.AspNetCore其內部包括

  • Microsoft.Extensions.ApiDescription.Server
  • Swashbuckle.AspNetCore.Swagger
  • Swashbuckle.AspNetCore.SwaggerGen
  • Swashbuckle.AspNetCore.SwaggerUI
  • Microsoft.OpenApi

實現讀寫分離

註冊多實例上下文

Referral.Infrastructure中,我們構建了兩個業務Context,每一個Context會對應一個MYSQL的ConnectionString

我們首先需要將Master和Slave兩個節點的連接字符串在appsettings.json中配置出來。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "MYSQL-Master": "server=localhost;port=16000;user=root;password=xxxxxxxxxxxxxxx;database=xxxx;charset=utf8mb4;ConnectionReset=false;",
  "MYSQL-Slave": "server=localhost;port=17001;user=root;password=xxxxxxxxxxxxxxx;database=xxxx;charset=utf8mb4;ConnectionReset=false;"
}

注意,配置節點名稱分別是MYSQL-MasterMYSQL-Slave,它們的端口是不一樣的。

接下來,在Startup.csConfigureServices中添加MYSQL集羣上下文服務AddMySqlClusterContext

public void ConfigureServices(IServiceCollection services)
{
    // 添加MYSQL集羣上下文服務
    services.AddMySqlClusterContext(Configuration.GetValue<string>("MYSQL-Master"), Configuration.GetValue<string>("MYSQL-Slave"));
}

位於EF上下文擴展EFContextExtensions中的AddMySqlClusterContext定義

/// <summary>
/// EF上下文擴展
/// </summary>
public static class EFContextExtensions
{
    /// <summary>
    /// 添加MYSQL集羣上下文服務
    /// </summary>
    /// <param name="services"></param>
    /// <param name="masterConnectionString"></param>
    /// <param name="slaveConnectionString"></param>
    /// <returns></returns>
    public static IServiceCollection AddMySqlClusterContext(this IServiceCollection services, string masterConnectionString, string slaveConnectionString)
    {
        // 添加引薦MasterContext
        services.AddDbContext<ReferralMasterContext>(optionsAction =>
        {
            optionsAction.UseMySql(masterConnectionString);
        });

        // 添加引薦SlaveContext
        services.AddDbContext<ReferralSlaveContext>(optionsAction =>
        {
            optionsAction.UseMySql(slaveConnectionString);
        });
        return services;
    }
}

這個我們就分開註冊了兩個不同的MYSQL實例,其中一個Master用於寫,另外一個Slave用於讀。

基於MediatR實現CQRS模式

Referral.Api,我們定義了一個業務終結點ReferralController

/// <summary>
/// 引薦服務
/// </summary>
[ApiVersion("1.0")]
[Route("api/v{version:ApiVersion}/[controller]/[action]")]
[ApiController]
public class ReferralController : ControllerBase
{
    readonly IMediator _mediator;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="mediator"></param>
    public ReferralController(IMediator mediator)
    {
        _mediator = mediator;
    }

    /// <summary>
    /// 創建引薦
    /// </summary>
    /// <param name="cmd"></param>
    /// <returns></returns>
    [HttpPost]
    public async Task<bool> Create([FromBody]CreateReferralCommand cmd)
    {
        // 發送創建引薦的命令
        return await _mediator.Send(cmd, HttpContext.RequestAborted);
    }

    /// <summary>
    /// 修改引薦
    /// </summary>
    /// <param name="cmd"></param>
    /// <returns></returns>
    [HttpPost]
    public async Task<bool> Modify([FromBody]ModifyReferralCommand cmd)
    {
        // 發送修改引薦的命令
        return await _mediator.Send(cmd, HttpContext.RequestAborted);
    }

    /// <summary>
    /// 刪除引薦
    /// </summary>
    /// <param name="cmd"></param>
    /// <returns></returns>
    [HttpPost]
    public async Task<bool> Delete([FromBody]DeleteReferralCommand cmd)
    {
        // 發送修改引薦的命令
        return await _mediator.Send(cmd, HttpContext.RequestAborted);
    }

    /// <summary>
    /// 查詢引薦
    /// </summary>
    /// <param name="cmd"></param>
    /// <returns></returns>
    [HttpGet]
    public async Task<PagedList<ReferralCodeDto>> Query([FromQuery]QueryReferralCommand cmd)
    {
        // 發送查詢引薦的命令
        return await _mediator.Send(cmd, HttpContext.RequestAborted);
    }
}

這裏全部通過MediatR將來自前端的請求通過命令的方式發送出去,等待命令被處理之後,再將結果返回給調用者,實現CQRS模式。

領域模型設計

這個案例中,我們僅設計了一個引薦代碼的領域模型ReferralCode

/// <summary>
/// 引薦代碼領域模型
/// </summary>
public class ReferralCode : Entity<long>, IAggregateRoot
{
    /// <summary>
    /// 引薦名稱
    /// </summary>
    public string Name { get; private set; }

    /// <summary>
    /// 引薦代碼
    /// </summary>
    public string Code { get; private set; }

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="name"></param>
    /// <param name="code"></param>
    public ReferralCode(string name, string code)
    {
        Name = name;
        Code = code;
    }

    /// <summary>
    /// 修改
    /// </summary>
    /// <param name="name"></param>
    /// <param name="code"></param>
    public void Modify(string name, string code)
    {
        Name = name;
        Code = code;
    }
}

基於封閉原則,所有的Set都被設置爲Private,創建通過構造函數來進行,修改通過獨立的Modify進行。

Referral.Infrastructure中關於領域模型和實體的映射關係,我們是這樣的設計的

/// <summary>
/// 引薦代碼領域模型和實體類型配置類
/// </summary>
internal class ReferralCodeEntityTypeConfiguration : IEntityTypeConfiguration<ReferralCode>
{
    public void Configure(EntityTypeBuilder<ReferralCode> builder)
    {
        builder.HasKey(p => p.Id);
        builder.ToTable("referralcode");
        builder.HasIndex(p => p.Code);
        builder.Property(p => p.Name).HasMaxLength(120);
        builder.Property(p => p.Code).HasMaxLength(200);
    }
}

引入模型和實體映射

Referral.Application項目中定義好AutoMapperProfile配置ReferralMapperProfile

/// <summary>
/// 引薦映射配置
/// </summary>
public class ReferralMapperProfile : Profile
{
    /// <summary>
    /// 構造函數
    /// </summary>
    public ReferralMapperProfile()
    {
        CreateMap<ReferralCode, ReferralCodeDto>().ReverseMap();
    }
}

這裏通過CreateMap做正向映射,通過ReverseMap做反向映射。

Startup.csConfigureServices掃描並註冊所有的AutoMapperProfile配置。

public void ConfigureServices(IServiceCollection services)
{
    // 添加程序集映射配置
    services.AddAssemblyMapppers();
}

它定義在Referral.Application中擴展組中

/// <summary>
/// 自動映射擴展
/// </summary>
public static class AutoMapperExtensions
{
    /// <summary>
    /// 添加程序集映射配置
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public static IServiceCollection AddAssemblyMapppers(this IServiceCollection services)
    {
        return services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
    }
}

接下來,就只需要在需要轉換的地方,通過IMapper或者AutoMapper.IConfigurationProviderMap<T>即可。

創建引薦命令和處理

創建引薦命令定義

/// <summary>
/// 創建引薦命令定義
/// </summary>
public class CreateReferralCommand : IRequest<bool>
{
    /// <summary>
    /// 引薦名稱
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 引薦代碼
    /// </summary>
    public string Code { get; set; }
}

創建引薦命令處理

/// <summary>
/// 創建引薦命令處理
/// </summary>
internal class CreateReferralCommandHandler : IRequestHandler<CreateReferralCommand, bool>
{
    /// <summary>
    /// 引薦代碼倉儲
    /// </summary>
    private readonly IReferralCodeRepository _referralCodeRepository;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="referralCodeRepository"></param>
    public CreateReferralCommandHandler(IReferralCodeRepository referralCodeRepository)
    {
        _referralCodeRepository = referralCodeRepository;
    }

    /// <summary>
    /// 處理程序
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<bool> Handle(CreateReferralCommand request, CancellationToken cancellationToken)
    {
        var referralCode = new ReferralCode(request.Name, request.Code);
        await _referralCodeRepository.AddAsync(referralCode, cancellationToken);
        return true;
    }
}

這裏從容器中獲取業務倉儲實例_referralCodeRepository,通過它的AddAsync方法實現添加動作。

刪除引薦命令和處理

刪除引薦命令定義

/// <summary>
/// 刪除引薦命令定義
/// </summary>
public class DeleteReferralCommand : IRequest<bool>
{
    /// <summary>
    /// 引薦ID
    /// </summary>
    public int Id { get; set; }
}

刪除引薦命令處理

/// <summary>
/// 刪除引薦命令處理
/// </summary>
internal class DeleteReferralCommandHandler : IRequestHandler<DeleteReferralCommand, bool>
{
    /// <summary>
    /// 引薦代碼倉儲
    /// </summary>
    private readonly IReferralCodeRepository _referralCodeRepository;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="referralCodeRepository"></param>
    public DeleteReferralCommandHandler(IReferralCodeRepository referralCodeRepository)
    {
        _referralCodeRepository = referralCodeRepository;
    }

    /// <summary>
    /// 處理程序
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<bool> Handle(DeleteReferralCommand request, CancellationToken cancellationToken)
    {
        return await _referralCodeRepository.DeleteAsync(request.Id);
    }
}

這裏從容器中獲取業務倉儲實例_referralCodeRepository,通過它的DeleteAsync方法實現刪除動作。

修改引薦命令和處理

修改引薦命令定義

/// <summary>
/// 修改引薦命令定義
/// </summary>
public class ModifyReferralCommand : IRequest<bool>
{
    /// <summary>
    /// 引薦ID
    /// </summary>
    public int Id { get; set; }

    /// <summary>
    /// 引薦名稱
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 引薦代碼
    /// </summary>
    public string Code { get; set; }
}

修改引薦命令處理

/// <summary>
/// 修改引薦命令處理
/// </summary>
internal class ModifyReferralCommandHandler : IRequestHandler<ModifyReferralCommand, bool>
{
    /// <summary>
    /// 引薦代碼倉儲
    /// </summary>
    private readonly IReferralCodeRepository _referralCodeRepository;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="referralCodeRepository"></param>
    public ModifyReferralCommandHandler(IReferralCodeRepository referralCodeRepository)
    {
        _referralCodeRepository = referralCodeRepository;
    }

    /// <summary>
    /// 處理程序
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<bool> Handle(ModifyReferralCommand request, CancellationToken cancellationToken)
    {
        var referralCode = await _referralCodeRepository.GetAsync(request.Id, cancellationToken);
        if (referralCode != null)
        {
            referralCode.Modify(request.Name, request.Code);
            await _referralCodeRepository.UpdateAsync(referralCode);
            return true;
        }
        return false;
    }
}

這裏從容器中獲取業務倉儲實例_referralCodeRepository,先通過GetAsync查詢要修改的數據是否存在,如果存在那麼通過UpdateAsync更新它。

查詢引薦命令處理

查詢引薦命令定義

/// <summary>
/// 查詢引薦命令定義
/// </summary>
public class QueryReferralCommand : IRequest<PagedList<ReferralCodeDto>>
{
    /// <summary>
    /// 引薦ID
    /// </summary>
    public int Id { get; set; }

    /// <summary>
    /// 引薦名稱
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 引薦代碼
    /// </summary>
    public string Code { get; set; }

    /// <summary>
    /// 分頁頁碼
    /// </summary>
    public int PageIndex { get; set; }

    /// <summary>
    /// 分頁大小
    /// </summary>
    public int PageSize { get; set; }
}

查詢引薦命令處理

/// <summary>
/// 查詢引薦命令處理
/// </summary>
public class QueryReferralCommandHandler : IRequestHandler<QueryReferralCommand, List<ReferralCode>>
{
    private readonly ReferralSlaveContext _referralSlaveContext;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="referralSlaveContext"></param>
    public QueryReferralCommandHandler(ReferralSlaveContext referralSlaveContext)
    {
        _referralSlaveContext = referralSlaveContext;
    }

    /// <summary>
    /// 處理程序
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<List<ReferralCode>> Handle(QueryReferralCommand request, CancellationToken cancellationToken)
    {
        IQueryable<ReferralCode> query = _referralSlaveContext.ReferralCodes;
        if (request.Id > 0)
        {
            query = query.Where(x => x.Id == request.Id);
        }
        if (!string.IsNullOrEmpty(request.Name))
        {
            query = query.Where(x => x.Name == request.Name);
        }
        if (!string.IsNullOrEmpty(request.Code))
        {
            query = query.Where(x => x.Code == request.Code);
        }
        return await query.ToListAsync();
    }
}

這裏從容器中獲取SlaveContext實例_referralSlaveContext,通過判斷查詢入參的條件來拼接IQueryable<ReferralCode>,最後通過ToListAsync獲取篩選結果。

但是上面這種寫法有點囉嗦,我們引入一個LINQ查詢擴展QueryableExtensions以便優化它。

/// <summary>
/// LINQ查詢擴展
/// </summary>
public static class QueryableExtensions
{
    /// <summary>
    /// 分頁查詢
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="query"></param>
    /// <param name="skipCount"></param>
    /// <param name="maxResultCount"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    public static IQueryable<T> PageBy<T>(this IQueryable<T> query, int skipCount, int maxResultCount)
    {
        if (query == null)
        {
            throw new ArgumentNullException("query");
        }

        return query.Skip(skipCount).Take(maxResultCount);
    }

    /// <summary>
    /// 根據If條件篩選
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="query"></param>
    /// <param name="condition"></param>
    /// <param name="predicate"></param>
    /// <returns></returns>
    public static IQueryable<T> WhereIf<T>(this IQueryable<T> query, bool condition, Expression<Func<T, bool>> predicate)
    {
        return condition
            ? query.Where(predicate)
            : query;
    }

    /// <summary>
    /// 根據If條件篩選
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="query"></param>
    /// <param name="condition"></param>
    /// <param name="predicate"></param>
    /// <returns></returns>
    public static IQueryable<T> WhereIf<T>(this IQueryable<T> query, bool condition, Expression<Func<T, int, bool>> predicate)
    {
        return condition
            ? query.Where(predicate)
            : query;
    }
}

最終我們可以將前面的查詢優化爲如下的寫法

/// <summary>
/// 處理程序
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<List<ReferralCode>> Handle(QueryReferralCommand request, CancellationToken cancellationToken)
{
    IQueryable<ReferralCode> query = _referralSlaveContext.ReferralCodes
        .WhereIf(request.Id > 0, x => x.Id == request.Id)
        .WhereIf(!string.IsNullOrEmpty(request.Name), x => x.Name == request.Name)
        .WhereIf(!string.IsNullOrEmpty(request.Code), x => x.Code == request.Code);

    return await query.ToListAsync();
}

這裏我們再升級下,支持分頁查詢,首先我們自定義一個擴展方法Paged,在這裏我們結合AutoMapperProjectTo<T>機制一起來用,這樣就省去了重複的模型轉換代碼了。

/// <summary>
/// 分頁查詢
/// </summary>
/// <typeparam name="TDomainModel"></typeparam>
/// <typeparam name="TDataModel"></typeparam>
/// <param name="query"></param>
/// <param name="pageIndex"></param>
/// <param name="pageSize"></param>
/// <param name="configuration"></param>
/// <param name="maxPageSize"></param>
/// <param name="defaultPageSize"></param>
/// <param name="defaultPageIndex"></param>
/// <returns></returns>
public static async Task<PagedList<TDataModel>> Paged<TDomainModel,TDataModel>(this IQueryable<TDomainModel> query, int pageIndex, int pageSize, AutoMapper.IConfigurationProvider configuration,int maxPageSize=200,int defaultPageSize=15,int defaultPageIndex =1)
{
    if (pageIndex <= 0)
    {
        pageIndex = defaultPageIndex;
    }
    if (pageSize <= 0 || pageSize > maxPageSize)
    {
        pageSize = defaultPageSize;
    }
    var resultCount = await query.CountAsync();
    if (pageSize * (pageIndex - 1) >= resultCount)
    {
        return new PagedList<TDataModel>(new List<TDataModel>(), resultCount, pageIndex, pageSize);
    }
    var items = await query.PageBy(pageIndex - 1, pageSize).ProjectTo<TDataModel>(configuration).ToListAsync();
    return new PagedList<TDataModel>(items, resultCount, pageIndex, pageSize);
}

最終我們將分頁查詢處理寫成

/// <summary>
/// 查詢引薦命令處理
/// </summary>
public class QueryReferralCommandHandler : IRequestHandler<QueryReferralCommand, PagedList<ReferralCodeDto>>
{
    private readonly ReferralSlaveContext _referralSlaveContext;
    private readonly IConfigurationProvider _configurationProvider;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="referralSlaveContext"></param>
    public QueryReferralCommandHandler(ReferralSlaveContext referralSlaveContext, IConfigurationProvider configurationProvider)
    {
        _referralSlaveContext = referralSlaveContext;
        _configurationProvider = configurationProvider;
    }

    /// <summary>
    /// 處理程序
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<PagedList<ReferralCodeDto>> Handle(QueryReferralCommand request, CancellationToken cancellationToken)
    {
        IQueryable<ReferralCode> query = _referralSlaveContext.ReferralCodes
            .WhereIf(request.Id > 0, x => x.Id == request.Id)
            .WhereIf(!string.IsNullOrEmpty(request.Name), x => x.Name == request.Name)
            .WhereIf(!string.IsNullOrEmpty(request.Code), x => x.Code == request.Code);

        return await query.Paged<ReferralCode, ReferralCodeDto>(request.PageIndex, request.PageSize, _configurationProvider);
    }
}

最後看下PagedList的定義

/// <summary>
/// 分頁數據
/// </summary>
/// <typeparam name="TData"></typeparam>
public class PagedList<TData>
{
    /// <summary>
    /// 結果集
    /// </summary>
    public IEnumerable<TData> Data { get; set; }

    /// <summary>
    /// 分頁序號
    /// </summary>
    public int PageIndex { get; set; }

    /// <summary>
    /// 分頁大小
    /// </summary>
    public int PageSize { get; set; }

    /// <summary>
    /// 總頁數
    /// </summary>
    public int PageCount { get; set; }

    /// <summary>
    /// 總記錄數
    /// </summary>
    public int RecordCount { get; set; }

    public PagedList(IEnumerable<TData> dataSource, int recordCount, int pageIndex, int pageSize)
    {
        Data = dataSource;
        RecordCount = recordCount;
        PageIndex = pageIndex;
        PageSize = pageSize;
        if (pageSize == 0)
        {
            PageCount = 0;
        }
        else
        {
            PageCount = (int)Math.Ceiling((decimal)recordCount / (decimal)pageSize);
        }
    }
}

自動事務加持

通過MediatR實現命令和查詢分離的同時,其實我們還做了一個自動事務的設計,它有個類似中間件的邏輯,我們在Referral.Application中定義了一個命令處理擴展CommandHandlerExtensions,我們看下它的定義

/// <summary>
/// 命令處理擴展
/// </summary>
public static class CommandHandlerExtensions
{
    /// <summary>
    /// 添加命令處理服務
    /// </summary>
    /// <param name="services"></param>
    /// <returns></returns>
    public static IServiceCollection AddCommandHandlers(this IServiceCollection services)
    {
        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ReferralContextTransactionBehavior<,>));
        return services.AddMediatR(typeof(ReferralCode).Assembly, typeof(CreateReferralCommand).Assembly);
    }
}

這裏將ReferralContextTransactionBehavior<,>註冊爲IPipelineBehavior<,>的實現。它本質是事務行爲管理類TransactionBehavior<TDbContext, TRequest, TResponse>的實現。

它的處理核心邏輯是

/// <summary>
/// 處理程序
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <param name="next"></param>
/// <returns></returns>
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
    var response = default(TResponse);
    var typeName = request.GetGenericTypeName();

    try
    {
        // 如果當前開啓了事務,那麼就繼續後面的動作
        if (_dbContext.HasActiveTransaction)
        {
            return await next();
        }

        var strategy = _dbContext.Database.CreateExecutionStrategy();

        await strategy.ExecuteAsync(async () =>
        {
            Guid transactionId;
            using (var transaction = await _dbContext.BeginTransactionAsync())
            using (_logger.BeginScope("TransactionContext:{TransactionId}", transaction.TransactionId))
            {
                _logger.LogInformation("----- 開始事務 {TransactionId} ({@Command})", transaction.TransactionId, typeName, request);

                response = await next();

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

                await _dbContext.CommitTransactionAsync(transaction);

                transactionId = transaction.TransactionId;
            }
        });

        return response;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "處理事務出錯 {CommandName} ({@Command})", typeName, request);

        throw;
    }
}

它的邏輯是在執行命令的處理程序之前先判斷上下文是否開啓事務,如果沒有開啓事務就創建一個事務,再來執行命令處理的邏輯,處理完畢之後,再來提交這個事務。

參考

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