diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..3ea53bc --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,8 @@ +# Repository Instructions + +- Treat 100% test coverage as a hard requirement for this repository. +- Before finishing any code change, add or update tests until the changed area reaches 100% coverage. +- Run the relevant tests and coverage check for the affected project or slice before reporting completion. +- If 100% coverage is not achievable because of an existing gap, call it out explicitly and do not claim the task is fully done until the gap is addressed or the user approves the exception. +- Treat the codebase as two phases: before application start and after application start. +- Any changes in the after-application-start phase must be zero-allocation. diff --git a/src/DispatchR/Configuration/ServiceRegistrator.cs b/src/DispatchR/Configuration/ServiceRegistrator.cs index b88950a..c250bfe 100644 --- a/src/DispatchR/Configuration/ServiceRegistrator.cs +++ b/src/DispatchR/Configuration/ServiceRegistrator.cs @@ -170,28 +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) - { - var serviceType = notification.Interface; - var implementationType = notification.HandlerType; - + public static void RegisterNotification(IServiceCollection services, List allTypes, + Type syncNotificationHandlerType) + { + var allNotifications = allTypes + .Where(handlerType => handlerType.IsClass && !handlerType.IsAbstract) + .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(); - } - - services.AddScoped(serviceType, implementationType); - } - } + } + + services.AddScoped(serviceType, implementationType); + } + } private static bool IsAwaitable(Type type) { @@ -208,4 +209,4 @@ private static bool IsAwaitable(Type type) return false; } } -} +} diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/AbstractNotificationHandler.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/AbstractNotificationHandler.cs new file mode 100644 index 0000000..e3d0938 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/AbstractNotificationHandler.cs @@ -0,0 +1,8 @@ +using DispatchR.Abstractions.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public abstract class AbstractNotificationHandler : INotificationHandler +{ + public abstract ValueTask Handle(OpenGenericTargetNotification request, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/ConcreteWorkflowEvent.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/ConcreteWorkflowEvent.cs new file mode 100644 index 0000000..e5c47c8 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/ConcreteWorkflowEvent.cs @@ -0,0 +1,3 @@ +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed record ConcreteWorkflowEvent : IWorkflowEvent; \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/ConcreteWorkflowNotificationHandler.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/ConcreteWorkflowNotificationHandler.cs new file mode 100644 index 0000000..197ffda --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/ConcreteWorkflowNotificationHandler.cs @@ -0,0 +1,11 @@ +using DispatchR.Abstractions.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed class ConcreteWorkflowNotificationHandler : INotificationHandler> +{ + public ValueTask Handle(WorkflowNotification request, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/IWorkflowEvent.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/IWorkflowEvent.cs new file mode 100644 index 0000000..225a843 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/IWorkflowEvent.cs @@ -0,0 +1,3 @@ +namespace DispatchR.TestCommon.Fixtures.Notification; + +public interface IWorkflowEvent; \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/IWorkflowNotificationHandler.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/IWorkflowNotificationHandler.cs new file mode 100644 index 0000000..481db0d --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/IWorkflowNotificationHandler.cs @@ -0,0 +1,6 @@ +using DispatchR.Abstractions.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public interface IWorkflowNotificationHandler : INotificationHandler> + where TNotification : IWorkflowEvent; \ No newline at end of file diff --git a/tests/DispatchR.TestCommon/Fixtures/Notification/WorkflowNotification.cs b/tests/DispatchR.TestCommon/Fixtures/Notification/WorkflowNotification.cs new file mode 100644 index 0000000..13dc764 --- /dev/null +++ b/tests/DispatchR.TestCommon/Fixtures/Notification/WorkflowNotification.cs @@ -0,0 +1,6 @@ +using DispatchR.Abstractions.Notification; + +namespace DispatchR.TestCommon.Fixtures.Notification; + +public sealed record WorkflowNotification(TNotification Event) : INotification + where TNotification : IWorkflowEvent; \ No newline at end of file diff --git a/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs b/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs index 835526c..ec3f932 100644 --- a/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs +++ b/tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs @@ -327,4 +327,32 @@ p.ImplementationType is not null && Assert.NotNull(openGenericHandler); } + + [Fact] + public void AddDispatchR_RegisterNotifications_SkipsInterfaceAndAbstractHandlers() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddDispatchR(cfg => + { + cfg.Assemblies.Add(typeof(Fixture).Assembly); + cfg.RegisterPipelines = false; + cfg.RegisterNotifications = true; + }); + + // Assert + Assert.DoesNotContain(services, p => + p.IsKeyedService is false && + p.ImplementationType == typeof(AbstractNotificationHandler)); + + Assert.DoesNotContain(services, p => + p.IsKeyedService is false && + p.ImplementationType == typeof(IWorkflowNotificationHandler<>)); + + Assert.Contains(services, p => + p.IsKeyedService is false && + p.ImplementationType == typeof(ConcreteWorkflowNotificationHandler)); + } }