增加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,7 @@
|
||||
namespace MAX.Core.Mediation;
|
||||
|
||||
/// <summary>提供请求自身的参数校验;不合法时抛出 ValidationException。</summary>
|
||||
public interface IValidatableRequest
|
||||
{
|
||||
void Validate();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Adapted from ITIXO/MediatR.Extensions.AttributedBehaviors (MIT).
|
||||
// Copyright (c) 2021 ITIXO s.r.o. See ThirdPartyNotices/MediatR.Extensions.AttributedBehaviors.LICENSE.txt.
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MAX.Core.Mediation;
|
||||
|
||||
/// <summary>声明命令管道。每个特性独占一行,按源码行号从上到下执行。</summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)]
|
||||
public sealed class MediatRBehaviorAttribute(Type behaviorType, [CallerLineNumber] int lineNumber = 0) : Attribute
|
||||
{
|
||||
public Type BehaviorType { get; } = behaviorType ?? throw new ArgumentNullException(nameof(behaviorType));
|
||||
public int LineNumber { get; } = lineNumber;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using MAX.Framework.Mediation;
|
||||
// Adapted from ITIXO/MediatR.Extensions.AttributedBehaviors (MIT).
|
||||
// Copyright (c) 2021 ITIXO s.r.o. See ThirdPartyNotices/MediatR.Extensions.AttributedBehaviors.LICENSE.txt.
|
||||
using System.Reflection;
|
||||
using MAX.Core.Mediation;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace MAX.Framework.DependencyInjection;
|
||||
|
||||
public static class MediatRBehaviorServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>扫描命令特性,按声明行号注册闭合管道,适配 MediatR 9。</summary>
|
||||
public static IServiceCollection AddMediatRAttributedBehaviors(this IServiceCollection services,
|
||||
params Assembly[] assemblies)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(assemblies);
|
||||
// 先完成全部校验,避免无效配置留下部分注册。
|
||||
var registrations = new List<ServiceDescriptor>();
|
||||
foreach (var request in assemblies.Distinct().SelectMany(a => a.DefinedTypes))
|
||||
{
|
||||
var attributes = request.GetCustomAttributes<MediatRBehaviorAttribute>(false)
|
||||
.OrderBy(a => a.LineNumber).ToArray();
|
||||
if (attributes.Length == 0) continue;
|
||||
if (request.IsAbstract || request.ContainsGenericParameters)
|
||||
throw new InvalidOperationException($"{request.FullName} 必须是具体的闭合请求类型。");
|
||||
var responses = request.GetInterfaces()
|
||||
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRequest<>))
|
||||
.Select(i => i.GetGenericArguments()[0]).Distinct().ToArray();
|
||||
if (responses.Length != 1)
|
||||
throw new InvalidOperationException($"{request.FullName} 必须实现唯一的 IRequest<TResponse>(无返回值使用 IRequest)。");
|
||||
if (attributes.GroupBy(a => a.LineNumber).Any(g => g.Count() > 1))
|
||||
throw new InvalidOperationException($"{request.FullName} 的管道特性必须独占一行,不能使用相同的行号。");
|
||||
if (attributes.GroupBy(a => a.BehaviorType).Any(g => g.Count() > 1))
|
||||
throw new InvalidOperationException($"{request.FullName} 重复声明了同一个管道。");
|
||||
|
||||
var serviceType = typeof(IPipelineBehavior<,>).MakeGenericType(request.AsType(), responses[0]);
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
var behavior = attribute.BehaviorType;
|
||||
if (behavior.IsGenericTypeDefinition)
|
||||
{
|
||||
if (behavior.GetGenericArguments().Length != 2)
|
||||
throw new InvalidOperationException($"开放泛型管道 {behavior} 必须具有 TRequest、TResponse 两个泛型参数。");
|
||||
try
|
||||
{
|
||||
behavior = behavior.MakeGenericType(request.AsType(), responses[0]);
|
||||
}
|
||||
catch (ArgumentException exception)
|
||||
{
|
||||
throw new InvalidOperationException($"管道 {behavior} 的泛型约束与请求 {request.FullName} 不匹配。", exception);
|
||||
}
|
||||
}
|
||||
if (!behavior.IsClass || behavior.IsAbstract || behavior.ContainsGenericParameters
|
||||
|| !serviceType.IsAssignableFrom(behavior))
|
||||
throw new InvalidOperationException($"管道 {behavior.FullName} 必须是实现 {serviceType} 的具体闭合类型。");
|
||||
registrations.Add(ServiceDescriptor.Transient(serviceType, behavior));
|
||||
}
|
||||
}
|
||||
services.TryAddSingleton<UnitOfWorkExecutionContext>();
|
||||
services.TryAddTransient<ServiceFactory>(provider => provider.GetRequiredService);
|
||||
services.TryAddTransient<IMediator, Mediator>();
|
||||
services.TryAddTransient<ISender>(provider => provider.GetRequiredService<IMediator>());
|
||||
services.TryAddTransient<IPublisher>(provider => provider.GetRequiredService<IMediator>());
|
||||
foreach (var registration in registrations)
|
||||
services.TryAddEnumerable(registration);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ namespace MAX.Framework.DependencyInjection
|
||||
RunDebugStep("Plugin", "注册全部插件", () => RunPlugins(host, assemblies, instances));
|
||||
|
||||
RunDebugStep("Startup", "初始化并注册启动服务", () => RunStartups(host.Services, assemblies, instances));
|
||||
RunDebugStep("MediatR", "按命令特性注册管道", () => host.Services.AddMediatRAttributedBehaviors(assemblies.ToArray()));
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using FreeSql;
|
||||
using MediatR;
|
||||
|
||||
namespace MAX.Framework.Mediation;
|
||||
|
||||
/// <summary>为通过特性选择的命令管理事务;所有读写使用执行上下文中的 ORM。</summary>
|
||||
public sealed class TransactionBehavior<TRequest, TResponse>(
|
||||
IFreeSql freeSql, UnitOfWorkExecutionContext context) : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
|
||||
RequestHandlerDelegate<TResponse> next)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (context.IsActive)
|
||||
throw new InvalidOperationException("不支持嵌套事务命令,请在同一个命令处理器内完成事务操作");
|
||||
using var work = freeSql.CreateUnitOfWork();
|
||||
work.GetOrBeginTransaction();
|
||||
using var execution = context.Enter(work.Orm);
|
||||
var response = await next();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
work.Commit();
|
||||
return response;
|
||||
// 未提交时,工作单元释放自动回滚;执行上下文同时清理。
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace MAX.Framework.Mediation;
|
||||
|
||||
/// <summary>沿当前异步调用链传递事务 ORM,隔离并行命令的执行状态。</summary>
|
||||
public sealed class UnitOfWorkExecutionContext
|
||||
{
|
||||
private readonly AsyncLocal<IFreeSql?> _current = new();
|
||||
|
||||
public IFreeSql Orm => _current.Value
|
||||
?? throw new InvalidOperationException("当前操作必须在事务管道内执行");
|
||||
|
||||
public bool IsActive => _current.Value != null;
|
||||
|
||||
internal IDisposable Enter(IFreeSql orm)
|
||||
{
|
||||
if (IsActive)
|
||||
throw new InvalidOperationException("不支持嵌套事务命令,请在同一个命令处理器内完成事务操作");
|
||||
_current.Value = orm;
|
||||
return new ExecutionScope(this);
|
||||
}
|
||||
|
||||
private sealed class ExecutionScope(UnitOfWorkExecutionContext context) : IDisposable
|
||||
{
|
||||
public void Dispose() => context._current.Value = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using MAX.Core.Mediation;
|
||||
using MediatR;
|
||||
|
||||
namespace MAX.Framework.Mediation;
|
||||
|
||||
/// <summary>执行请求自身的参数校验,验证通过后才进入后续管道。</summary>
|
||||
public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>, IValidatableRequest
|
||||
{
|
||||
public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
|
||||
RequestHandlerDelegate<TResponse> next)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
request.Validate();
|
||||
return next();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user