From e8ec98823082c5e61a6e28de93b4910b83817988 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:02:51 +0000 Subject: [PATCH 1/2] feat: support open generic notification registration and add tests Agent-Logs-Url: https://github.com/hasanxdev/DispatchR/sessions/595ce1db-3d15-4029-9de2-c03cfde0e56d Co-authored-by: hasanxdev <30981174+hasanxdev@users.noreply.github.com> --- .../Configuration/ServiceRegistrator.cs | 39 ++++++---- .../NotificationTests.cs | 73 +++++++++++++++++++ .../OpenGenericNotificationExecutionStore.cs | 23 ++++++ .../OpenGenericNotificationHandler.cs | 13 ++++ .../OpenGenericOnlyNotification.cs | 5 ++ .../OpenGenericTargetNotification.cs | 5 ++ .../OpenGenericTargetNotificationHandler.cs | 12 +++ .../AddDispatchRConfigurationTests.cs | 29 +++++++- 8 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationExecutionStore.cs create mode 100644 tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationHandler.cs create mode 100644 tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericOnlyNotification.cs create mode 100644 tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotification.cs create mode 100644 tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotificationHandler.cs diff --git a/src/DispatchR/Configuration/ServiceRegistrator.cs b/src/DispatchR/Configuration/ServiceRegistrator.cs index b57d506..16a2f9f 100644 --- a/src/DispatchR/Configuration/ServiceRegistrator.cs +++ b/src/DispatchR/Configuration/ServiceRegistrator.cs @@ -170,20 +170,29 @@ public static void RegisterHandlers(IServiceCollection services, List allT } } - public static void RegisterNotification(IServiceCollection services, List allTypes, - Type syncNotificationHandlerType) - { - var allNotifications = allTypes - .SelectMany(handlerType => handlerType.GetInterfaces() - .Where(i => i.IsGenericType && syncNotificationHandlerType == i.GetGenericTypeDefinition()) - .Select(i => new { HandlerType = handlerType, Interface = i })) - .ToList(); - - foreach (var notification in allNotifications) - { - services.AddScoped(notification.Interface, notification.HandlerType); - } - } + public static void RegisterNotification(IServiceCollection services, List allTypes, + Type syncNotificationHandlerType) + { + var allNotifications = allTypes + .SelectMany(handlerType => handlerType.GetInterfaces() + .Where(i => i.IsGenericType && syncNotificationHandlerType == i.GetGenericTypeDefinition()) + .Select(i => new { HandlerType = handlerType, Interface = i })) + .ToList(); + + foreach (var notification in allNotifications) + { + var serviceType = notification.Interface; + var implementationType = notification.HandlerType; + + if (serviceType.ContainsGenericParameters) + { + serviceType = serviceType.GetGenericTypeDefinition(); + implementationType = implementationType.GetGenericTypeDefinition(); + } + + services.AddScoped(serviceType, implementationType); + } + } private static bool IsAwaitable(Type type) { @@ -200,4 +209,4 @@ private static bool IsAwaitable(Type type) return false; } } -} \ No newline at end of file +} diff --git a/tests/DispatchR.IntegrationTest/NotificationTests.cs b/tests/DispatchR.IntegrationTest/NotificationTests.cs index 06dd78a..7a9d3c5 100644 --- a/tests/DispatchR.IntegrationTest/NotificationTests.cs +++ b/tests/DispatchR.IntegrationTest/NotificationTests.cs @@ -103,4 +103,77 @@ public void RegisterNotification_SingleClassWithMultipleNotificationInterfaces_R Assert.Contains(handlers1, h => h is MultiNotificationHandler); Assert.Contains(handlers2, h => h is MultiNotificationHandler); } + + [Fact] + public async Task Publish_CallsOpenGenericAndSpecificHandlers_WhenBothAreRegistered() + { + // Arrange + OpenGenericNotificationExecutionStore.Reset(); + + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = true; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + await mediator.Publish(new OpenGenericTargetNotification(Guid.NewGuid()), CancellationToken.None); + + // Assert + Assert.Equal(1, OpenGenericNotificationExecutionStore.Count($"generic:{nameof(OpenGenericTargetNotification)}")); + Assert.Equal(1, OpenGenericNotificationExecutionStore.Count($"specific:{nameof(OpenGenericTargetNotification)}")); + } + + [Fact] + public async Task PublishObject_CallsOpenGenericAndSpecificHandlers_WhenBothAreRegistered() + { + // Arrange + OpenGenericNotificationExecutionStore.Reset(); + + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = true; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + object notificationObject = new OpenGenericTargetNotification(Guid.NewGuid()); + await mediator.Publish(notificationObject, CancellationToken.None); + + // Assert + Assert.Equal(1, OpenGenericNotificationExecutionStore.Count($"generic:{nameof(OpenGenericTargetNotification)}")); + Assert.Equal(1, OpenGenericNotificationExecutionStore.Count($"specific:{nameof(OpenGenericTargetNotification)}")); + } + + [Fact] + public async Task Publish_CallsOpenGenericHandler_WhenNoSpecificHandlerExists() + { + // Arrange + OpenGenericNotificationExecutionStore.Reset(); + + var services = new ServiceCollection(); + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = true; + }); + var serviceProvider = services.BuildServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + + // Act + await mediator.Publish(new OpenGenericOnlyNotification(Guid.NewGuid()), CancellationToken.None); + + // Assert + Assert.Equal(1, OpenGenericNotificationExecutionStore.Count($"generic:{nameof(OpenGenericOnlyNotification)}")); + Assert.Equal(0, OpenGenericNotificationExecutionStore.Count($"specific:{nameof(OpenGenericOnlyNotification)}")); + } } diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationExecutionStore.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationExecutionStore.cs new file mode 100644 index 0000000..8f12194 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationExecutionStore.cs @@ -0,0 +1,23 @@ +using System.Collections.Concurrent; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public static class OpenGenericNotificationExecutionStore +{ + private static readonly ConcurrentDictionary Counters = new(); + + public static void Reset() + { + Counters.Clear(); + } + + public static void Increment(string key) + { + Counters.AddOrUpdate(key, 1, (_, current) => current + 1); + } + + public static int Count(string key) + { + return Counters.TryGetValue(key, out var count) ? count : 0; + } +} diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationHandler.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationHandler.cs new file mode 100644 index 0000000..a94ba5d --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationHandler.cs @@ -0,0 +1,13 @@ +using DispatchR.Abstractions.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed class OpenGenericNotificationHandler : INotificationHandler + where TNotification : INotification +{ + public ValueTask Handle(TNotification request, CancellationToken cancellationToken) + { + OpenGenericNotificationExecutionStore.Increment($"generic:{typeof(TNotification).Name}"); + return ValueTask.CompletedTask; + } +} diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericOnlyNotification.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericOnlyNotification.cs new file mode 100644 index 0000000..5727b5d --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericOnlyNotification.cs @@ -0,0 +1,5 @@ +using DispatchR.Abstractions.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed record OpenGenericOnlyNotification(Guid Id) : INotification; diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotification.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotification.cs new file mode 100644 index 0000000..54036a0 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotification.cs @@ -0,0 +1,5 @@ +using DispatchR.Abstractions.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed record OpenGenericTargetNotification(Guid Id) : INotification; diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotificationHandler.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotificationHandler.cs new file mode 100644 index 0000000..82a2075 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotificationHandler.cs @@ -0,0 +1,12 @@ +using DispatchR.Abstractions.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed class OpenGenericTargetNotificationHandler : INotificationHandler +{ + public ValueTask Handle(OpenGenericTargetNotification request, CancellationToken cancellationToken) + { + OpenGenericNotificationExecutionStore.Increment($"specific:{nameof(OpenGenericTargetNotification)}"); + return ValueTask.CompletedTask; + } +} diff --git a/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs b/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs index cd87485..e49589b 100644 --- a/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs +++ b/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs @@ -1,3 +1,4 @@ +using DispatchR.Abstractions.Notification; using DispatchR.Abstractions.Stream; using DispatchR.Exceptions; using DispatchR.Extensions; @@ -237,4 +238,30 @@ p.IsKeyedService is false && Assert.Equal(3, countOfAllSimpleHandlers); } -} \ No newline at end of file + + [Fact] + public void AddDispatchR_RegisterNotifications_RegisterOpenGenericNotificationHandler() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = true; + }); + + // Assert + var openGenericHandler = services.SingleOrDefault(p => + p.IsKeyedService is false && + p.ServiceType.IsGenericTypeDefinition && + p.ServiceType == typeof(INotificationHandler<>) && + p.ImplementationType is not null && + p.ImplementationType.IsGenericTypeDefinition && + p.ImplementationType == typeof(OpenGenericNotificationHandler<>)); + + Assert.NotNull(openGenericHandler); + } +} From 7b0ecde3fee502e6fb1ffc4f13ff74af6624acf0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:11:08 +0000 Subject: [PATCH 2/2] test: cover open-generic notifications across publish scenarios Agent-Logs-Url: https://github.com/hasanxdev/DispatchR/sessions/595ce1db-3d15-4029-9de2-c03cfde0e56d Co-authored-by: hasanxdev <30981174+hasanxdev@users.noreply.github.com> --- .../Configuration/ServiceRegistrator.cs | 8 +++++-- .../NotificationTests.cs | 24 +++++++++---------- .../OpenGenericNotificationExecutionStore.cs | 17 +++++-------- .../OpenGenericNotificationHandler.cs | 10 +++++++- .../OpenGenericTargetNotificationHandler.cs | 10 +++++++- .../AddDispatchRConfigurationTests.cs | 2 +- 6 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/DispatchR/Configuration/ServiceRegistrator.cs b/src/DispatchR/Configuration/ServiceRegistrator.cs index 16a2f9f..fe807b6 100644 --- a/src/DispatchR/Configuration/ServiceRegistrator.cs +++ b/src/DispatchR/Configuration/ServiceRegistrator.cs @@ -186,8 +186,12 @@ public static void RegisterNotification(IServiceCollection services, List if (serviceType.ContainsGenericParameters) { - serviceType = serviceType.GetGenericTypeDefinition(); - implementationType = implementationType.GetGenericTypeDefinition(); + serviceType = serviceType.IsGenericTypeDefinition + ? serviceType + : serviceType.GetGenericTypeDefinition(); + implementationType = implementationType.IsGenericTypeDefinition + ? implementationType + : implementationType.GetGenericTypeDefinition(); } services.AddScoped(serviceType, implementationType); diff --git a/tests/DispatchR.IntegrationTest/NotificationTests.cs b/tests/DispatchR.IntegrationTest/NotificationTests.cs index 7a9d3c5..9d97844 100644 --- a/tests/DispatchR.IntegrationTest/NotificationTests.cs +++ b/tests/DispatchR.IntegrationTest/NotificationTests.cs @@ -108,9 +108,8 @@ public void RegisterNotification_SingleClassWithMultipleNotificationInterfaces_R public async Task Publish_CallsOpenGenericAndSpecificHandlers_WhenBothAreRegistered() { // Arrange - OpenGenericNotificationExecutionStore.Reset(); - var services = new ServiceCollection(); + services.AddSingleton(); services.AddDispatchR(cfg => { cfg.Assemblies.Add(typeof(Fixture).Assembly); @@ -119,22 +118,22 @@ public async Task Publish_CallsOpenGenericAndSpecificHandlers_WhenBothAreRegiste }); var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetRequiredService(); + var executionStore = serviceProvider.GetRequiredService(); // Act await mediator.Publish(new OpenGenericTargetNotification(Guid.NewGuid()), CancellationToken.None); // Assert - Assert.Equal(1, OpenGenericNotificationExecutionStore.Count($"generic:{nameof(OpenGenericTargetNotification)}")); - Assert.Equal(1, OpenGenericNotificationExecutionStore.Count($"specific:{nameof(OpenGenericTargetNotification)}")); + Assert.Equal(1, executionStore.Count($"generic:{nameof(OpenGenericTargetNotification)}")); + Assert.Equal(1, executionStore.Count($"specific:{nameof(OpenGenericTargetNotification)}")); } [Fact] public async Task PublishObject_CallsOpenGenericAndSpecificHandlers_WhenBothAreRegistered() { // Arrange - OpenGenericNotificationExecutionStore.Reset(); - var services = new ServiceCollection(); + services.AddSingleton(); services.AddDispatchR(cfg => { cfg.Assemblies.Add(typeof(Fixture).Assembly); @@ -143,23 +142,23 @@ public async Task PublishObject_CallsOpenGenericAndSpecificHandlers_WhenBothAreR }); var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetRequiredService(); + var executionStore = serviceProvider.GetRequiredService(); // Act object notificationObject = new OpenGenericTargetNotification(Guid.NewGuid()); await mediator.Publish(notificationObject, CancellationToken.None); // Assert - Assert.Equal(1, OpenGenericNotificationExecutionStore.Count($"generic:{nameof(OpenGenericTargetNotification)}")); - Assert.Equal(1, OpenGenericNotificationExecutionStore.Count($"specific:{nameof(OpenGenericTargetNotification)}")); + Assert.Equal(1, executionStore.Count($"generic:{nameof(OpenGenericTargetNotification)}")); + Assert.Equal(1, executionStore.Count($"specific:{nameof(OpenGenericTargetNotification)}")); } [Fact] public async Task Publish_CallsOpenGenericHandler_WhenNoSpecificHandlerExists() { // Arrange - OpenGenericNotificationExecutionStore.Reset(); - var services = new ServiceCollection(); + services.AddSingleton(); services.AddDispatchR(cfg => { cfg.Assemblies.Add(typeof(Fixture).Assembly); @@ -168,12 +167,13 @@ public async Task Publish_CallsOpenGenericHandler_WhenNoSpecificHandlerExists() }); var serviceProvider = services.BuildServiceProvider(); var mediator = serviceProvider.GetRequiredService(); + var executionStore = serviceProvider.GetRequiredService(); // Act await mediator.Publish(new OpenGenericOnlyNotification(Guid.NewGuid()), CancellationToken.None); // Assert - Assert.Equal(1, OpenGenericNotificationExecutionStore.Count($"generic:{nameof(OpenGenericOnlyNotification)}")); - Assert.Equal(0, OpenGenericNotificationExecutionStore.Count($"specific:{nameof(OpenGenericOnlyNotification)}")); + Assert.Equal(1, executionStore.Count($"generic:{nameof(OpenGenericOnlyNotification)}")); + Assert.Equal(0, executionStore.Count($"specific:{nameof(OpenGenericOnlyNotification)}")); } } diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationExecutionStore.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationExecutionStore.cs index 8f12194..29ac22f 100644 --- a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationExecutionStore.cs +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationExecutionStore.cs @@ -2,22 +2,17 @@ namespace DispatchR.TestCommon.Fixtures.Notification; -public static class OpenGenericNotificationExecutionStore +public sealed class OpenGenericNotificationExecutionStore { - private static readonly ConcurrentDictionary Counters = new(); + private readonly ConcurrentDictionary _counters = new(); - public static void Reset() + public void Increment(string key) { - Counters.Clear(); + _counters.AddOrUpdate(key, 1, (_, current) => current + 1); } - public static void Increment(string key) + public int Count(string key) { - Counters.AddOrUpdate(key, 1, (_, current) => current + 1); - } - - public static int Count(string key) - { - return Counters.TryGetValue(key, out var count) ? count : 0; + return _counters.TryGetValue(key, out var count) ? count : 0; } } diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationHandler.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationHandler.cs index a94ba5d..404fc38 100644 --- a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationHandler.cs +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericNotificationHandler.cs @@ -5,9 +5,17 @@ namespace DispatchR.TestCommon.Fixtures.Notification; public sealed class OpenGenericNotificationHandler : INotificationHandler where TNotification : INotification { + private static readonly OpenGenericNotificationExecutionStore FallbackStore = new(); + private readonly OpenGenericNotificationExecutionStore _store; + + public OpenGenericNotificationHandler(OpenGenericNotificationExecutionStore? store = null) + { + _store = store ?? FallbackStore; + } + public ValueTask Handle(TNotification request, CancellationToken cancellationToken) { - OpenGenericNotificationExecutionStore.Increment($"generic:{typeof(TNotification).Name}"); + _store.Increment($"generic:{typeof(TNotification).Name}"); return ValueTask.CompletedTask; } } diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotificationHandler.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotificationHandler.cs index 82a2075..fa3f003 100644 --- a/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotificationHandler.cs +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/OpenGenericTargetNotificationHandler.cs @@ -4,9 +4,17 @@ namespace DispatchR.TestCommon.Fixtures.Notification; public sealed class OpenGenericTargetNotificationHandler : INotificationHandler { + private static readonly OpenGenericNotificationExecutionStore FallbackStore = new(); + private readonly OpenGenericNotificationExecutionStore _store; + + public OpenGenericTargetNotificationHandler(OpenGenericNotificationExecutionStore? store = null) + { + _store = store ?? FallbackStore; + } + public ValueTask Handle(OpenGenericTargetNotification request, CancellationToken cancellationToken) { - OpenGenericNotificationExecutionStore.Increment($"specific:{nameof(OpenGenericTargetNotification)}"); + _store.Increment($"specific:{nameof(OpenGenericTargetNotification)}"); return ValueTask.CompletedTask; } } diff --git a/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs b/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs index e49589b..6ca79a6 100644 --- a/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs +++ b/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs @@ -240,7 +240,7 @@ p.IsKeyedService is false && } [Fact] - public void AddDispatchR_RegisterNotifications_RegisterOpenGenericNotificationHandler() + public void AddDispatchR_RegisterNotifications_IncludesOpenGenericNotificationHandler() { // Arrange var services = new ServiceCollection();