乘风破浪,遇见最佳跨平台跨终端框架.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.Interface 接口层,定义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 领域事件处理
  • Extensions 服务扩展
  • IntegrationEvents 集成事件定义
  • MapperProfiles 模型映射关系
  • Queries 查询和处理

命令和处理组包括

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

查询和处理组包括

  • ReferralQuery 引荐查询定义,继承自MediatR命令请求接口IRequest<TResponse>
  • ReferralQueryHandler 引荐查询处理,继承自MediatR命令处理接口IRequestHandler<ReferralQuery, 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用于读。

将实体仓储类绑定MasterContext

默认情况下,我们对实体对象进行增删改都是需要走IReferralCodeRepository的实现者ReferralCodeRepository来操作的,那也就默认限定了其背后操作的是ReferralMasterContext对应的数据源,而查询的时候,我们应该优先使用ReferralSlaveContext,万一不小心用到了主数据源,原则上也不会有太大问题。

/// <summary>
/// 引荐代码仓储类
/// </summary>
public class ReferralCodeRepository : Repository<ReferralCode, long, ReferralMasterContext>, IReferralCodeRepository
{
    public ReferralCodeRepository(ReferralMasterContext context) : base(context)
    {
    }
}

基于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;
    }
}

它的逻辑是在执行命令的处理程序之前先判断上下文是否开启事务,如果没有开启事务就创建一个事务,再来执行命令处理的逻辑,处理完毕之后,再来提交这个事务。

优化数据库连接池最大连接数

ConnectionString中,数据库连接池中所允许的最大连接数MaxPoolSize100,数据库连接池中所允许的最小连接数MinPoolSize10,我们可以适当的修改它,来优化并发处理能力。

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

参考

posted @ 2022-11-01 18:56  TaylorShi  阅读(148)  评论(3编辑  收藏  举报