Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions src/DispatchR/Configuration/ServiceRegistrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,20 +170,28 @@ public static void RegisterHandlers(IServiceCollection services, List<Type> allT
}
}

public static void RegisterNotification(IServiceCollection services, List<Type> 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<Type> 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();
}

services.AddScoped(serviceType, implementationType);
}
}

private static bool IsAwaitable(Type type)
{
Expand All @@ -200,4 +208,4 @@ private static bool IsAwaitable(Type type)
return false;
}
}
}
}
19 changes: 19 additions & 0 deletions src/Sample/DispatchR/Notification/AllNotificationsLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using DispatchR.Abstractions.Notification;

namespace Sample.DispatchR.Notification;

/// <summary>
/// Open-generic notification handler — handles every INotification published through DispatchR.
/// Useful for cross-cutting concerns such as logging, auditing or telemetry.
/// </summary>
public sealed class AllNotificationsLogger<TNotification>(ILogger<AllNotificationsLogger<TNotification>> logger)
: INotificationHandler<TNotification>
where TNotification : INotification
{
public ValueTask Handle(TNotification notification, CancellationToken cancellationToken)
{
logger.LogInformation("[Generic] Received notification of type {NotificationType}: {@Notification}",
typeof(TNotification).Name, notification);
return ValueTask.CompletedTask;
}
}
5 changes: 5 additions & 0 deletions src/Sample/DispatchR/Notification/GenericAwareNotification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using DispatchR.Abstractions.Notification;

namespace Sample.DispatchR.Notification;

public sealed record GenericAwareNotification(Guid Id, string Message) : INotification;
10 changes: 10 additions & 0 deletions src/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@
return "It works";
});

// Demonstrates open-generic INotificationHandler<TNotification>:
// AllNotificationsLogger<TNotification> is registered once and handles every INotification.
// GenericAwareNotification is handled by both its own specific handler (if any) and the generic one.
app.MapGet("/Notification/DispatchR/Generic", async (DispatchR.IMediator mediator, ILogger<Program> logger) =>
{
var notification = new DispatchRNotificationSample.GenericAwareNotification(Guid.NewGuid(), "Hello from open-generic handler!");
await mediator.Publish(notification, CancellationToken.None);
return "It works";
});

app.Run();

namespace Sample
Expand Down
73 changes: 73 additions & 0 deletions tests/DispatchR.IntegrationTest/NotificationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@

// Act
object notificationObject = new MultiHandlersNotification(Guid.Empty);
await mediator.Publish(notificationObject, CancellationToken.None);

Check warning on line 77 in tests/DispatchR.IntegrationTest/NotificationTests.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'IMediator.Publish(object, CancellationToken)' is obsolete: 'This method has performance issues. Use only if strictly necessary'

// Assert
spyPipelineOneMock.Verify(p => p.Handle(It.IsAny<MultiHandlersNotification>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
Expand Down Expand Up @@ -103,4 +103,77 @@
Assert.Contains(handlers1, h => h is MultiNotificationHandler);
Assert.Contains(handlers2, h => h is MultiNotificationHandler);
}

[Fact]
public async Task Publish_CallsOpenGenericAndSpecificHandlers_WhenBothAreRegistered()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<OpenGenericNotificationExecutionStore>();
services.AddDispatchR(cfg =>
{
cfg.Assemblies.Add(typeof(Fixture).Assembly);
cfg.RegisterPipelines = false;
cfg.RegisterNotifications = true;
});
var serviceProvider = services.BuildServiceProvider();
var mediator = serviceProvider.GetRequiredService<IMediator>();
var executionStore = serviceProvider.GetRequiredService<OpenGenericNotificationExecutionStore>();

// Act
await mediator.Publish(new OpenGenericTargetNotification(Guid.NewGuid()), CancellationToken.None);

// Assert
Assert.Equal(1, executionStore.Count($"generic:{nameof(OpenGenericTargetNotification)}"));
Assert.Equal(1, executionStore.Count($"specific:{nameof(OpenGenericTargetNotification)}"));
}

