From 048f96c6cdd72f2038d44b3f0b9c33f9f60a2048 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:41:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E4=B8=8A=E4=B8=8B=E6=96=87=E5=92=8CCQRS=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现ArchitectureContext提供系统、模型、工具等组件访问管理 - 添加CqrsDispatcher作为GFramework自有CQRS运行时分发器 - 集成Microsoft.Extensions.DependencyInjection作为IoC容器适配器 - 实现完整的命令、查询、事件处理机制 - 支持上下文感知处理器注入架构上下文 - 提供管道行为链处理机制 - 实现流式请求处理功能 - 添加服务实例缓存和优先级排序支持 --- .../Cqrs/ICqrsRuntime.cs | 51 ++++++++ .../GFramework.Core.Abstractions.csproj | 3 + GFramework.Core.Tests/CqrsTestRuntime.cs | 62 +++++++++ .../Ioc/MicrosoftDiContainerTests.cs | 9 +- .../Architectures/ArchitectureContext.cs | 31 ++--- .../Cqrs/Internal/CqrsDispatcher.cs | 122 ++++++++++-------- .../Internal/DefaultCqrsHandlerRegistrar.cs | 26 ++++ GFramework.Core/Ioc/MicrosoftDiContainer.cs | 25 +++- .../Services/Modules/CqrsRuntimeModule.cs | 61 +++++++++ .../Services/ServiceModuleManager.cs | 5 +- .../Cqrs/Command/ICommand.cs | 0 .../Cqrs/Command/ICommandInput.cs | 2 +- .../Cqrs/ICqrsHandlerRegistrar.cs | 17 +++ .../Cqrs/IInput.cs | 2 +- .../Cqrs/INotification.cs | 0 .../Cqrs/INotificationHandler.cs | 0 .../Cqrs/IPipelineBehavior.cs | 0 .../Cqrs/IRequest.cs | 0 .../Cqrs/IRequestHandler.cs | 0 .../Cqrs/IStreamRequest.cs | 0 .../Cqrs/IStreamRequestHandler.cs | 0 .../Cqrs/MessageHandlerDelegate.cs | 0 .../Cqrs/Notification/INotificationInput.cs | 2 +- .../Cqrs/Query/IQuery.cs | 0 .../Cqrs/Query/IQueryInput.cs | 2 +- .../Cqrs/Request/IRequestInput.cs | 2 +- .../Cqrs/Unit.cs | 0 .../Directory.Build.props | 18 +++ .../GFramework.Cqrs.Abstractions.csproj | 11 ++ GFramework.Cqrs.Abstractions/GlobalUsings.cs | 3 + GFramework.Cqrs/GFramework.Cqrs.csproj | 16 +++ GFramework.sln | 28 ++++ 32 files changed, 416 insertions(+), 82 deletions(-) create mode 100644 GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs create mode 100644 GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs create mode 100644 GFramework.Core/Services/Modules/CqrsRuntimeModule.cs rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Command/ICommand.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Command/ICommandInput.cs (84%) create mode 100644 GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IInput.cs (96%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/INotification.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/INotificationHandler.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IPipelineBehavior.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IRequest.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IRequestHandler.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IStreamRequest.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/IStreamRequestHandler.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/MessageHandlerDelegate.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Notification/INotificationInput.cs (94%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Query/IQuery.cs (100%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Query/IQueryInput.cs (79%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Request/IRequestInput.cs (95%) rename {GFramework.Core.Abstractions => GFramework.Cqrs.Abstractions}/Cqrs/Unit.cs (100%) create mode 100644 GFramework.Cqrs.Abstractions/Directory.Build.props create mode 100644 GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj create mode 100644 GFramework.Cqrs.Abstractions/GlobalUsings.cs create mode 100644 GFramework.Cqrs/GFramework.Cqrs.csproj diff --git a/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs new file mode 100644 index 00000000..e26310d7 --- /dev/null +++ b/GFramework.Core.Abstractions/Cqrs/ICqrsRuntime.cs @@ -0,0 +1,51 @@ +using GFramework.Core.Abstractions.Architectures; + +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 定义架构上下文使用的 CQRS runtime seam。 +/// 该抽象把请求分发、通知发布与流式处理从具体实现中解耦, +/// 使 不再直接依赖某个固定的 runtime 类型。 +/// +public interface ICqrsRuntime +{ + /// + /// 发送请求并返回响应。 + /// + /// 响应类型。 + /// 当前架构上下文,用于上下文感知处理器注入与嵌套请求访问。 + /// 要分发的请求。 + /// 取消令牌。 + /// 请求响应。 + ValueTask SendAsync( + IArchitectureContext context, + IRequest request, + CancellationToken cancellationToken = default); + + /// + /// 发布通知到所有已注册处理器。 + /// + /// 通知类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 + /// 要发布的通知。 + /// 取消令牌。 + /// 表示通知分发完成的值任务。 + ValueTask PublishAsync( + IArchitectureContext context, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification; + + /// + /// 创建流式请求的异步响应序列。 + /// + /// 流元素类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 + /// 流式请求。 + /// 取消令牌。 + /// 按需生成的异步响应序列。 + IAsyncEnumerable CreateStream( + IArchitectureContext context, + IStreamRequest request, + CancellationToken cancellationToken = default); +} diff --git a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj index 84d53f63..7a70b332 100644 --- a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj +++ b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj @@ -17,6 +17,9 @@ + + + all diff --git a/GFramework.Core.Tests/CqrsTestRuntime.cs b/GFramework.Core.Tests/CqrsTestRuntime.cs index e9664925..f2f64f9b 100644 --- a/GFramework.Core.Tests/CqrsTestRuntime.cs +++ b/GFramework.Core.Tests/CqrsTestRuntime.cs @@ -1,4 +1,5 @@ using System.Reflection; +using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Architectures; @@ -38,6 +39,65 @@ internal static class CqrsTestRuntime ?? throw new InvalidOperationException( "Failed to locate CqrsHandlerRegistrar.RegisterHandlers."); + private static readonly Type CqrsDispatcherType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.CqrsDispatcher", + throwOnError: true)! + ?? throw new InvalidOperationException( + "Failed to locate CqrsDispatcher type."); + + private static readonly ConstructorInfo CqrsDispatcherConstructor = CqrsDispatcherType.GetConstructor( + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic, + binder: null, + [ + typeof(IIocContainer), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate CqrsDispatcher constructor."); + + private static readonly Type DefaultCqrsHandlerRegistrarType = typeof(ArchitectureContext).Assembly + .GetType( + "GFramework.Core.Cqrs.Internal.DefaultCqrsHandlerRegistrar", + throwOnError: true)! + ?? throw new InvalidOperationException( + "Failed to locate DefaultCqrsHandlerRegistrar type."); + + private static readonly ConstructorInfo DefaultCqrsHandlerRegistrarConstructor = + DefaultCqrsHandlerRegistrarType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + [ + typeof(IIocContainer), + typeof(ILogger) + ], + modifiers: null) + ?? throw new InvalidOperationException( + "Failed to locate DefaultCqrsHandlerRegistrar constructor."); + + /// + /// 为裸测试容器补齐默认 CQRS runtime seam。 + /// 这使仅使用 的测试环境也能观察与生产路径一致的 runtime 行为, + /// 而无需完整启动服务模块管理器。 + /// + /// 目标测试容器。 + internal static void RegisterInfrastructure(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); + + var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); + var runtime = (ICqrsRuntime)CqrsDispatcherConstructor.Invoke([container, runtimeLogger]); + var registrar = + (ICqrsHandlerRegistrar)DefaultCqrsHandlerRegistrarConstructor.Invoke([container, registrarLogger]); + + container.Register(runtime); + container.Register(registrar); + } + /// /// 通过与生产代码一致的注册入口扫描并注册指定程序集中的 CQRS 处理器。 /// @@ -48,6 +108,8 @@ internal static class CqrsTestRuntime ArgumentNullException.ThrowIfNull(container); ArgumentNullException.ThrowIfNull(assemblies); + RegisterInfrastructure(container); + var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsTestRuntime)); RegisterHandlersMethod.Invoke( null, diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index b4e5af67..7126a3ad 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -1,6 +1,5 @@ using System.Reflection; using GFramework.Core.Abstractions.Bases; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Core.Tests.Cqrs; @@ -14,6 +13,8 @@ namespace GFramework.Core.Tests.Ioc; [TestFixture] public class MicrosoftDiContainerTests { + private MicrosoftDiContainer _container = null!; + /// /// 在每个测试方法执行前进行设置 /// @@ -29,9 +30,9 @@ public class MicrosoftDiContainerTests BindingFlags.NonPublic | BindingFlags.Instance); loggerField?.SetValue(_container, LoggerFactoryResolver.Provider.CreateLogger(nameof(MicrosoftDiContainer))); - } - private MicrosoftDiContainer _container = null!; + CqrsTestRuntime.RegisterInfrastructure(_container); + } /// /// 测试注册单例实例的功能 @@ -328,6 +329,8 @@ public class MicrosoftDiContainerTests descriptor.ServiceType == typeof(INotificationHandler)), Is.False); + // Clear 会移除测试手工补齐的 CQRS seam,需要先恢复基础设施再验证程序集去重状态是否已重置。 + CqrsTestRuntime.RegisterInfrastructure(_container); _container.RegisterCqrsHandlersFromAssembly(assembly); Assert.That( diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index 77c04fcf..63bb34e8 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -2,18 +2,13 @@ using System.Collections.Concurrent; using GFramework.Core.Abstractions.Architectures; using GFramework.Core.Abstractions.Command; using GFramework.Core.Abstractions.Cqrs; -using GFramework.Core.Abstractions.Cqrs.Command; -using GFramework.Core.Abstractions.Cqrs.Query; using GFramework.Core.Abstractions.Environment; using GFramework.Core.Abstractions.Events; using GFramework.Core.Abstractions.Ioc; -using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Model; using GFramework.Core.Abstractions.Query; using GFramework.Core.Abstractions.Systems; using GFramework.Core.Abstractions.Utility; -using GFramework.Core.Cqrs.Internal; -using GFramework.Core.Logging; using ICommand = GFramework.Core.Abstractions.Command.ICommand; namespace GFramework.Core.Architectures; @@ -25,15 +20,15 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext { private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container)); private readonly ConcurrentDictionary _serviceCache = new(); - private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ArchitectureContext)); - private CqrsDispatcher? _cqrsDispatcher; + private ICqrsRuntime? _cqrsRuntime; #region CQRS Integration /// - /// 获取 CQRS 运行时分发器(延迟初始化)。 + /// 获取 CQRS runtime seam(延迟初始化)。 /// - private CqrsDispatcher CqrsDispatcher => _cqrsDispatcher ??= new CqrsDispatcher(_container, this, _logger); + private ICqrsRuntime CqrsRuntime => _cqrsRuntime ??= + _container.Get() ?? throw new InvalidOperationException("ICqrsRuntime not registered"); /// /// 获取指定类型的服务实例,如果缓存中存在则直接返回,否则从容器中获取并缓存 @@ -73,7 +68,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return await CqrsDispatcher.SendAsync(request, cancellationToken); + return await CqrsRuntime.SendAsync(this, request, cancellationToken); } /// @@ -100,7 +95,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext where TNotification : INotification { ArgumentNullException.ThrowIfNull(notification); - await CqrsDispatcher.PublishAsync(notification, cancellationToken); + await CqrsRuntime.PublishAsync(this, notification, cancellationToken); } /// @@ -115,7 +110,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return CqrsDispatcher.CreateStream(request, cancellationToken); + return CqrsRuntime.CreateStream(this, request, cancellationToken); } /// @@ -151,7 +146,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 查询结果类型 /// 要发送的查询 /// 查询结果 - public TResult SendQuery(Abstractions.Query.IQuery query) + public TResult SendQuery(IQuery query) { if (query == null) throw new ArgumentNullException(nameof(query)); var queryBus = GetOrCache(); @@ -165,7 +160,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 查询响应类型 /// 要发送的查询对象 /// 查询结果 - public TResponse SendQuery(GFramework.Core.Abstractions.Cqrs.Query.IQuery query) + public TResponse SendQuery(Abstractions.Cqrs.Query.IQuery query) { return SendQueryAsync(query).AsTask().GetAwaiter().GetResult(); } @@ -191,7 +186,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 要发送的查询对象 /// 取消令牌,用于取消操作 /// 包含查询结果的ValueTask - public async ValueTask SendQueryAsync(GFramework.Core.Abstractions.Cqrs.Query.IQuery query, + public async ValueTask SendQueryAsync(Abstractions.Cqrs.Query.IQuery query, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(query); @@ -327,7 +322,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 要发送的命令对象 /// 取消令牌,用于取消操作 /// 包含命令执行结果的ValueTask - public async ValueTask SendCommandAsync(GFramework.Core.Abstractions.Cqrs.Command.ICommand command, + public async ValueTask SendCommandAsync(Abstractions.Cqrs.Command.ICommand command, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); @@ -366,7 +361,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 命令响应类型 /// 要发送的命令对象 /// 命令执行结果 - public TResponse SendCommand(GFramework.Core.Abstractions.Cqrs.Command.ICommand command) + public TResponse SendCommand(Abstractions.Cqrs.Command.ICommand command) { return SendCommandAsync(command).AsTask().GetAwaiter().GetResult(); } @@ -388,7 +383,7 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// 命令执行结果类型 /// 要发送的命令 /// 命令执行结果 - public TResult SendCommand(Abstractions.Command.ICommand command) + public TResult SendCommand(ICommand command) { ArgumentNullException.ThrowIfNull(command); var commandBus = GetOrCache(); diff --git a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs index 69f6794d..f1950cf7 100644 --- a/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Core/Cqrs/Internal/CqrsDispatcher.cs @@ -14,34 +14,70 @@ namespace GFramework.Core.Cqrs.Internal; /// internal sealed class CqrsDispatcher( IIocContainer container, - IArchitectureContext context, - ILogger logger) + ILogger logger) : ICqrsRuntime { - private delegate ValueTask RequestInvoker(object handler, object request, CancellationToken cancellationToken); - private delegate ValueTask RequestPipelineInvoker( - object handler, - IReadOnlyList behaviors, - object request, - CancellationToken cancellationToken); - private delegate ValueTask NotificationInvoker(object handler, object notification, CancellationToken cancellationToken); - private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestInvoker> + RequestInvokers = new(); + + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestPipelineInvoker> + RequestPipelineInvokers = new(); - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestInvoker> RequestInvokers = new(); - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), RequestPipelineInvoker> RequestPipelineInvokers = new(); private static readonly ConcurrentDictionary NotificationInvokers = new(); - private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = new(); + + private static readonly ConcurrentDictionary<(Type RequestType, Type ResponseType), StreamInvoker> StreamInvokers = + new(); + + /// + /// 发布通知到所有已注册处理器。 + /// + /// 通知类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 + /// 通知对象。 + /// 取消令牌。 + public async ValueTask PublishAsync( + IArchitectureContext context, + TNotification notification, + CancellationToken cancellationToken = default) + where TNotification : INotification + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(notification); + + var notificationType = notification.GetType(); + var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType); + var handlers = container.GetAll(handlerType); + + if (handlers.Count == 0) + { + logger.Debug($"No CQRS notification handler registered for {notificationType.FullName}."); + return; + } + + var invoker = NotificationInvokers.GetOrAdd( + notificationType, + CreateNotificationInvoker); + + foreach (var handler in handlers) + { + PrepareHandler(handler, context); + await invoker(handler, notification, cancellationToken); + } + } /// /// 发送请求并返回结果。 /// /// 响应类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 /// 请求对象。 /// 取消令牌。 /// 请求响应。 public async ValueTask SendAsync( + IArchitectureContext context, IRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); @@ -50,12 +86,12 @@ internal sealed class CqrsDispatcher( ?? throw new InvalidOperationException( $"No CQRS request handler registered for {requestType.FullName}."); - PrepareHandler(handler); + PrepareHandler(handler, context); var behaviorType = typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)); var behaviors = container.GetAll(behaviorType); foreach (var behavior in behaviors) - PrepareHandler(behavior); + PrepareHandler(behavior, context); if (behaviors.Count == 0) { @@ -75,51 +111,20 @@ internal sealed class CqrsDispatcher( return pipelineResult is null ? default! : (TResponse)pipelineResult; } - /// - /// 发布通知到所有已注册处理器。 - /// - /// 通知类型。 - /// 通知对象。 - /// 取消令牌。 - public async ValueTask PublishAsync( - TNotification notification, - CancellationToken cancellationToken = default) - where TNotification : INotification - { - ArgumentNullException.ThrowIfNull(notification); - - var notificationType = notification.GetType(); - var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType); - var handlers = container.GetAll(handlerType); - - if (handlers.Count == 0) - { - logger.Debug($"No CQRS notification handler registered for {notificationType.FullName}."); - return; - } - - var invoker = NotificationInvokers.GetOrAdd( - notificationType, - CreateNotificationInvoker); - - foreach (var handler in handlers) - { - PrepareHandler(handler); - await invoker(handler, notification, cancellationToken); - } - } - /// /// 创建流式请求并返回异步响应序列。 /// /// 响应元素类型。 + /// 当前架构上下文,用于上下文感知处理器注入。 /// 流式请求对象。 /// 取消令牌。 /// 异步响应序列。 public IAsyncEnumerable CreateStream( + IArchitectureContext context, IStreamRequest request, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(request); var requestType = request.GetType(); @@ -128,7 +133,7 @@ internal sealed class CqrsDispatcher( ?? throw new InvalidOperationException( $"No CQRS stream handler registered for {requestType.FullName}."); - PrepareHandler(handler); + PrepareHandler(handler, context); var invoker = StreamInvokers.GetOrAdd( (requestType, typeof(TResponse)), @@ -141,7 +146,8 @@ internal sealed class CqrsDispatcher( /// 为上下文感知处理器注入当前架构上下文。 /// /// 处理器实例。 - private void PrepareHandler(object handler) + /// 当前架构上下文。 + private static void PrepareHandler(object handler, IArchitectureContext context) { if (handler is IContextAware contextAware) contextAware.SetContext(context); @@ -260,4 +266,18 @@ internal sealed class CqrsDispatcher( var typedRequest = (TRequest)request; return typedHandler.Handle(typedRequest, cancellationToken); } + + private delegate ValueTask RequestInvoker(object handler, object request, + CancellationToken cancellationToken); + + private delegate ValueTask RequestPipelineInvoker( + object handler, + IReadOnlyList behaviors, + object request, + CancellationToken cancellationToken); + + private delegate ValueTask NotificationInvoker(object handler, object notification, + CancellationToken cancellationToken); + + private delegate object StreamInvoker(object handler, object request, CancellationToken cancellationToken); } diff --git a/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs new file mode 100644 index 00000000..5d59f8a6 --- /dev/null +++ b/GFramework.Core/Cqrs/Internal/DefaultCqrsHandlerRegistrar.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Abstractions.Logging; + +namespace GFramework.Core.Cqrs.Internal; + +/// +/// 默认的 CQRS 处理器注册器实现。 +/// 该适配器把容器公开的 handler 接入入口转发到现有的注册流水线, +/// 使容器主路径只依赖 抽象。 +/// +internal sealed class DefaultCqrsHandlerRegistrar(IIocContainer container, ILogger logger) : ICqrsHandlerRegistrar +{ + private readonly IIocContainer _container = container ?? throw new ArgumentNullException(nameof(container)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// + /// 按当前 runtime 约定扫描并注册处理器程序集。 + /// + /// 要接入的程序集集合。 + public void RegisterHandlers(IEnumerable assemblies) + { + ArgumentNullException.ThrowIfNull(assemblies); + CqrsHandlerRegistrar.RegisterHandlers(_container, assemblies, _logger); + } +} diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 712a41c1..c094647d 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -1,11 +1,9 @@ using System.ComponentModel; using System.Reflection; using GFramework.Core.Abstractions.Bases; -using GFramework.Core.Abstractions.Cqrs; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Systems; -using GFramework.Core.Cqrs.Internal; using GFramework.Core.Logging; using GFramework.Core.Rule; @@ -424,7 +422,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) continue; } - CqrsHandlerRegistrar.RegisterHandlers(this, [assembly], _logger); + ResolveCqrsHandlerRegistrar().RegisterHandlers([assembly]); _registeredCqrsHandlerAssemblyKeys.Add(assemblyKey); } } @@ -456,6 +454,27 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) #region Get + /// + /// 获取当前容器中已注册的 CQRS 处理器注册器。 + /// 该方法仅供容器内部在注册阶段使用,因此直接读取服务描述符中的实例绑定, + /// 避免在容器未冻结前依赖完整的服务提供者构建流程。 + /// + /// 已注册的 CQRS 处理器注册器实例。 + /// 未找到可用的 CQRS 处理器注册器实例时抛出。 + private ICqrsHandlerRegistrar ResolveCqrsHandlerRegistrar() + { + var descriptor = GetServicesUnsafe.LastOrDefault(static service => + service.ServiceType == typeof(ICqrsHandlerRegistrar)); + + if (descriptor?.ImplementationInstance is ICqrsHandlerRegistrar registrar) + return registrar; + + const string errorMessage = + "ICqrsHandlerRegistrar not registered. Ensure the CQRS runtime module has been installed before registering handlers."; + _logger.Error(errorMessage); + throw new InvalidOperationException(errorMessage); + } + /// /// 获取指定泛型类型的服务实例 /// 返回第一个匹配的注册实例,如果不存在则返回null diff --git a/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs new file mode 100644 index 00000000..915e07e2 --- /dev/null +++ b/GFramework.Core/Services/Modules/CqrsRuntimeModule.cs @@ -0,0 +1,61 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Core.Abstractions.Cqrs; +using GFramework.Core.Abstractions.Ioc; +using GFramework.Core.Cqrs.Internal; +using GFramework.Core.Logging; + +namespace GFramework.Core.Services.Modules; + +/// +/// CQRS runtime 模块,用于把默认请求分发器与处理器注册器接入架构容器。 +/// 该模块在架构初始化早期完成注册,保证用户初始化阶段即可使用 CQRS 入口与 handler 自动接入能力。 +/// +public sealed class CqrsRuntimeModule : IServiceModule +{ + /// + /// 获取模块名称。 + /// + public string ModuleName => nameof(CqrsRuntimeModule); + + /// + /// 获取模块优先级。 + /// CQRS runtime 需要先于架构默认 handler 扫描路径可用,因此放在基础总线模块之后、用户初始化之前注册。 + /// + public int Priority => 15; + + /// + /// 获取模块启用状态,默认启用。 + /// + public bool IsEnabled => true; + + /// + /// 注册默认 CQRS runtime seam 实现。 + /// + /// 目标依赖注入容器。 + public void Register(IIocContainer container) + { + ArgumentNullException.ThrowIfNull(container); + + var dispatcherLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsDispatcher)); + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger(nameof(DefaultCqrsHandlerRegistrar)); + + container.Register(new CqrsDispatcher(container, dispatcherLogger)); + container.Register(new DefaultCqrsHandlerRegistrar(container, registrarLogger)); + } + + /// + /// 初始化模块。 + /// + public void Initialize() + { + } + + /// + /// 异步销毁模块。 + /// + /// 已完成的值任务。 + public ValueTask DestroyAsync() + { + return ValueTask.CompletedTask; + } +} diff --git a/GFramework.Core/Services/ServiceModuleManager.cs b/GFramework.Core/Services/ServiceModuleManager.cs index d07c128b..a3965f4d 100644 --- a/GFramework.Core/Services/ServiceModuleManager.cs +++ b/GFramework.Core/Services/ServiceModuleManager.cs @@ -42,7 +42,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager /// /// 注册内置服务模块,并根据优先级排序后完成服务注册。 - /// 内置模块包括事件总线、命令执行器、查询执行器等核心模块。 + /// 内置模块包括事件总线、命令执行器、CQRS runtime、查询执行器等核心模块。 /// 同时注册通过 ArchitectureModuleRegistry 自动注册的外部模块。 /// /// IoC容器实例,用于模块服务注册。 @@ -57,6 +57,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager // 注册内置模块 RegisterModule(new EventBusModule()); RegisterModule(new CommandExecutorModule()); + RegisterModule(new CqrsRuntimeModule()); RegisterModule(new QueryExecutorModule()); RegisterModule(new AsyncQueryExecutorModule()); @@ -148,4 +149,4 @@ public sealed class ServiceModuleManager : IServiceModuleManager _builtInModulesRegistered = false; _logger.Info("All service modules destroyed"); } -} \ No newline at end of file +} diff --git a/GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/Command/ICommand.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Command/ICommand.cs diff --git a/GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs similarity index 84% rename from GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs index 5ec607e5..a00d67e6 100644 --- a/GFramework.Core.Abstractions/Cqrs/Command/ICommandInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Command/ICommandInput.cs @@ -4,4 +4,4 @@ /// 命令输入接口,定义命令模式中输入数据的契约 /// 该接口作为标记接口使用,不包含任何成员定义 /// -public interface ICommandInput : IInput; \ No newline at end of file +public interface ICommandInput : IInput; diff --git a/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs new file mode 100644 index 00000000..0257b3ec --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Cqrs/ICqrsHandlerRegistrar.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace GFramework.Core.Abstractions.Cqrs; + +/// +/// 定义 CQRS 处理器程序集接入的 runtime seam。 +/// 该抽象负责承接“生成注册器优先、反射扫描回退”的处理器注册流程, +/// 让容器与架构启动链不再直接依赖固定的注册实现类型。 +/// +public interface ICqrsHandlerRegistrar +{ + /// + /// 扫描并注册指定程序集集合中的 CQRS 处理器。 + /// + /// 要接入的程序集集合。 + void RegisterHandlers(IEnumerable assemblies); +} diff --git a/GFramework.Core.Abstractions/Cqrs/IInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs similarity index 96% rename from GFramework.Core.Abstractions/Cqrs/IInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IInput.cs index dfed5012..ddf00622 100644 --- a/GFramework.Core.Abstractions/Cqrs/IInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/IInput.cs @@ -17,4 +17,4 @@ namespace GFramework.Core.Abstractions.Cqrs; /// 表示输入数据的标记接口。 /// 该接口用于标识各类CQRS模式中的输入参数类型。 /// -public interface IInput; \ No newline at end of file +public interface IInput; diff --git a/GFramework.Core.Abstractions/Cqrs/INotification.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotification.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/INotification.cs rename to GFramework.Cqrs.Abstractions/Cqrs/INotification.cs diff --git a/GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/INotificationHandler.cs rename to GFramework.Cqrs.Abstractions/Cqrs/INotificationHandler.cs diff --git a/GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs b/GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/IPipelineBehavior.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IPipelineBehavior.cs diff --git a/GFramework.Core.Abstractions/Cqrs/IRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/IRequest.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IRequest.cs diff --git a/GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/IRequestHandler.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IRequestHandler.cs diff --git a/GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/IStreamRequest.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IStreamRequest.cs diff --git a/GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs b/GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/IStreamRequestHandler.cs rename to GFramework.Cqrs.Abstractions/Cqrs/IStreamRequestHandler.cs diff --git a/GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs b/GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/MessageHandlerDelegate.cs rename to GFramework.Cqrs.Abstractions/Cqrs/MessageHandlerDelegate.cs diff --git a/GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs similarity index 94% rename from GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs index 8b791839..236b30ff 100644 --- a/GFramework.Core.Abstractions/Cqrs/Notification/INotificationInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Notification/INotificationInput.cs @@ -17,4 +17,4 @@ namespace GFramework.Core.Abstractions.Cqrs.Notification; /// 表示通知输入数据的标记接口。 /// 该接口继承自 IInput,用于标识CQRS模式中通知类型的输入参数。 /// -public interface INotificationInput : IInput; \ No newline at end of file +public interface INotificationInput : IInput; diff --git a/GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/Query/IQuery.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Query/IQuery.cs diff --git a/GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs similarity index 79% rename from GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs index c505c4ff..7e0a5b4f 100644 --- a/GFramework.Core.Abstractions/Cqrs/Query/IQueryInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Query/IQueryInput.cs @@ -3,4 +3,4 @@ /// /// 查询输入接口,定义了查询操作的输入规范 /// -public interface IQueryInput : IInput; \ No newline at end of file +public interface IQueryInput : IInput; diff --git a/GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs b/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs similarity index 95% rename from GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs index 0a0b1591..7b7ff83c 100644 --- a/GFramework.Core.Abstractions/Cqrs/Request/IRequestInput.cs +++ b/GFramework.Cqrs.Abstractions/Cqrs/Request/IRequestInput.cs @@ -17,4 +17,4 @@ namespace GFramework.Core.Abstractions.Cqrs.Request; /// 表示请求输入数据的标记接口。 /// 该接口继承自 IInput,用于标识CQRS模式中请求类型的输入参数。 /// -public interface IRequestInput : IInput; \ No newline at end of file +public interface IRequestInput : IInput; diff --git a/GFramework.Core.Abstractions/Cqrs/Unit.cs b/GFramework.Cqrs.Abstractions/Cqrs/Unit.cs similarity index 100% rename from GFramework.Core.Abstractions/Cqrs/Unit.cs rename to GFramework.Cqrs.Abstractions/Cqrs/Unit.cs diff --git a/GFramework.Cqrs.Abstractions/Directory.Build.props b/GFramework.Cqrs.Abstractions/Directory.Build.props new file mode 100644 index 00000000..8febdc42 --- /dev/null +++ b/GFramework.Cqrs.Abstractions/Directory.Build.props @@ -0,0 +1,18 @@ + + + netstandard2.1 + true + true + preview + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj b/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj new file mode 100644 index 00000000..8e07fd2c --- /dev/null +++ b/GFramework.Cqrs.Abstractions/GFramework.Cqrs.Abstractions.csproj @@ -0,0 +1,11 @@ + + + + GeWuYou.$(AssemblyName) + true + T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute + enable + true + + + diff --git a/GFramework.Cqrs.Abstractions/GlobalUsings.cs b/GFramework.Cqrs.Abstractions/GlobalUsings.cs new file mode 100644 index 00000000..5cd04a4e --- /dev/null +++ b/GFramework.Cqrs.Abstractions/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/GFramework.Cqrs/GFramework.Cqrs.csproj b/GFramework.Cqrs/GFramework.Cqrs.csproj new file mode 100644 index 00000000..9f002283 --- /dev/null +++ b/GFramework.Cqrs/GFramework.Cqrs.csproj @@ -0,0 +1,16 @@ + + + + GeWuYou.$(AssemblyName) + net8.0;net9.0;net10.0 + disable + enable + true + true + + + + + + + diff --git a/GFramework.sln b/GFramework.sln index 6d4eb45f..67d53c69 100644 --- a/GFramework.sln +++ b/GFramework.sln @@ -38,6 +38,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGene EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.Tests", "GFramework.Godot.Tests\GFramework.Godot.Tests.csproj", "{576119E2-13D0-4ACF-A012-D01C320E8BF3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Abstractions", "GFramework.Cqrs.Abstractions\GFramework.Cqrs.Abstractions.csproj", "{69C06523-98AA-49DE-95D4-4BF203716DD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs", "GFramework.Cqrs\GFramework.Cqrs.csproj", "{E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -276,6 +280,30 @@ Global {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x64.Build.0 = Release|Any CPU {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.ActiveCfg = Release|Any CPU {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x64.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x64.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x86.ActiveCfg = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Debug|x86.Build.0 = Debug|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|Any CPU.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x64.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x64.Build.0 = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x86.ActiveCfg = Release|Any CPU + {69C06523-98AA-49DE-95D4-4BF203716DD2}.Release|x86.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x64.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Debug|x86.Build.0 = Debug|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|Any CPU.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x64.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x64.Build.0 = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.ActiveCfg = Release|Any CPU + {E7034F34-0D2B-4D99-B8E2-D149EF6C88F2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE