Skip to content
Draft
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
43 changes: 28 additions & 15 deletions src/DispatchR/Configuration/ServiceRegistrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,20 +170,33 @@ 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.IsGenericTypeDefinition
? serviceType
: serviceType.GetGenericTypeDefinition();
implementationType = implementationType.IsGenericTypeDefinition
? implementationType
: implementationType.GetGenericTypeDefinition();
}

services.AddScoped(serviceType, implementationType);
}
}

private static bool IsAwaitable(Type type)
{
Expand All @@ -200,4 +213,4 @@ private static bool IsAwaitable(Type type)
return false;
}
}
}
}
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