乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - MYSQL主从实例+Entity Framework Core实现读写分离之实战演练
前言
之前写过一篇《乘风破浪,遇见云原生(Cloud Native)之Docker Desktop for Windows 运行MYSQL多实例并实现主从(Master-Slave)部署》,实现了MYSQL主从多实例部署,基于它我们来写一写怎么在Entity Framework Core的配合下实现读写分离,我们通过MediatR来实现CQRS架构设计。
业务背景
某车企开展引荐活动送积分,需要提供一个服务对引荐信息进行管理,通过API接口提供引荐信息的管理能力。
简单架构示意图
涉及组件
MediatR
EntityFrameworkCore
Swashbuckle
MySqlConnector
Newtonsoft.Json
解决方案和分层
dotnet new sln -o HelloEfCoreMasterSlave
这里我们将采用面向领域驱动设计(DDD
)的模式,先将解决方案中项目完成分组:
0.Shared
共享项目,定义业务无关的基础代码和接口定义1.Infrastructure
基础层,定义仓储、Context2.Domain
领域层,定义领域模式和领域事件3.Application
应用层,定义命令和处理程序,协调调度任务4.Interface
接口层,定义API终结点、验证5.Test
应用测试,定义API终结点、验证
共享项目
Framework.Core
这里面放一些公共的代码,比如全局的已知异常定义IKnowException
和实现类KnowException
、分页数据PagedList<TData>
。
Framework.Domain.Abstractions
领域抽象项目,这里定义包括:
IAggregateRoot
聚合根接口IEntity
、IEntity<TKey>
实体接口Entity
、Entity<TKey>
实体抽象类IDomainEvent
领域事件接口,继承自MediatR.INotification
IDomainEventHandler<TDomainEvent>
领域事件处理程序,继承自INotificationHandler<TDomainEvent>
ValueObject
值对象
依赖包
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection --version 8.0.0
- 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
- Microsoft.EntityFrameworkCore.Abstractions
- Microsoft.EntityFrameworkCore.Analyzers
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection --version 12.0.0
- AutoMapper
- Microsoft.Extensions.Options
依赖项目
Framework.Domain.Abstractions
Framework.Core
基础层
Referral.Infrastructure
基础层项目,这里分组包括
Contexts
上下文管理EntityConfigurations
实体配置Repositories
实体仓储
上下文管理组包括
ReferralContextTransactionBehavior<TRequest, TResponse>
领域事务行为管理类ReferralMasterContext
业务MasterContext,继承自Entity Framework Core上下文EFContext
,代表MYSQL主实例的ContextReferralSlaveContext
业务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
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
dotnet add package Swashbuckle.AspNetCore --version 6.4.0
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-Master
和MYSQL-Slave
,它们的端口是不一样的。
接下来,在Startup.cs
的ConfigureServices
中添加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
项目中定义好AutoMapper
的Profile
配置ReferralMapperProfile
/// <summary>
/// 引荐映射配置
/// </summary>
public class ReferralMapperProfile : Profile
{
/// <summary>
/// 构造函数
/// </summary>
public ReferralMapperProfile()
{
CreateMap<ReferralCode, ReferralCodeDto>().ReverseMap();
}
}
这里通过CreateMap
做正向映射,通过ReverseMap
做反向映射。
在Startup.cs
的ConfigureServices
扫描并注册所有的AutoMapper
的Profile
配置。
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.IConfigurationProvider
来Map<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
,在这里我们结合AutoMapper
的ProjectTo<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
中,数据库连接池中所允许的最大连接数MaxPoolSize
是100
,数据库连接池中所允许的最小连接数MinPoolSize
是10
,我们可以适当的修改它,来优化并发处理能力。
{
"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;"
}
参考
- 《乘风破浪,遇见云原生(Cloud Native)之Docker Desktop for Windows 运行MYSQL多实例并实现主从(Master-Slave)部署》
- Get Error : Entity type 'Course' is defined with a single key property, but 2 values were passed to the 'DbSet.Find' method
- “开源、共享、创新”, 中国最具前景开发者峰会落幕魔都
- 增删改查,命名
- 再讲
IQueryable<T>
,揭开表达式树的神秘面纱 - .NET 复习笔记 / LINQ,从IQueryable说起
- .NET 在云原生时代的蜕变,让我在云时代脱颖而出
- 多语言(Java、.NET、Node.js)混合架构下开源调用链追踪APM项目初步选型
- China.NETConf2019 - 用ASP.NETCore构建可检测的高可用服务
- 设计模式之美:Mediator(中介者)
- https://github.com/AutoMapper/AutoMapper
- AutoMapper extensions for Microsoft.Extensions.DependencyInjection
- EF Core 相关的千倍性能之差: AutoMapper ProjectTo VS Mapster ProjectToType
- https://github.com/MapsterMapper/Mapster
- https://github.com/AutoMapper/AutoMapper.EF6
- https://github.com/dotnet-architecture/eShopOnContainers
- https://github.com/jbogard/MediatR
- MediatR extensions for Microsoft.Extensions.DependencyInjection
- https://github.com/aspnetboilerplate/aspnetboilerplate
- Abp/Linq/Extensions/QueryableExtensions.cs
- 索引
- 高效查询
- EF查询 常用
IQueryable<T>
拓展 - ABP Linq 扩展的 WhereIf 查询内部实现
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 趁着过年的时候手搓了一个低代码框架
· 用 C# 插值字符串处理器写一个 sscanf
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!
2021-11-01 时光卷轴,IT启示录-2021年-11月刊