[Fact]
public async Task PublishObject_CallsOpenGenericAndSpecificHandlers_WhenBothAreRegistered()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<OpenGenericNotificationExecutionStore>();
services.AddDispatchR(cfg =>
{
cfg.Assemblies.Add(typeof(Fixture).Assembly);
cfg.RegisterPipelines = false;
cfg.RegisterNotifications = true;
});
var serviceProvider = services.BuildServiceProvider();
var mediator = serviceProvider.GetRequiredService<IMediator>();
var executionStore = serviceProvider.GetRequiredService<OpenGenericNotificationExecutionStore>();

// Act
object notificationObject = new OpenGenericTargetNotification(Guid.NewGuid());
await mediator.Publish(notificationObject, CancellationToken.None);

Check warning on line 149 in tests/DispatchR.IntegrationTest/NotificationTests.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'IMediator.Publish(object, CancellationToken)' is obsolete: 'This method has performance issues. Use only if strictly necessary'

// Assert
Assert.Equal(1, executionStore.Count($"generic:{nameof(OpenGenericTargetNotification)}"));
Assert.Equal(1, executionStore.Count($"specific:{nameof(OpenGenericTargetNotification)}"));
}

[Fact]
public async Task Publish_CallsOpenGenericHandler_WhenNoSpecificHandlerExists()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<OpenGenericNotificationExecutionStore>();
services.AddDispatchR(cfg =>
{
cfg.Assemblies.Add(typeof(Fixture).Assembly);
cfg.RegisterPipelines = false;
cfg.RegisterNotifications = true;
});
var serviceProvider = services.BuildServiceProvider();
var mediator = serviceProvider.GetRequiredService<IMediator>();
var executionStore = serviceProvider.GetRequiredService<OpenGenericNotificationExecutionStore>();

// Act
await mediator.Publish(new OpenGenericOnlyNotification(Guid.NewGuid()), CancellationToken.None);

// Assert
Assert.Equal(1, executionStore.Count($"generic:{nameof(OpenGenericOnlyNotification)}"));
Assert.Equal(0, executionStore.Count($"specific:{nameof(OpenGenericOnlyNotification)}"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Collections.Concurrent;

namespace DispatchR.TestCommon.Fixtures.Notification;

public sealed class OpenGenericNotificationExecutionStore
{
private readonly ConcurrentDictionary<string, int> _counters = new();

public void Increment(string key)
{
_counters.AddOrUpdate(key, 1, (_, current) => current + 1);
}

public int Count(string key)
{
return _counters.TryGetValue(key, out var count) ? count : 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using DispatchR.Abstractions.Notification;

namespace DispatchR.TestCommon.Fixtures.Notification;

public sealed class OpenGenericNotificationHandler<TNotification> : INotificationHandler<TNotification>
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)
{
_store.Increment($"generic:{typeof(TNotification).Name}");
return ValueTask.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using DispatchR.Abstractions.Notification;

namespace DispatchR.TestCommon.Fixtures.Notification;

public sealed record OpenGenericOnlyNotification(Guid Id) : INotification;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using DispatchR.Abstractions.Notification;

namespace DispatchR.TestCommon.Fixtures.Notification;

public sealed record OpenGenericTargetNotification(Guid Id) : INotification;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using DispatchR.Abstractions.Notification;

namespace DispatchR.TestCommon.Fixtures.Notification;

public sealed class OpenGenericTargetNotificationHandler : INotificationHandler<OpenGenericTargetNotification>
{
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)
{
_store.Increment($"specific:{nameof(OpenGenericTargetNotification)}");
return ValueTask.CompletedTask;
}
}
29 changes: 28 additions & 1 deletion tests/DispatchR.UnitTest/AddDispatchRConfigurationTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using DispatchR.Abstractions.Notification;
using DispatchR.Abstractions.Stream;
using DispatchR.Exceptions;
using DispatchR.Extensions;
Expand Down Expand Up @@ -237,4 +238,30 @@ p.IsKeyedService is false &&

Assert.Equal(3, countOfAllSimpleHandlers);
}
}

[Fact]
public void AddDispatchR_RegisterNotifications_IncludesOpenGenericNotificationHandler()
{
// 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);
}
}
Loading