增加Mediat Pip特性,并用实例来做一个demo
This commit is contained in:
1 parent
292cb8638c
commit
c9ac7412ab
19 files changed
+682
-108
No files matched your search
@@ -0,0 +1,211 @@
|
||||
using Store.Users.Behaviors;
|
||||
using Store.Users.Entities;
|
||||
using YourNamespace;
|
||||
using System.Reflection;
|
||||
using MAX.Framework.Mediation;
|
||||
using MAX.Core.Mediation;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MAX.Core.DependencyInjection;
|
||||
using MAX.Framework.DependencyInjection;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Store.Users.Commands;
|
||||
using Store.Users.Contracts.Dtos;
|
||||
|
||||
// 使用实际 Startup 注册路径,替换数据库处理器以单独验证 MediatR 管道。
|
||||
var services = new ServiceCollection();
|
||||
var registrarType = typeof(ServiceCollectionExtensions).Assembly
|
||||
.GetType("MAX.Framework.DependencyInjection.ServiceRegistrarProxy", throwOnError: true)!;
|
||||
var registrar = (IServiceRegistrar)Activator.CreateInstance(registrarType, services)!;
|
||||
new Store.Users.Startup().Register(registrar);
|
||||
services.AddMediatRAttributedBehaviors(typeof(ShopRenewCommand).Assembly);
|
||||
services.AddMediatRAttributedBehaviors(typeof(ShopRenewCommand).Assembly); // 重复扫描不重复注册
|
||||
var pipelineType = typeof(IPipelineBehavior<ShopRenewCommand, ShopRenewResponse>);
|
||||
var pipelines = services.Where(s => s.ServiceType == pipelineType).ToArray();
|
||||
if (pipelines.Length != 4
|
||||
|| pipelines[0].ImplementationType?.Name != "ValidationBehavior`2"
|
||||
|| pipelines[1].ImplementationType?.Name != "TransactionBehavior`2"
|
||||
|| pipelines[2].ImplementationType?.Name != "AccountConsumptionBehavior`2"
|
||||
|| pipelines[3].ImplementationType?.Name != "ShopOwnershipBehavior`2")
|
||||
throw new Exception("管道注册顺序错误,应为参数验证、事务、消费检查");
|
||||
// 本测试只验证参数管道;事务与消费管道需在测试库中进行集成测试。
|
||||
services.Remove(pipelines[1]);
|
||||
services.Remove(pipelines[2]);
|
||||
services.Remove(pipelines[3]);
|
||||
services.AddSingleton<ProbeHandler>();
|
||||
services.AddTransient<IRequestHandler<ShopRenewCommand, ShopRenewResponse>>(sp => sp.GetRequiredService<ProbeHandler>());
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var mediator = provider.GetRequiredService<IMediator>();
|
||||
var probe = provider.GetRequiredService<ProbeHandler>();
|
||||
var valid = new ShopRenewCommand("user", "shop", 1);
|
||||
ShopRenewCommand[] invalid =
|
||||
[
|
||||
valid with { UserName = " " }, valid with { UserName = new string('u', 21) },
|
||||
valid with { ShopId = " " }, valid with { ShopId = new string('s', 51) },
|
||||
valid with { RenewDuration = 0 }, valid with { RenewDuration = 121 },
|
||||
valid with { Remark = new string('r', 256) }
|
||||
];
|
||||
foreach (var command in invalid)
|
||||
{
|
||||
try
|
||||
{
|
||||
await mediator.Send(command);
|
||||
throw new Exception("无效参数未被管道拦截");
|
||||
}
|
||||
catch (ValidationException) { }
|
||||
}
|
||||
if (probe.Calls != 0) throw new Exception("无效命令进入了处理器");
|
||||
await mediator.Send(valid);
|
||||
await mediator.Send(valid with
|
||||
{
|
||||
UserName = " " + new string('u', 20) + " ", ShopId = new string('s', 50),
|
||||
RenewDuration = 120, Remark = " " + new string('r', 255) + " "
|
||||
});
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
cancellation.Cancel();
|
||||
try
|
||||
{
|
||||
await mediator.Send(valid, cancellation.Token);
|
||||
throw new Exception("已取消命令未被拦截");
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
if (probe.Calls != 2) throw new Exception("有效命令或取消命令的调用次数错误");
|
||||
var minimumQuote = ((IAccountConsumptionRequest)(valid with { RenewDuration = 3 })).CalculateConsumption(new UserEntity { ShopPointsPerMonth = 10 });
|
||||
var customQuote = ((IAccountConsumptionRequest)(valid with { RenewDuration = 2 })).CalculateConsumption(new UserEntity { ShopPointsPerMonth = 45 });
|
||||
if (minimumQuote != new ConsumptionQuote(90m, 30m) || customQuote != new ConsumptionQuote(90m, 45m))
|
||||
throw new Exception("续费最低价格或账户自定义价格计算错误");
|
||||
var reuseServices = new ServiceCollection();
|
||||
reuseServices.AddMediatRAttributedBehaviors(typeof(AccountOnlyRequest).Assembly);
|
||||
var accountBehaviors = reuseServices.Where(s => s.ServiceType == typeof(IPipelineBehavior<AccountOnlyRequest, string>)).ToArray();
|
||||
var shopBehaviors = reuseServices.Where(s => s.ServiceType == typeof(IPipelineBehavior<ShopOnlyRequest, string>)).ToArray();
|
||||
if (accountBehaviors.Length != 2
|
||||
|| accountBehaviors[0].ImplementationType != typeof(TransactionBehavior<AccountOnlyRequest, string>)
|
||||
|| accountBehaviors[1].ImplementationType != typeof(AccountConsumptionBehavior<AccountOnlyRequest, string>)
|
||||
|| shopBehaviors.Length != 1
|
||||
|| shopBehaviors[0].ImplementationType != typeof(ShopOwnershipBehavior<ShopOnlyRequest, string>))
|
||||
throw new Exception("非商铺消费和非消费商铺操作的管道注册未独立");
|
||||
Console.WriteLine("PASS: 独立消费与商铺操作管道注册、续费计费策略验证通过。");
|
||||
|
||||
var orderedServices = new ServiceCollection();
|
||||
orderedServices.AddSingleton<ExecutionLog>();
|
||||
orderedServices.AddTransient<IRequestHandler<OrderRequest, string>, OrderHandler>();
|
||||
orderedServices.AddMediatRAttributedBehaviors(typeof(OrderRequest).Assembly);
|
||||
orderedServices.AddMediatRAttributedBehaviors(typeof(OrderRequest).Assembly);
|
||||
using var orderedProvider = orderedServices.BuildServiceProvider();
|
||||
await orderedProvider.GetRequiredService<IMediator>().Send(new OrderRequest());
|
||||
var events = orderedProvider.GetRequiredService<ExecutionLog>().Events;
|
||||
if (!events.SequenceEqual(new[] { "Outer before", "Inner before", "Handler", "Inner after", "Outer after" }))
|
||||
throw new Exception("特性管道执行顺序不正确:" + string.Join(", ", events));
|
||||
if (pipelines[1].ImplementationType != typeof(TransactionBehavior<ShopRenewCommand, ShopRenewResponse>))
|
||||
throw new Exception("开放泛型事务管道未正确闭合");
|
||||
var executionContext = new UnitOfWorkExecutionContext();
|
||||
var enter = typeof(UnitOfWorkExecutionContext).GetMethod("Enter", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
IFreeSql CreateOrm() => DispatchProxy.Create<IFreeSql, UnusedOrmProxy>();
|
||||
IDisposable Enter(IFreeSql orm) => (IDisposable)enter.Invoke(executionContext, [orm])!;
|
||||
try
|
||||
{
|
||||
_ = executionContext.Orm;
|
||||
throw new Exception("无事务时不应允许访问 ORM");
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
async Task VerifyIsolation(IFreeSql orm)
|
||||
{
|
||||
using (Enter(orm))
|
||||
{
|
||||
await Task.Yield();
|
||||
if (!ReferenceEquals(executionContext.Orm, orm)) throw new Exception("并行事务上下文串用");
|
||||
try
|
||||
{
|
||||
using var nested = Enter(CreateOrm());
|
||||
throw new Exception("嵌套事务应被拒绝");
|
||||
}
|
||||
catch (TargetInvocationException exception) when (exception.InnerException is InvalidOperationException) { }
|
||||
if (!ReferenceEquals(executionContext.Orm, orm)) throw new Exception("嵌套失败破坏了外层上下文");
|
||||
}
|
||||
if (executionContext.IsActive) throw new Exception("执行结束后上下文未清理");
|
||||
}
|
||||
await Task.WhenAll(VerifyIsolation(CreateOrm()), VerifyIsolation(CreateOrm()));
|
||||
try
|
||||
{
|
||||
using var scope = Enter(CreateOrm());
|
||||
throw new ApplicationException("模拟处理器失败");
|
||||
}
|
||||
catch (ApplicationException) { }
|
||||
if (executionContext.IsActive) throw new Exception("异常后上下文未清理");
|
||||
Console.WriteLine("PASS: 泛型事务管道闭合、并行上下文隔离、嵌套拒绝和异常清理验证通过。");
|
||||
|
||||
Console.WriteLine("PASS: CallerLineNumber 顺序、真实嵌套调用与重复扫描验证通过。");
|
||||
|
||||
Console.WriteLine("PASS: 实际 DI 注册、7 项非法输入、有效边界参数和取消请求验证通过。");
|
||||
|
||||
sealed class ProbeHandler : IRequestHandler<ShopRenewCommand, ShopRenewResponse>
|
||||
{
|
||||
public int Calls { get; private set; }
|
||||
public Task<ShopRenewResponse> Handle(ShopRenewCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
Calls++;
|
||||
return Task.FromResult(new ShopRenewResponse { ShopId = request.ShopId });
|
||||
}
|
||||
}
|
||||
|
||||
[MediatRBehavior(typeof(OuterOrderBehavior))]
|
||||
[MediatRBehavior(typeof(InnerOrderBehavior))]
|
||||
sealed record OrderRequest : IRequest<string>;
|
||||
|
||||
sealed class ExecutionLog
|
||||
{
|
||||
public List<string> Events { get; } = [];
|
||||
}
|
||||
|
||||
sealed class OrderHandler(ExecutionLog log) : IRequestHandler<OrderRequest, string>
|
||||
{
|
||||
public Task<string> Handle(OrderRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
log.Events.Add("Handler");
|
||||
return Task.FromResult("ok");
|
||||
}
|
||||
}
|
||||
|
||||
sealed class OuterOrderBehavior(ExecutionLog log) : IPipelineBehavior<OrderRequest, string>
|
||||
{
|
||||
public async Task<string> Handle(OrderRequest request, CancellationToken cancellationToken,
|
||||
RequestHandlerDelegate<string> next)
|
||||
{
|
||||
log.Events.Add("Outer before");
|
||||
var result = await next();
|
||||
log.Events.Add("Outer after");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class InnerOrderBehavior(ExecutionLog log) : IPipelineBehavior<OrderRequest, string>
|
||||
{
|
||||
public async Task<string> Handle(OrderRequest request, CancellationToken cancellationToken,
|
||||
RequestHandlerDelegate<string> next)
|
||||
{
|
||||
log.Events.Add("Inner before");
|
||||
var result = await next();
|
||||
log.Events.Add("Inner after");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
public class UnusedOrmProxy : DispatchProxy
|
||||
{
|
||||
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
|
||||
=> throw new NotSupportedException("上下文测试不应访问数据库");
|
||||
}
|
||||
// 非商铺消费:没有 ShopId,也不启用商铺归属管道。
|
||||
[MediatRBehavior(typeof(TransactionBehavior<,>))]
|
||||
[MediatRBehavior(typeof(AccountConsumptionBehavior<,>))]
|
||||
sealed record AccountOnlyRequest(string UserName, decimal Amount) : IRequest<string>, IAccountConsumptionRequest
|
||||
{
|
||||
public AccountConsumptionContext? ConsumptionContext { get; set; }
|
||||
public ConsumptionQuote CalculateConsumption(UserEntity user) => new(Amount, Amount);
|
||||
}
|
||||
|
||||
// 商铺操作:不涉及余额与计费,也可用于不需要事务的读取。
|
||||
[MediatRBehavior(typeof(ShopOwnershipBehavior<,>))]
|
||||
sealed record ShopOnlyRequest(string UserName, string ShopId) : IRequest<string>, IShopOwnershipRequest
|
||||
{
|
||||
public ShopEntity? OwnedShop { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><OutputType>Exe</OutputType></PropertyGroup>
|
||||
<ItemGroup><ProjectReference Include="../../Modules/Store.Users/Store.Users.csproj" /></ItemGroup>
|
||||
</Project>
|
||||
Reference in new issue
Block a user