211 lines
9.8 KiB
C#
211 lines
9.8 KiB
C#
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; }
|
|
} |