diff --git a/GFramework.Core.Abstractions/architecture/IArchitectureContext.cs b/GFramework.Core.Abstractions/architecture/IArchitectureContext.cs index 01b121d..5b0f1f7 100644 --- a/GFramework.Core.Abstractions/architecture/IArchitectureContext.cs +++ b/GFramework.Core.Abstractions/architecture/IArchitectureContext.cs @@ -127,6 +127,44 @@ public interface IArchitectureContext /// TResponse SendRequest(IRequest request); + /// + /// [Mediator] 异步发送命令并返回结果 + /// 通过Mediator模式发送命令请求,支持取消操作 + /// + /// 命令响应类型 + /// 要发送的命令对象 + /// 取消令牌,用于取消操作 + /// 包含命令执行结果的ValueTask + ValueTask SendCommandAsync(Mediator.ICommand command, + CancellationToken cancellationToken = default); + + /// + /// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性) + /// + /// 命令响应类型 + /// 要发送的命令对象 + /// 命令执行结果 + TResponse SendCommand(Mediator.ICommand command); + + /// + /// [Mediator] 异步发送查询并返回结果 + /// 通过Mediator模式发送查询请求,支持取消操作 + /// + /// 查询响应类型 + /// 要发送的查询对象 + /// 取消令牌,用于取消操作 + /// 包含查询结果的ValueTask + ValueTask SendQueryAsync(Mediator.IQuery command, + CancellationToken cancellationToken = default); + + /// + /// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性) + /// + /// 查询响应类型 + /// 要发送的查询对象 + /// 查询结果 + TResponse SendQuery(Mediator.IQuery command); + /// /// 发布通知(一对多事件) /// diff --git a/GFramework.Core.Tests/GFramework.Core.Tests.csproj b/GFramework.Core.Tests/GFramework.Core.Tests.csproj index af6aebe..4183a79 100644 --- a/GFramework.Core.Tests/GFramework.Core.Tests.csproj +++ b/GFramework.Core.Tests/GFramework.Core.Tests.csproj @@ -6,6 +6,11 @@ net10.0;net8.0 + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/GFramework.Core.Tests/architecture/ArchitectureContextTests.cs b/GFramework.Core.Tests/architecture/ArchitectureContextTests.cs index e00fd86..1fc270c 100644 --- a/GFramework.Core.Tests/architecture/ArchitectureContextTests.cs +++ b/GFramework.Core.Tests/architecture/ArchitectureContextTests.cs @@ -128,7 +128,8 @@ public class ArchitectureContextTests [Test] public void SendQuery_Should_ThrowArgumentNullException_When_Query_IsNull() { - Assert.That(() => _context!.SendQuery(null!), + // 明确指定调用旧的 IQuery 重载 + Assert.That(() => _context!.SendQuery((Mediator.IQuery)null!), Throws.ArgumentNullException.With.Property("ParamName").EqualTo("query")); } diff --git a/GFramework.Core.Tests/architecture/ArchitectureServicesTests.cs b/GFramework.Core.Tests/architecture/ArchitectureServicesTests.cs index 7c1d2ac..ccc30dd 100644 --- a/GFramework.Core.Tests/architecture/ArchitectureServicesTests.cs +++ b/GFramework.Core.Tests/architecture/ArchitectureServicesTests.cs @@ -13,7 +13,9 @@ using GFramework.Core.environment; using GFramework.Core.events; using GFramework.Core.ioc; using GFramework.Core.query; +using Mediator; using NUnit.Framework; +using ICommand = GFramework.Core.Abstractions.command.ICommand; namespace GFramework.Core.Tests.architecture; @@ -264,11 +266,80 @@ public class TestArchitectureContextV3 : IArchitectureContext { } + public ValueTask SendRequestAsync(IRequest request, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public TResponse SendRequest(IRequest request) + { + throw new NotImplementedException(); + } + + public ValueTask SendCommandAsync(Mediator.ICommand command, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public TResponse SendCommand(Mediator.ICommand command) + { + throw new NotImplementedException(); + } + + public ValueTask SendQueryAsync(Mediator.IQuery command, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public TResponse SendQuery(Mediator.IQuery command) + { + throw new NotImplementedException(); + } + + public ValueTask PublishAsync(TNotification notification, + CancellationToken cancellationToken = default) where TNotification : INotification + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable CreateStream(IStreamRequest request, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask SendAsync(TCommand command, CancellationToken cancellationToken = default) + where TCommand : IRequest + { + throw new NotImplementedException(); + } + + public ValueTask SendAsync(IRequest command, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask QueryAsync(IRequest query, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask PublishEventAsync(TNotification notification, + CancellationToken cancellationToken = default) where TNotification : INotification + { + throw new NotImplementedException(); + } + public void SendCommand(ICommand command) { } - public TResult SendCommand(ICommand command) + public TResult SendCommand(Abstractions.command.ICommand command) { return default!; } @@ -283,7 +354,7 @@ public class TestArchitectureContextV3 : IArchitectureContext return (Task)Task.CompletedTask; } - public TResult SendQuery(IQuery query) + public TResult SendQuery(Abstractions.query.IQuery query) { return default!; } diff --git a/GFramework.Core.Tests/architecture/GameContextTests.cs b/GFramework.Core.Tests/architecture/GameContextTests.cs index 054b70b..c7cb31b 100644 --- a/GFramework.Core.Tests/architecture/GameContextTests.cs +++ b/GFramework.Core.Tests/architecture/GameContextTests.cs @@ -13,7 +13,9 @@ using GFramework.Core.environment; using GFramework.Core.events; using GFramework.Core.ioc; using GFramework.Core.query; +using Mediator; using NUnit.Framework; +using ICommand = GFramework.Core.Abstractions.command.ICommand; namespace GFramework.Core.Tests.architecture; @@ -333,6 +335,75 @@ public class TestArchitectureContext : IArchitectureContext { } + public ValueTask SendRequestAsync(IRequest request, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public TResponse SendRequest(IRequest request) + { + throw new NotImplementedException(); + } + + public ValueTask SendCommandAsync(Mediator.ICommand command, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public TResponse SendCommand(Mediator.ICommand command) + { + throw new NotImplementedException(); + } + + public ValueTask SendQueryAsync(Mediator.IQuery command, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public TResponse SendQuery(Mediator.IQuery command) + { + throw new NotImplementedException(); + } + + public ValueTask PublishAsync(TNotification notification, + CancellationToken cancellationToken = default) where TNotification : INotification + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable CreateStream(IStreamRequest request, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask SendAsync(TCommand command, CancellationToken cancellationToken = default) + where TCommand : IRequest + { + throw new NotImplementedException(); + } + + public ValueTask SendAsync(IRequest command, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask QueryAsync(IRequest query, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask PublishEventAsync(TNotification notification, + CancellationToken cancellationToken = default) where TNotification : INotification + { + throw new NotImplementedException(); + } + /// /// 发送命令 /// @@ -347,7 +418,7 @@ public class TestArchitectureContext : IArchitectureContext /// 返回值类型 /// 命令对象 /// 命令执行结果 - public TResult SendCommand(ICommand command) + public TResult SendCommand(Abstractions.command.ICommand command) { return default!; } @@ -368,7 +439,7 @@ public class TestArchitectureContext : IArchitectureContext /// 查询结果类型 /// 查询对象 /// 查询结果 - public TResult SendQuery(IQuery query) + public TResult SendQuery(Abstractions.query.IQuery query) { return default!; } diff --git a/GFramework.Core.Tests/mediator/MediatorComprehensiveTests.cs b/GFramework.Core.Tests/mediator/MediatorComprehensiveTests.cs new file mode 100644 index 0000000..7e498b0 --- /dev/null +++ b/GFramework.Core.Tests/mediator/MediatorComprehensiveTests.cs @@ -0,0 +1,443 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using GFramework.Core.Abstractions.architecture; +using GFramework.Core.Abstractions.enums; +using GFramework.Core.Abstractions.events; +using GFramework.Core.Abstractions.model; +using GFramework.Core.Abstractions.system; +using GFramework.Core.Abstractions.utility; +using GFramework.Core.architecture; +using GFramework.Core.command; +using GFramework.Core.environment; +using GFramework.Core.events; +using GFramework.Core.ioc; +using GFramework.Core.logging; +using GFramework.Core.query; +using Mediator; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +// ✅ Mediator 库的命名空间 + +// ✅ 使用 global using 或别名来区分 + +namespace GFramework.Core.Tests.mediator; + +[TestFixture] +public class MediatorComprehensiveTests +{ + [SetUp] + public void SetUp() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); + _container = new MicrosoftDiContainer(); + + var loggerField = typeof(MicrosoftDiContainer).GetField("_logger", + BindingFlags.NonPublic | BindingFlags.Instance); + loggerField?.SetValue(_container, + LoggerFactoryResolver.Provider.CreateLogger(nameof(MediatorComprehensiveTests))); + + // 注册基础服务(Legacy CQRS) + _eventBus = new EventBus(); + _commandBus = new CommandExecutor(); + _queryBus = new QueryExecutor(); + _asyncQueryBus = new AsyncQueryExecutor(); + _environment = new DefaultEnvironment(); + + _container.RegisterPlurality(_eventBus); + _container.RegisterPlurality(_commandBus); + _container.RegisterPlurality(_queryBus); + _container.RegisterPlurality(_asyncQueryBus); + _container.RegisterPlurality(_environment); + + // ✅ 注册 Mediator + _container.RegisterMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; }); + + // ✅ 手动注册 Mediator Handlers + _container.Services.AddSingleton, TestRequestHandler>(); + _container.Services.AddSingleton, TestCommandHandler>(); + _container.Services.AddSingleton, TestCommandWithResultHandler>(); + _container.Services.AddSingleton, TestQueryHandler>(); + _container.Services.AddSingleton, TestNotificationHandler>(); + _container.Services.AddSingleton, TestStreamRequestHandler>(); + + // 注册测试组件(Legacy) + _testSystem = new TestSystem(); + _testModel = new TestModel(); + _testUtility = new TestUtility(); + _testCommand = new TestTraditionalCommand(); + _testQuery = new TestTraditionalQuery { Result = 999 }; + + _container.RegisterPlurality(_testSystem); + _container.RegisterPlurality(_testModel); + _container.RegisterPlurality(_testUtility); + _container.RegisterPlurality(_testCommand); + _container.RegisterPlurality(_testQuery); + + // ✅ Freeze 容器 + _container.Freeze(); + + _context = new ArchitectureContext(_container); + } + + [TearDown] + public void TearDown() + { + _context = null; + _container = null; + _eventBus = null; + _commandBus = null; + _queryBus = null; + _asyncQueryBus = null; + _environment = null; + _testSystem = null; + _testModel = null; + _testUtility = null; + _testCommand = null; + _testQuery = null; + } + + private ArchitectureContext? _context; + private MicrosoftDiContainer? _container; + private EventBus? _eventBus; + private CommandExecutor? _commandBus; + private QueryExecutor? _queryBus; + private AsyncQueryExecutor? _asyncQueryBus; + private DefaultEnvironment? _environment; + private TestSystem? _testSystem; + private TestModel? _testModel; + private TestUtility? _testUtility; + private TestTraditionalCommand? _testCommand; + private TestTraditionalQuery? _testQuery; + + [Test] + public async Task SendRequestAsync_Should_ReturnResult_When_Request_IsValid() + { + var testRequest = new TestRequest { Value = 42 }; + var result = await _context!.SendRequestAsync(testRequest); + + Assert.That(result, Is.EqualTo(42)); + } + + [Test] + public void SendRequestAsync_Should_ThrowArgumentNullException_When_Request_IsNull() + { + Assert.ThrowsAsync(async () => + await _context!.SendRequestAsync(null!)); + } + + [Test] + public void SendRequest_Should_ReturnResult_When_Request_IsValid() + { + var testRequest = new TestRequest { Value = 123 }; + var result = _context!.SendRequest(testRequest); + + Assert.That(result, Is.EqualTo(123)); + } + + [Test] + public async Task PublishAsync_Should_PublishNotification_When_Notification_IsValid() + { + TestNotificationHandler.LastReceivedMessage = null; + var notification = new TestNotification { Message = "test" }; + + await _context!.PublishAsync(notification); + await Task.Delay(100); + + Assert.That(TestNotificationHandler.LastReceivedMessage, Is.EqualTo("test")); + } + + [Test] + public async Task CreateStream_Should_ReturnStream_When_StreamRequest_IsValid() + { + var testStreamRequest = new TestStreamRequest { Values = [1, 2, 3, 4, 5] }; + var stream = _context!.CreateStream(testStreamRequest); + + var results = new List(); + await foreach (var item in stream) + { + results.Add(item); + } + + Assert.That(results, Is.EqualTo(new[] { 1, 2, 3, 4, 5 })); + } + + [Test] + public async Task SendAsync_CommandWithoutResult_Should_Execute_When_Command_IsValid() + { + var testCommand = new TestCommand { ShouldExecute = true }; + await _context!.SendAsync(testCommand); + + Assert.That(testCommand.Executed, Is.True); + } + + [Test] + public async Task SendAsync_CommandWithResult_Should_ReturnResult_When_Command_IsValid() + { + var testCommand = new TestCommandWithResult { ResultValue = 42 }; + var result = await _context!.SendAsync(testCommand); + + Assert.That(result, Is.EqualTo(42)); + } + + [Test] + public async Task QueryAsync_Should_ReturnResult_When_Query_IsValid() + { + var testQuery = new TestQuery { QueryResult = "test result" }; + var result = await _context!.QueryAsync(testQuery); + + Assert.That(result, Is.EqualTo("test result")); + } + + [Test] + public async Task PublishEventAsync_Should_PublishNotification_When_Notification_IsValid() + { + TestNotificationHandler.LastReceivedMessage = null; + var testNotification = new TestNotification { Message = "test event" }; + + await _context!.PublishEventAsync(testNotification); + await Task.Delay(100); + + Assert.That(TestNotificationHandler.LastReceivedMessage, Is.EqualTo("test event")); + } + + [Test] + public async Task Mediator_And_CommandExecutor_Should_Coexist() + { + // 使用传统方式(Legacy) + _context!.SendCommand(_testCommand!); + Assert.That(_testCommand!.Executed, Is.True); + + // 使用 Mediator 方式 + var mediatorCommand = new TestCommandWithResult { ResultValue = 123 }; + var result = await _context.SendAsync(mediatorCommand); + Assert.That(result, Is.EqualTo(123)); + } + + [Test] + public async Task Mediator_And_QueryExecutor_Should_Coexist() + { + // 使用传统方式(Legacy) + var traditionalResult = _context!.SendQuery(_testQuery!); + Assert.That(traditionalResult, Is.EqualTo(999)); + + // 使用 Mediator 方式 + var mediatorQuery = new TestQuery { QueryResult = "mediator result" }; + var mediatorResult = await _context.QueryAsync(mediatorQuery); + Assert.That(mediatorResult, Is.EqualTo("mediator result")); + } + + [Test] + public void GetService_Should_Use_Cache() + { + var firstResult = _context!.GetService(); + Assert.That(firstResult, Is.Not.Null); + Assert.That(firstResult, Is.SameAs(_eventBus)); + + var secondResult = _context.GetService(); + Assert.That(secondResult, Is.SameAs(firstResult)); + } + + [Test] + public void Architecture_Component_Getters_Should_Work() + { + var system = _context!.GetSystem(); + var model = _context.GetModel(); + var utility = _context.GetUtility(); + var environment = _context.GetEnvironment(); + + Assert.That(system, Is.SameAs(_testSystem)); + Assert.That(model, Is.SameAs(_testModel)); + Assert.That(utility, Is.SameAs(_testUtility)); + Assert.That(environment, Is.SameAs(_environment)); + } + + [Test] + public void Unregistered_Mediator_Should_Throw_InvalidOperationException() + { + var containerWithoutMediator = new MicrosoftDiContainer(); + containerWithoutMediator.Freeze(); + + var contextWithoutMediator = new ArchitectureContext(containerWithoutMediator); + var testRequest = new TestRequest { Value = 42 }; + + Assert.ThrowsAsync(async () => + await contextWithoutMediator.SendRequestAsync(testRequest)); + } +} + +#region Test Classes - Mediator (新实现) + +// ✅ 这些类使用 Mediator.IRequest +public sealed record TestRequest : IRequest +{ + public int Value { get; init; } +} + +public sealed record TestCommand : IRequest +{ + public bool ShouldExecute { get; init; } + public bool Executed { get; set; } +} + +public sealed record TestCommandWithResult : IRequest +{ + public int ResultValue { get; init; } +} + +public sealed record TestQuery : IRequest +{ + public string QueryResult { get; init; } = string.Empty; +} + +public sealed record TestNotification : INotification +{ + public string Message { get; init; } = string.Empty; +} + +public sealed record TestStreamRequest : IStreamRequest +{ + public int[] Values { get; init; } = []; +} + +// ✅ 这些 Handler 使用 Mediator.IRequestHandler +public sealed class TestRequestHandler : IRequestHandler +{ + public ValueTask Handle(TestRequest request, CancellationToken cancellationToken) + { + return new ValueTask(request.Value); + } +} + +public sealed class TestCommandHandler : IRequestHandler +{ + public ValueTask Handle(TestCommand request, CancellationToken cancellationToken) + { + if (request.ShouldExecute) + { + request.Executed = true; + } + + return ValueTask.FromResult(Unit.Value); + } +} + +public sealed class TestCommandWithResultHandler : IRequestHandler +{ + public ValueTask Handle(TestCommandWithResult request, CancellationToken cancellationToken) + { + return new ValueTask(request.ResultValue); + } +} + +public sealed class TestQueryHandler : IRequestHandler +{ + public ValueTask Handle(TestQuery request, CancellationToken cancellationToken) + { + return new ValueTask(request.QueryResult); + } +} + +public sealed class TestNotificationHandler : INotificationHandler +{ + public static string? LastReceivedMessage { get; set; } + + public ValueTask Handle(TestNotification notification, CancellationToken cancellationToken) + { + LastReceivedMessage = notification.Message; + return ValueTask.CompletedTask; + } +} + +public sealed class TestStreamRequestHandler : IStreamRequestHandler +{ + public async IAsyncEnumerable Handle( + TestStreamRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + foreach (var value in request.Values) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return value; + await Task.Yield(); + } + } +} + +#endregion + +#region Test Classes - Legacy CQRS (旧实现) + +public class TestSystem : ISystem +{ + private IArchitectureContext _context = null!; + public int Id { get; init; } + + public void SetContext(IArchitectureContext context) => _context = context; + public IArchitectureContext GetContext() => _context; + + public void Init() + { + } + + public void Destroy() + { + } + + public void OnArchitecturePhase(ArchitecturePhase phase) + { + } +} + +public class TestModel : IModel +{ + private IArchitectureContext _context = null!; + public int Id { get; init; } + + public void SetContext(IArchitectureContext context) => _context = context; + public IArchitectureContext GetContext() => _context; + + public void Init() + { + } + + public void OnArchitecturePhase(ArchitecturePhase phase) + { + } + + public void Destroy() + { + } +} + +public class TestUtility : IUtility +{ + private IArchitectureContext _context = null!; + public int Id { get; init; } + + public void SetContext(IArchitectureContext context) => _context = context; + public IArchitectureContext GetContext() => _context; +} + +// ✅ 使用你框架的 ICommand +public class TestTraditionalCommand : ICommand +{ + private IArchitectureContext _context = null!; + public bool Executed { get; private set; } + + public void Execute() => Executed = true; + public void SetContext(IArchitectureContext context) => _context = context; + public IArchitectureContext GetContext() => _context; +} + +// ✅ 使用你框架的 IQuery +public class TestTraditionalQuery : IQuery +{ + private IArchitectureContext _context = null!; + public int Result { get; init; } + + public int Do() => Result; + public void SetContext(IArchitectureContext context) => _context = context; + public IArchitectureContext GetContext() => _context; +} + +#endregion \ No newline at end of file diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj index 513b2a8..dbe8741 100644 --- a/GFramework.Core/GFramework.Core.csproj +++ b/GFramework.Core/GFramework.Core.csproj @@ -12,7 +12,7 @@ - + diff --git a/GFramework.Core/architecture/ArchitectureContext.cs b/GFramework.Core/architecture/ArchitectureContext.cs index 2dda931..e69f4de 100644 --- a/GFramework.Core/architecture/ArchitectureContext.cs +++ b/GFramework.Core/architecture/ArchitectureContext.cs @@ -27,6 +27,11 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext /// private IMediator? Mediator => GetOrCache(); + /// + /// 获取 ISender 实例(更轻量的发送器) + /// + private ISender? Sender => GetOrCache(); + /// /// 获取 IPublisher 实例(用于发布通知) /// @@ -97,6 +102,68 @@ public class ArchitectureContext(IIocContainer container) : IArchitectureContext return SendRequestAsync(request).AsTask().GetAwaiter().GetResult(); } + /// + /// [Mediator] 异步发送命令并返回结果 + /// 通过Mediator模式发送命令请求,支持取消操作 + /// + /// 命令响应类型 + /// 要发送的命令对象 + /// 取消令牌,用于取消操作 + /// 包含命令执行结果的ValueTask + public async ValueTask SendCommandAsync(Mediator.ICommand command, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(command); + + var sender = Sender; + if (sender == null) + throw new InvalidOperationException("Sender not registered."); + + return await sender.Send(command, cancellationToken); + } + + /// + /// [Mediator] 发送命令的同步版本(不推荐,仅用于兼容性) + /// + /// 命令响应类型 + /// 要发送的命令对象 + /// 命令执行结果 + public TResponse SendCommand(Mediator.ICommand command) + { + return SendCommandAsync(command).AsTask().GetAwaiter().GetResult(); + } + + /// + /// [Mediator] 异步发送查询并返回结果 + /// 通过Mediator模式发送查询请求,支持取消操作 + /// + /// 查询响应类型 + /// 要发送的查询对象 + /// 取消令牌,用于取消操作 + /// 包含查询结果的ValueTask + public async ValueTask SendQueryAsync(Mediator.IQuery command, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(command); + + var sender = Sender; + if (sender == null) + throw new InvalidOperationException("Sender not registered."); + + return await sender.Send(command, cancellationToken); + } + + /// + /// [Mediator] 发送查询的同步版本(不推荐,仅用于兼容性) + /// + /// 查询响应类型 + /// 要发送的查询对象 + /// 查询结果 + public TResponse SendQuery(Mediator.IQuery command) + { + return SendQueryAsync(command).AsTask().GetAwaiter().GetResult(); + } + /// /// [Mediator] 发布通知(一对多) /// 用于事件驱动场景,多个处理器可以同时处理同一个通知