diff --git a/src/DispatchR/Configuration/ServiceRegistrator.cs b/src/DispatchR/Configuration/ServiceRegistrator.cs index b57d506..afbf7c2 100644 --- a/src/DispatchR/Configuration/ServiceRegistrator.cs +++ b/src/DispatchR/Configuration/ServiceRegistrator.cs @@ -1,7 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; -using System.Runtime.CompilerServices; -using DispatchR.Abstractions.Notification; -using DispatchR.Abstractions.Send; +using DispatchR.Abstractions.Send; +using Microsoft.Extensions.DependencyInjection; namespace DispatchR.Configuration { @@ -153,11 +151,21 @@ public static void RegisterHandlers(IServiceCollection services, List allT services.AddScoped(handlerInterface, sp => { - var pipelinesWithHandler = Unsafe - .As(sp.GetKeyedServices(key)); + var keyedServices = sp.GetKeyedServices(key); + + IReadOnlyList pipelinesWithHandler = keyedServices switch + { + IRequestHandler[] asArray => asArray, + IReadOnlyList asList => asList, + _ => keyedServices.ToArray() + }; + + // Single handler - no pipeline chaining needed + if (pipelinesWithHandler.Count == 1) + return pipelinesWithHandler[0]; IRequestHandler lastPipeline = pipelinesWithHandler[0]; - for (int i = 1; i < pipelinesWithHandler.Length; i++) + for (var i = 1; i < pipelinesWithHandler.Count; i++) { var pipeline = pipelinesWithHandler[i]; pipeline.SetNext(lastPipeline); diff --git a/src/DispatchR/IMediator.cs b/src/DispatchR/IMediator.cs index c5f8a65..ea397b4 100644 --- a/src/DispatchR/IMediator.cs +++ b/src/DispatchR/IMediator.cs @@ -1,9 +1,9 @@ -using System.Runtime.CompilerServices; -using DispatchR.Abstractions.Notification; +using DispatchR.Abstractions.Notification; using DispatchR.Abstractions.Send; using DispatchR.Abstractions.Stream; using DispatchR.Exceptions; using Microsoft.Extensions.DependencyInjection; +using System.Runtime.CompilerServices; namespace DispatchR; @@ -17,7 +17,7 @@ IAsyncEnumerable CreateStream(IStreamRequest(TNotification request, CancellationToken cancellationToken) where TNotification : INotification; - + /// /// This method is not recommended for performance-critical scenarios. /// Use it only if it is strictly necessary, as its performance is lower compared @@ -28,8 +28,8 @@ ValueTask Publish(TNotification request, CancellationToken cancel /// /// /// - [Obsolete(message: "This method has performance issues. Use only if strictly necessary", - error: false, + [Obsolete(message: "This method has performance issues. Use only if strictly necessary", + error: false, DiagnosticId = Constants.DiagnosticPerformanceIssue)] ValueTask Publish(object request, CancellationToken cancellationToken); } @@ -60,13 +60,29 @@ public IAsyncEnumerable CreateStream(IStreamRequ public async ValueTask Publish(TNotification request, CancellationToken cancellationToken) where TNotification : INotification { - var notificationsInDi = serviceProvider.GetRequiredService>>(); + var handlers = serviceProvider.GetRequiredService>>(); + + if (handlers is INotificationHandler[] handlerArray) + { + foreach (var handler in handlerArray) + { + await ProcessHandlerAsync(handler); + } + } + else + { + foreach (var handler in handlers) + { + await ProcessHandlerAsync(handler); + } + } + + return; - var notifications = Unsafe.As[]>(notificationsInDi); - foreach (var notification in notifications) + async ValueTask ProcessHandlerAsync(INotificationHandler handler) { - var valueTask = notification.Handle(request, cancellationToken); - if (valueTask.IsCompletedSuccessfully is false) + var valueTask = handler.Handle(request, cancellationToken); + if (!valueTask.IsCompletedSuccessfully) { await valueTask; } diff --git a/tests/DispatchR.IntegrationTest/NotificationTests.cs b/tests/DispatchR.IntegrationTest/NotificationTests.cs index 06dd78a..e83430b 100644 --- a/tests/DispatchR.IntegrationTest/NotificationTests.cs +++ b/tests/DispatchR.IntegrationTest/NotificationTests.cs @@ -103,4 +103,144 @@ public void RegisterNotification_SingleClassWithMultipleNotificationInterfaces_R Assert.Contains(handlers1, h => h is MultiNotificationHandler); Assert.Contains(handlers2, h => h is MultiNotificationHandler); } + + [Fact] + public async Task Publish_CallsSingleHandler_WhenOnlyOneHandlerIsRegistered() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = false; + }); + + var spyHandlerMock = new Mock>(); + spyHandlerMock.Setup(p => p.Handle(It.IsAny(), It.IsAny())) + .Returns(ValueTask.CompletedTask); + + services.AddScoped>(sp => spyHandlerMock.Object); + + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + await mediator.Publish(new MultiHandlersNotification(Guid.Empty), CancellationToken.None); + + // Assert + spyHandlerMock.Verify(p => p.Handle(It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + [Fact] + public async Task Publish_CallsAsyncHandlers_WhenHandlersRequireAwaiting() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = true; + cfg.IncludeHandlers = [typeof(NotificationOneHandler)]; + }); + + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + await mediator.Publish(new MultiHandlersNotification(Guid.Empty), CancellationToken.None); + + // Assert - if this completes without exception, the async handler was properly awaited + Assert.True(true); + } + + [Fact] + public async Task Publish_CallsSyncHandlers_WhenHandlersAreAlreadyCompleted() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = true; + cfg.IncludeHandlers = [typeof(NotificationTwoHandler), typeof(NotificationThreeHandler)]; + }); + + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + await mediator.Publish(new MultiHandlersNotification(Guid.Empty), CancellationToken.None); + + // Assert - if this completes without exception, the sync handlers were properly handled + Assert.True(true); + } + + [Fact] + public async Task Publish_HandlesNonArrayEnumerable_WhenHandlersAreNotArray() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = false; + }); + + var handler1Mock = new Mock>(); + var handler2Mock = new Mock>(); + + handler1Mock.Setup(p => p.Handle(It.IsAny(), It.IsAny())) + .Returns(ValueTask.CompletedTask); + handler2Mock.Setup(p => p.Handle(It.IsAny(), It.IsAny())) + .Returns(ValueTask.CompletedTask); + + // Register a custom service that returns a non-array IEnumerable + services.AddScoped>>(sp => + { + var list = new List> + { + handler1Mock.Object, + handler2Mock.Object + }; + // Return as IEnumerable (not array) by using LINQ + return list.Where(h => h != null); + }); + + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + await mediator.Publish(new MultiHandlersNotification(Guid.Empty), CancellationToken.None); + + // Assert + handler1Mock.Verify(p => p.Handle(It.IsAny(), It.IsAny()), Times.Exactly(1)); + handler2Mock.Verify(p => p.Handle(It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + [Fact] + public async Task Publish_HandlesMixedAsyncAndSyncHandlers_WhenMultipleHandlersAreRegistered() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = true; + cfg.IncludeHandlers = [typeof(NotificationOneHandler), typeof(NotificationTwoHandler), typeof(NotificationThreeHandler)]; + }); + + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + await mediator.Publish(new MultiHandlersNotification(Guid.Empty), CancellationToken.None); + + // Assert - if this completes without exception, all handlers (async and sync) were properly handled + Assert.True(true); + } } diff --git a/tests/DispatchR.UnitTest/RequestHandlerTests.cs b/tests/DispatchR.UnitTest/RequestHandlerTests.cs index 4914b8a..b7a83db 100644 --- a/tests/DispatchR.UnitTest/RequestHandlerTests.cs +++ b/tests/DispatchR.UnitTest/RequestHandlerTests.cs @@ -26,14 +26,14 @@ public void Send_ReturnsExpectedResponse_SyncRequestHandler() }); var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetRequiredService(); - + // Act var result = mediator.Send(new Ping(), CancellationToken.None); - + // Assert Assert.Equal(1, result); } - + [Fact] public async Task Send_ReturnsExpectedResponse_AsyncRequestHandlerWithTask() { @@ -48,14 +48,14 @@ public async Task Send_ReturnsExpectedResponse_AsyncRequestHandlerWithTask() }); var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetRequiredService(); - + // Act var result = await mediator.Send(new PingTask(), CancellationToken.None); - + // Assert Assert.Equal(1, result); } - + [Fact] public async Task Send_ReturnsExpectedResponse_AsyncRequestHandlerWithValueTask() { @@ -70,14 +70,14 @@ public async Task Send_ReturnsExpectedResponse_AsyncRequestHandlerWithValueTask( }); var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetRequiredService(); - + // Act var result = await mediator.Send(new PingValueTask(), CancellationToken.None); - + // Assert Assert.Equal(1, result); } - + [Fact] public async Task Send_UsesPipelineBehaviors_RequestWithPipelines() { @@ -92,14 +92,14 @@ public async Task Send_UsesPipelineBehaviors_RequestWithPipelines() }); var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetRequiredService(); - + // Act var result = await mediator.Send(new PingValueTask(), CancellationToken.None); - + // Assert Assert.Equal(1, result); } - + [Fact] public async Task Send_UsesPipelineBehaviors_RequestWithOutResponseWithPipelines() { @@ -114,14 +114,14 @@ public async Task Send_UsesPipelineBehaviors_RequestWithOutResponseWithPipelines }); var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetRequiredService(); - + // Act await mediator.Send(Fixture.AnyRequestWithoutResponsePipeline, CancellationToken.None); - + // Assert // Just checking if it runs without exceptions } - + [Fact] public async Task Send_UsesPipelineBehaviors_ChangePipelineOrdering() { @@ -140,14 +140,14 @@ public async Task Send_UsesPipelineBehaviors_ChangePipelineOrdering() }); var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetRequiredService(); - + // Act var result = await mediator.Send(new PingValueTask(), CancellationToken.None); - + // Assert Assert.Equal(1, result); } - + [Fact] public void Send_ThrowsException_WhenNoHandlerIsRegistered() { @@ -160,10 +160,10 @@ public void Send_ThrowsException_WhenNoHandlerIsRegistered() cfg.RegisterNotifications = false; cfg.IncludeHandlers = [typeof(RequestWithoutHandler)]; }); - + var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetRequiredService(); - + // Act void Action() => mediator.Send(new RequestWithoutHandler(), CancellationToken.None); @@ -174,7 +174,7 @@ public void Send_ThrowsException_WhenNoHandlerIsRegistered() Make sure you have registered a handler that implements IRequestHandler in the DI container. """, exception.Message); } - + [Fact] public void Send_UsesCachedHandler_InstanceReusedInScopedLifetime() { @@ -190,13 +190,57 @@ public void Send_UsesCachedHandler_InstanceReusedInScopedLifetime() var serviceProvider = services.BuildServiceProvider(); var scope = serviceProvider.CreateScope(); var mediator = scope.ServiceProvider.GetRequiredService(); - + // Act var first = mediator.Send(new RequestReusedInScopedLifetime(), CancellationToken.None); var second = mediator.Send(new RequestReusedInScopedLifetime(), CancellationToken.None); var third = mediator.Send(new RequestReusedInScopedLifetime(), CancellationToken.None); - + // Assert Assert.Equal(3, first + second + third); } + + [Fact] + public void Send_ReturnsSingleHandler_WhenNoPipelinesAreRegistered() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [typeof(PingHandler)]; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + var result = mediator.Send(new Ping(), CancellationToken.None); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public async Task Send_ReturnsSingleHandler_WhenOnlyOneHandlerExistsWithPipelinesEnabled() + { + // Arrange + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = true; + cfg.RegisterNotifications = false; + cfg.IncludeHandlers = [typeof(PingTaskHandler)]; // Handler without pipeline behaviors + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + var result = await mediator.Send(new PingTask(), CancellationToken.None); + + // Assert + Assert.Equal(1, result); + } } \ No newline at end of file