前言
之前寫過一篇《乘風破浪,遇見雲原生(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.Api
應用入口,定義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
領域事件處理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
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用於讀。
基於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;
}
}
它的邏輯是在執行命令的處理程序之前先判斷上下文是否開啓事務,如果沒有開啓事務就創建一個事務,再來執行命令處理的邏輯,處理完畢之後,再來提交這個事務。
參考
- 《乘風破浪,遇見雲原生(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 查詢內部實現