diff --git a/.github/workflows/ci-linux.yaml b/.github/workflows/ci-linux.yaml
index b13b469..1dc7dd7 100644
--- a/.github/workflows/ci-linux.yaml
+++ b/.github/workflows/ci-linux.yaml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- tfm: [ 'net8.0', 'net9.0' ]
+ tfm: [ 'net8.0', 'net9.0', 'net10.0' ]
steps:
- uses: actions/checkout@v2
@@ -18,6 +18,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index c660d9f..3f4712b 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -15,6 +15,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index d1e347e..e0e2d4a 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -17,6 +17,7 @@ jobs:
dotnet-version: |
8.0.x
9.0.x
+ 10.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 2f5c976..344a778 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,8 +1,8 @@
- 1.0.0-alpha.23
- 1.0.0-alpha.23
+ 1.0.0-alpha.24
+ 1.0.0-alpha.24
Zapto
https://github.com/zapto-dev/Mediator
Copyright © 2025 Zapto
@@ -19,4 +19,8 @@
9.0.0
+
+ 10.0.0
+
+
diff --git a/src/Mediator.DependencyInjection/Generic/Handlers/GenericNotificationHandler.cs b/src/Mediator.DependencyInjection/Generic/Handlers/GenericNotificationHandler.cs
index 4537e64..3e39782 100644
--- a/src/Mediator.DependencyInjection/Generic/Handlers/GenericNotificationHandler.cs
+++ b/src/Mediator.DependencyInjection/Generic/Handlers/GenericNotificationHandler.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -37,8 +36,27 @@ internal interface INotificationCache
internal sealed class GenericNotificationCache : INotificationCache
{
+ public GenericNotificationCache(IEnumerable registrations)
+ {
+ var notificationType = typeof(TNotification);
+ if (notificationType.IsGenericType)
+ {
+ var genericType = notificationType.GetGenericTypeDefinition();
+ MatchingRegistrations = GenericTypeHelper.CacheMatchingRegistrations(
+ registrations,
+ r => r.NotificationType,
+ genericType);
+ }
+ else
+ {
+ MatchingRegistrations = new List();
+ }
+ }
+
public List? HandlerTypes { get; set; }
+ public List MatchingRegistrations { get; }
+
public List Registrations { get; } = new();
public SemaphoreSlim Lock { get; } = new(1, 1);
@@ -48,12 +66,10 @@ internal sealed class GenericNotificationHandler
where TNotification : INotification
{
private readonly GenericNotificationCache _cache;
- private readonly IEnumerable _enumerable;
private readonly IServiceProvider _serviceProvider;
- public GenericNotificationHandler(IEnumerable enumerable, IServiceProvider serviceProvider, GenericNotificationCache cache)
+ public GenericNotificationHandler(IServiceProvider serviceProvider, GenericNotificationCache cache)
{
- _enumerable = enumerable;
_serviceProvider = serviceProvider;
_cache = cache;
}
@@ -95,17 +111,24 @@ public async ValueTask Handle(IServiceProvider provider, TNotification not
var genericType = notificationType.GetGenericTypeDefinition();
var handlerTypes = new List();
- foreach (var registration in _enumerable)
+
+ foreach (var registration in _cache.MatchingRegistrations)
{
- if (registration.NotificationType != genericType)
+ Type type;
+ if (registration.HandlerType.IsGenericType)
+ {
+ if (!GenericTypeHelper.CanMakeGenericType(registration.HandlerType, arguments))
+ {
+ continue;
+ }
+
+ type = registration.HandlerType.MakeGenericType(arguments);
+ }
+ else
{
- continue;
+ type = registration.HandlerType;
}
- var type = registration.HandlerType.IsGenericType
- ? registration.HandlerType.MakeGenericType(arguments)
- : registration.HandlerType;
-
var handler = _serviceProvider.GetRequiredService(type);
await ((INotificationHandler)handler!).Handle(provider, notification, ct);
diff --git a/src/Mediator.DependencyInjection/Generic/Handlers/GenericRequestHandler.cs b/src/Mediator.DependencyInjection/Generic/Handlers/GenericRequestHandler.cs
index 7da6da3..50b8b5b 100644
--- a/src/Mediator.DependencyInjection/Generic/Handlers/GenericRequestHandler.cs
+++ b/src/Mediator.DependencyInjection/Generic/Handlers/GenericRequestHandler.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -27,29 +26,64 @@ public GenericRequestRegistration(Type requestType, Type? responseType, Type han
internal sealed class GenericRequestCache
{
+ public GenericRequestCache(IEnumerable registrations)
+ {
+ var requestType = typeof(TRequest);
+ if (requestType.IsGenericType)
+ {
+ var genericType = requestType.GetGenericTypeDefinition();
+ MatchingRegistrations = GenericTypeHelper.CacheMatchingRegistrations(
+ registrations,
+ r => r.RequestType,
+ genericType);
+ }
+ else
+ {
+ MatchingRegistrations = new List();
+ }
+ }
+
public Type? RequestHandlerType { get; set; }
+
+ public List MatchingRegistrations { get; }
}
internal sealed class GenericRequestCache
{
+ public GenericRequestCache(IEnumerable registrations)
+ {
+ var requestType = typeof(TRequest);
+ if (requestType.IsGenericType)
+ {
+ var genericType = requestType.GetGenericTypeDefinition();
+ MatchingRegistrations = GenericTypeHelper.CacheMatchingRegistrations(
+ registrations,
+ r => r.RequestType,
+ genericType);
+ }
+ else
+ {
+ MatchingRegistrations = new List();
+ }
+ }
+
public Type? RequestHandlerType { get; set; }
+
+ public List MatchingRegistrations { get; }
}
internal sealed class GenericRequestHandler : IRequestHandler
where TRequest : IRequest
{
private readonly GenericRequestCache _cache;
- private readonly IEnumerable _enumerable;
private readonly IServiceProvider _serviceProvider;
private readonly IDefaultRequestHandler? _defaultHandler;
public GenericRequestHandler(
- IEnumerable enumerable,
IServiceProvider serviceProvider,
GenericRequestCache cache,
IDefaultRequestHandler? defaultHandler = null)
{
- _enumerable = enumerable;
_serviceProvider = serviceProvider;
_cache = cache;
_defaultHandler = defaultHandler;
@@ -86,17 +120,29 @@ public async ValueTask Handle(IServiceProvider provider, TRequest req
responseType = responseType.GetGenericTypeDefinition();
}
- foreach (var registration in _enumerable)
+
+ foreach (var registration in _cache.MatchingRegistrations)
{
- if (registration.RequestType != requestType ||
- registration.ResponseType is not null && registration.ResponseType != responseType)
+ if (registration.ResponseType is not null && registration.ResponseType != responseType)
{
continue;
}
- var type = registration.HandlerType.IsGenericType
- ? registration.HandlerType.MakeGenericType(arguments)
- : registration.HandlerType;
+ Type type;
+ if (registration.HandlerType.IsGenericType)
+ {
+ // Check if the generic arguments satisfy the handler's constraints
+ if (!GenericTypeHelper.CanMakeGenericType(registration.HandlerType, arguments))
+ {
+ continue;
+ }
+
+ type = registration.HandlerType.MakeGenericType(arguments);
+ }
+ else
+ {
+ type = registration.HandlerType;
+ }
var handler = (IRequestHandler) _serviceProvider.GetRequiredService(type);
@@ -125,17 +171,14 @@ internal sealed class GenericRequestHandler : IRequestHandler _cache;
- private readonly IEnumerable _enumerable;
private readonly IServiceProvider _serviceProvider;
private readonly IDefaultRequestHandler? _defaultHandler;
public GenericRequestHandler(
- IEnumerable enumerable,
IServiceProvider serviceProvider,
GenericRequestCache cache,
IDefaultRequestHandler? defaultHandler = null)
{
- _enumerable = enumerable;
_serviceProvider = serviceProvider;
_cache = cache;
_defaultHandler = defaultHandler;
@@ -168,15 +211,30 @@ public async ValueTask Handle(IServiceProvider provider, TRequest request, Cance
requestType = requestType.GetGenericTypeDefinition();
- foreach (var registration in _enumerable)
+
+ foreach (var registration in _cache.MatchingRegistrations)
{
- if (registration.RequestType != requestType ||
- registration.ResponseType != typeof(Unit))
+ if (registration.ResponseType != typeof(Unit))
{
continue;
}
- var type = registration.HandlerType.MakeGenericType(arguments);
+ Type type;
+ if (registration.HandlerType.IsGenericType)
+ {
+ // Check if the generic arguments satisfy the handler's constraints
+ if (!GenericTypeHelper.CanMakeGenericType(registration.HandlerType, arguments))
+ {
+ continue;
+ }
+
+ type = registration.HandlerType.MakeGenericType(arguments);
+ }
+ else
+ {
+ type = registration.HandlerType;
+ }
+
var handler = (IRequestHandler) _serviceProvider.GetRequiredService(type);
_cache.RequestHandlerType = type;
diff --git a/src/Mediator.DependencyInjection/Generic/Handlers/GenericStreamRequestHandler.cs b/src/Mediator.DependencyInjection/Generic/Handlers/GenericStreamRequestHandler.cs
index 1fe8988..6dc786a 100644
--- a/src/Mediator.DependencyInjection/Generic/Handlers/GenericStreamRequestHandler.cs
+++ b/src/Mediator.DependencyInjection/Generic/Handlers/GenericStreamRequestHandler.cs
@@ -1,8 +1,6 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
-using System.Threading.Tasks;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
@@ -26,24 +24,40 @@ public GenericStreamRequestRegistration(Type requestType, Type? responseType, Ty
internal sealed class GenericStreamRequestCache
{
+ public GenericStreamRequestCache(IEnumerable registrations)
+ {
+ var requestType = typeof(TRequest);
+ if (requestType.IsGenericType)
+ {
+ var genericType = requestType.GetGenericTypeDefinition();
+ MatchingRegistrations = GenericTypeHelper.CacheMatchingRegistrations(
+ registrations,
+ r => r.RequestType,
+ genericType);
+ }
+ else
+ {
+ MatchingRegistrations = new List();
+ }
+ }
+
public Type? RequestHandlerType { get; set; }
+
+ public List MatchingRegistrations { get; }
}
internal sealed class GenericStreamRequestHandler : IStreamRequestHandler
where TRequest : IStreamRequest
{
private readonly GenericStreamRequestCache _cache;
- private readonly IEnumerable _enumerable;
private readonly IServiceProvider _serviceProvider;
private readonly IDefaultStreamRequestHandler? _defaultHandler;
public GenericStreamRequestHandler(
- IEnumerable enumerable,
IServiceProvider serviceProvider,
GenericStreamRequestCache cache,
IDefaultStreamRequestHandler? defaultHandler = null)
{
- _enumerable = enumerable;
_serviceProvider = serviceProvider;
_cache = cache;
_defaultHandler = defaultHandler;
@@ -80,17 +94,29 @@ public IAsyncEnumerable Handle(IServiceProvider provider, TRequest re
responseType = responseType.GetGenericTypeDefinition();
}
- foreach (var registration in _enumerable)
+
+ foreach (var registration in _cache.MatchingRegistrations)
{
- if (registration.RequestType != requestType ||
- registration.ResponseType is not null && registration.ResponseType != responseType)
+ if (registration.ResponseType is not null && registration.ResponseType != responseType)
{
continue;
}
- var type = registration.HandlerType.IsGenericType
- ? registration.HandlerType.MakeGenericType(arguments)
- : registration.HandlerType;
+ Type type;
+ if (registration.HandlerType.IsGenericType)
+ {
+ // Check if the generic arguments satisfy the handler's constraints
+ if (!GenericTypeHelper.CanMakeGenericType(registration.HandlerType, arguments))
+ {
+ continue;
+ }
+
+ type = registration.HandlerType.MakeGenericType(arguments);
+ }
+ else
+ {
+ type = registration.HandlerType;
+ }
var handler = (IStreamRequestHandler) _serviceProvider.GetRequiredService(type);
diff --git a/src/Mediator.DependencyInjection/Generic/Handlers/GenericTypeHelper.cs b/src/Mediator.DependencyInjection/Generic/Handlers/GenericTypeHelper.cs
new file mode 100644
index 0000000..2401948
--- /dev/null
+++ b/src/Mediator.DependencyInjection/Generic/Handlers/GenericTypeHelper.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+
+namespace Zapto.Mediator;
+
+internal static class GenericTypeHelper
+{
+ ///
+ /// Checks if a generic type definition can be instantiated with the given type arguments
+ /// by validating all generic constraints.
+ ///
+ public static bool CanMakeGenericType(Type genericTypeDefinition, Type[] typeArguments)
+ {
+ if (!genericTypeDefinition.IsGenericTypeDefinition)
+ {
+ return false;
+ }
+
+ var genericParams = genericTypeDefinition.GetGenericArguments();
+
+ if (genericParams.Length != typeArguments.Length)
+ {
+ return false;
+ }
+
+ // Try to actually make the generic type to let the CLR validate constraints
+ // This is the most reliable way to check all constraints including complex ones
+ try
+ {
+ var constructedType = genericTypeDefinition.MakeGenericType(typeArguments);
+ return constructedType != null;
+ }
+ catch (ArgumentException)
+ {
+ // MakeGenericType throws ArgumentException when constraints are violated
+ return false;
+ }
+ catch (NotSupportedException)
+ {
+ // MakeGenericType throws NotSupportedException for certain invalid scenarios
+ return false;
+ }
+ }
+
+ ///
+ /// Caches generic registrations for a specific type to avoid repeated enumeration.
+ ///
+ public static List CacheMatchingRegistrations(
+ IEnumerable registrations,
+ Func getNotificationType,
+ Type targetGenericType)
+ {
+ var cached = new List();
+ foreach (var registration in registrations)
+ {
+ if (getNotificationType(registration) == targetGenericType)
+ {
+ cached.Add(registration);
+ }
+ }
+ return cached;
+ }
+}
+
diff --git a/src/Mediator.DependencyInjection/Generic/MediatorBuilder.NotificationHandler.cs b/src/Mediator.DependencyInjection/Generic/MediatorBuilder.NotificationHandler.cs
index ac2d15b..9299bca 100644
--- a/src/Mediator.DependencyInjection/Generic/MediatorBuilder.NotificationHandler.cs
+++ b/src/Mediator.DependencyInjection/Generic/MediatorBuilder.NotificationHandler.cs
@@ -12,7 +12,7 @@ public IMediatorBuilder AddNotificationHandler(
Type handlerType,
RegistrationScope scope = RegistrationScope.Transient)
{
- if (notificationType.IsGenericType)
+ if (handlerType.IsGenericTypeDefinition)
{
_services.Add(new ServiceDescriptor(handlerType, handlerType, GetLifetime(scope)));
_services.AddSingleton(new GenericNotificationRegistration(notificationType, handlerType));
@@ -35,7 +35,8 @@ public IMediatorBuilder AddNotificationHandler(Type handlerType, RegistrationSco
{
var notificationType = type.GetGenericArguments()[0];
- if (notificationType.IsGenericType)
+ // Only convert to generic type definition if the handler itself is an open generic
+ if (handlerType.IsGenericTypeDefinition && notificationType.IsGenericType)
{
notificationType = notificationType.GetGenericTypeDefinition();
}
diff --git a/src/Mediator.DependencyInjection/Mediator.DependencyInjection.csproj b/src/Mediator.DependencyInjection/Mediator.DependencyInjection.csproj
index 6c582cc..bc4ebf9 100644
--- a/src/Mediator.DependencyInjection/Mediator.DependencyInjection.csproj
+++ b/src/Mediator.DependencyInjection/Mediator.DependencyInjection.csproj
@@ -1,7 +1,7 @@
- netstandard2.0;net8.0;net9.0
+ netstandard2.0;net8.0;net9.0;net10.0
Zapto.Mediator.DependencyInjection
Zapto.Mediator
10
diff --git a/src/Mediator.Hosting/Mediator.Hosting.csproj b/src/Mediator.Hosting/Mediator.Hosting.csproj
index 1aac0e7..fc932a6 100644
--- a/src/Mediator.Hosting/Mediator.Hosting.csproj
+++ b/src/Mediator.Hosting/Mediator.Hosting.csproj
@@ -1,7 +1,7 @@
- netstandard2.0;net8.0;net9.0
+ netstandard2.0;net8.0;net9.0;net10.0
Zapto.Mediator.Hosting
Zapto.Mediator
10
diff --git a/src/Mediator/Mediator.csproj b/src/Mediator/Mediator.csproj
index bad72b5..ec006dd 100644
--- a/src/Mediator/Mediator.csproj
+++ b/src/Mediator/Mediator.csproj
@@ -18,9 +18,9 @@
-
-
-
+
+
+
diff --git a/tests/Mediator.DependencyInjection.Tests/Generics/ConstraintValidationTest.cs b/tests/Mediator.DependencyInjection.Tests/Generics/ConstraintValidationTest.cs
new file mode 100644
index 0000000..8189c02
--- /dev/null
+++ b/tests/Mediator.DependencyInjection.Tests/Generics/ConstraintValidationTest.cs
@@ -0,0 +1,938 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+using Zapto.Mediator;
+#if NET7_0_OR_GREATER
+using System.Numerics;
+#endif
+
+namespace Mediator.DependencyInjection.Tests.Generics;
+
+///
+/// Tests to validate that generic constraint checking works correctly for all handler types.
+/// This ensures handlers with unsatisfied constraints are properly skipped.
+///
+public class ConstraintValidationTest
+{
+ #region Test Types and Interfaces
+
+ public interface ISpecialInterface { }
+ public class ClassWithInterface : ISpecialInterface { }
+ public class ClassWithoutInterface { }
+
+ public class BaseClass { }
+ public class DerivedClass : BaseClass { }
+
+ #endregion
+
+ #region Notification Tests
+
+ public record TestNotification(T Value) : INotification;
+
+ public class NotificationResult
+ {
+ public List HandlersCalled { get; } = new();
+ }
+
+ // Handler with interface constraint
+ public class NotificationHandlerWithInterfaceConstraint : INotificationHandler>
+ where T : ISpecialInterface
+ {
+ private readonly NotificationResult _result;
+
+ public NotificationHandlerWithInterfaceConstraint(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, TestNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add("InterfaceConstraint");
+ return default;
+ }
+ }
+
+ // Handler with base class constraint
+ public class NotificationHandlerWithBaseClassConstraint : INotificationHandler>
+ where T : BaseClass
+ {
+ private readonly NotificationResult _result;
+
+ public NotificationHandlerWithBaseClassConstraint(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, TestNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add("BaseClassConstraint");
+ return default;
+ }
+ }
+
+ // Handler with struct constraint
+ public class NotificationHandlerWithStructConstraint : INotificationHandler>
+ where T : struct
+ {
+ private readonly NotificationResult _result;
+
+ public NotificationHandlerWithStructConstraint(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, TestNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add("StructConstraint");
+ return default;
+ }
+ }
+
+ // Handler with class constraint
+ public class NotificationHandlerWithClassConstraint : INotificationHandler>
+ where T : class
+ {
+ private readonly NotificationResult _result;
+
+ public NotificationHandlerWithClassConstraint(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, TestNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add("ClassConstraint");
+ return default;
+ }
+ }
+
+#if NET7_0_OR_GREATER
+ // Handler with self-referential constraint (like INumber)
+ public class NotificationHandlerWithNumberConstraint : INotificationHandler>
+ where T : INumber
+ {
+ private readonly NotificationResult _result;
+
+ public NotificationHandlerWithNumberConstraint(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, TestNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add("NumberConstraint");
+ return default;
+ }
+ }
+#endif
+
+ [Fact]
+ public async Task Notification_InterfaceConstraint_OnlyMatchesTypesWithInterface()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(NotificationHandlerWithInterfaceConstraint<>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke handler - ClassWithInterface implements ISpecialInterface
+ await mediator.Publish(new TestNotification(new ClassWithInterface()));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("InterfaceConstraint", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke handler - ClassWithoutInterface doesn't implement ISpecialInterface
+ await mediator.Publish(new TestNotification(new ClassWithoutInterface()));
+ Assert.Empty(result.HandlersCalled);
+ }
+
+ [Fact]
+ public async Task Notification_BaseClassConstraint_OnlyMatchesDerivedTypes()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(NotificationHandlerWithBaseClassConstraint<>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke handler - DerivedClass inherits from BaseClass
+ await mediator.Publish(new TestNotification(new DerivedClass()));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("BaseClassConstraint", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should invoke handler - BaseClass itself
+ await mediator.Publish(new TestNotification(new BaseClass()));
+ Assert.Single(result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke handler - string doesn't inherit from BaseClass
+ await mediator.Publish(new TestNotification("test"));
+ Assert.Empty(result.HandlersCalled);
+ }
+
+ [Fact]
+ public async Task Notification_StructConstraint_OnlyMatchesValueTypes()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(NotificationHandlerWithStructConstraint<>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke handler - int is a struct
+ await mediator.Publish(new TestNotification(42));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("StructConstraint", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke handler - string is a reference type
+ await mediator.Publish(new TestNotification("test"));
+ Assert.Empty(result.HandlersCalled);
+ }
+
+ [Fact]
+ public async Task Notification_ClassConstraint_OnlyMatchesReferenceTypes()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(NotificationHandlerWithClassConstraint<>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke handler - string is a reference type
+ await mediator.Publish(new TestNotification("test"));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("ClassConstraint", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke handler - int is a value type
+ await mediator.Publish(new TestNotification(42));
+ Assert.Empty(result.HandlersCalled);
+ }
+
+#if NET7_0_OR_GREATER
+ [Fact]
+ public async Task Notification_NumberConstraint_OnlyMatchesNumericTypes()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(NotificationHandlerWithNumberConstraint<>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke handler - int implements INumber
+ await mediator.Publish(new TestNotification(42));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("NumberConstraint", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should invoke handler - double implements INumber
+ await mediator.Publish(new TestNotification(3.14));
+ Assert.Single(result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke handler - string doesn't implement INumber
+ await mediator.Publish(new TestNotification("test"));
+ Assert.Empty(result.HandlersCalled);
+ }
+#endif
+
+ [Fact]
+ public async Task Notification_MultipleHandlersWithDifferentConstraints_OnlyInvokesMatching()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(NotificationHandlerWithInterfaceConstraint<>));
+ b.AddNotificationHandler(typeof(NotificationHandlerWithClassConstraint<>));
+ b.AddNotificationHandler(typeof(NotificationHandlerWithStructConstraint<>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // ClassWithInterface: should invoke InterfaceConstraint and ClassConstraint (reference type with interface)
+ await mediator.Publish(new TestNotification(new ClassWithInterface()));
+ Assert.Equal(2, result.HandlersCalled.Count);
+ Assert.Contains("InterfaceConstraint", result.HandlersCalled);
+ Assert.Contains("ClassConstraint", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // int: should only invoke StructConstraint (value type)
+ await mediator.Publish(new TestNotification(42));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("StructConstraint", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // ClassWithoutInterface: should only invoke ClassConstraint (reference type without interface)
+ await mediator.Publish(new TestNotification(new ClassWithoutInterface()));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("ClassConstraint", result.HandlersCalled);
+ }
+
+ #endregion
+
+ #region Request Tests
+
+ public record TestRequest(T Value) : IRequest;
+
+ // Handler with interface constraint for requests
+ public class RequestHandlerWithInterfaceConstraint : IRequestHandler, T>
+ where T : ISpecialInterface
+ {
+ public ValueTask Handle(IServiceProvider provider, TestRequest request, CancellationToken cancellationToken)
+ {
+ return new ValueTask(request.Value);
+ }
+ }
+
+#if NET7_0_OR_GREATER
+ // Handler with numeric constraint for requests
+ public class RequestHandlerWithNumberConstraint : IRequestHandler, T>
+ where T : INumber
+ {
+ public ValueTask Handle(IServiceProvider provider, TestRequest request, CancellationToken cancellationToken)
+ {
+ return new ValueTask(request.Value);
+ }
+ }
+#endif
+
+ [Fact]
+ public async Task Request_InterfaceConstraint_OnlyMatchesTypesWithInterface()
+ {
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddRequestHandler(typeof(RequestHandlerWithInterfaceConstraint<>));
+ })
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should work - ClassWithInterface implements ISpecialInterface
+ var result = await mediator.Send(new TestRequest(new ClassWithInterface()));
+ Assert.NotNull(result);
+
+ // Should throw - ClassWithoutInterface doesn't implement ISpecialInterface
+ await Assert.ThrowsAsync(async () =>
+ await mediator.Send(new TestRequest(new ClassWithoutInterface())));
+ }
+
+#if NET7_0_OR_GREATER
+ [Fact]
+ public async Task Request_NumberConstraint_OnlyMatchesNumericTypes()
+ {
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddRequestHandler(typeof(RequestHandlerWithNumberConstraint<>));
+ })
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should work - int implements INumber
+ var intResult = await mediator.Send(new TestRequest(42));
+ Assert.Equal(42, intResult);
+
+ // Should work - double implements INumber
+ var doubleResult = await mediator.Send(new TestRequest(3.14));
+ Assert.Equal(3.14, doubleResult);
+
+ // Should throw - string doesn't implement INumber
+ await Assert.ThrowsAsync(async () =>
+ await mediator.Send(new TestRequest("test")));
+ }
+#endif
+
+ #endregion
+
+ #region Stream Request Tests
+
+ public record TestStreamRequest(T Value) : IStreamRequest;
+
+ // Handler with interface constraint for stream requests
+ public class StreamRequestHandlerWithInterfaceConstraint : IStreamRequestHandler, T>
+ where T : ISpecialInterface
+ {
+#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+ public async IAsyncEnumerable Handle(IServiceProvider provider, TestStreamRequest request, [EnumeratorCancellation] CancellationToken cancellationToken)
+#pragma warning restore CS1998
+ {
+ yield return request.Value;
+ }
+ }
+
+#if NET7_0_OR_GREATER
+ // Handler with numeric constraint for stream requests
+ public class StreamRequestHandlerWithNumberConstraint : IStreamRequestHandler, T>
+ where T : INumber
+ {
+#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+ public async IAsyncEnumerable Handle(IServiceProvider provider, TestStreamRequest request, [EnumeratorCancellation] CancellationToken cancellationToken)
+#pragma warning restore CS1998
+ {
+ yield return request.Value;
+ }
+ }
+#endif
+
+ [Fact]
+ public async Task StreamRequest_InterfaceConstraint_OnlyMatchesTypesWithInterface()
+ {
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddStreamRequestHandler(typeof(StreamRequestHandlerWithInterfaceConstraint<>));
+ })
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should work - ClassWithInterface implements ISpecialInterface
+ var result = await mediator.CreateStream(new TestStreamRequest(new ClassWithInterface()))
+ .FirstOrDefaultAsync();
+ Assert.NotNull(result);
+
+ // Should throw - ClassWithoutInterface doesn't implement ISpecialInterface
+ await Assert.ThrowsAsync(async () =>
+ {
+ await foreach (var item in mediator.CreateStream(new TestStreamRequest(new ClassWithoutInterface())))
+ {
+ // Should not reach here
+ }
+ });
+ }
+
+#if NET7_0_OR_GREATER
+ [Fact]
+ public async Task StreamRequest_NumberConstraint_OnlyMatchesNumericTypes()
+ {
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddStreamRequestHandler(typeof(StreamRequestHandlerWithNumberConstraint<>));
+ })
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should work - int implements INumber
+ var intResult = await mediator.CreateStream(new TestStreamRequest(42)).FirstOrDefaultAsync();
+ Assert.Equal(42, intResult);
+
+ // Should work - long implements INumber
+ var longResult = await mediator.CreateStream(new TestStreamRequest(100L)).FirstOrDefaultAsync();
+ Assert.Equal(100L, longResult);
+
+ // Should throw - string doesn't implement INumber
+ await Assert.ThrowsAsync(async () =>
+ {
+ await foreach (var item in mediator.CreateStream(new TestStreamRequest("test")))
+ {
+ // Should not reach here
+ }
+ });
+ }
+#endif
+
+ #endregion
+
+ #region Closed Generic Handler Tests
+
+ // Test that closed generic handlers (non-generic type definition) work correctly
+ public class ClosedGenericNotificationHandler : INotificationHandler>
+ {
+ private readonly NotificationResult _result;
+
+ public ClosedGenericNotificationHandler(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, TestNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add("ClosedGeneric");
+ return default;
+ }
+ }
+
+ [Fact]
+ public async Task ClosedGenericHandler_IsRegisteredAsConcreteHandler()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler();
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke the closed generic handler
+ await mediator.Publish(new TestNotification(new ClassWithInterface()));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("ClosedGeneric", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke for other types
+ await mediator.Publish(new TestNotification(new ClassWithoutInterface()));
+ Assert.Empty(result.HandlersCalled);
+ }
+
+ #endregion
+
+ #region Nested Generic Tests
+
+ public record NestedNotification(List Values) : INotification;
+
+ // Handler for nested generic with constraint on inner type
+ public class NestedGenericNotificationHandler : INotificationHandler>
+ where T : ISpecialInterface
+ {
+ private readonly NotificationResult _result;
+
+ public NestedGenericNotificationHandler(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, NestedNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add($"NestedGeneric<{typeof(T).Name}>");
+ return default;
+ }
+ }
+
+ [Fact]
+ public async Task Notification_NestedGeneric_ConstraintAppliedToInnerType()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(NestedGenericNotificationHandler<>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke handler - List where inner type satisfies constraint
+ await mediator.Publish(new NestedNotification(new List()));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("NestedGeneric", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke handler - List where inner type doesn't satisfy constraint
+ await mediator.Publish(new NestedNotification(new List()));
+ Assert.Empty(result.HandlersCalled);
+ }
+
+ #endregion
+
+ #region Multiple Type Parameters Tests
+
+ public record MultiParamNotification(TKey Key, TValue Value) : INotification;
+
+ // Handler with constraints on multiple type parameters
+ public class MultiParamConstraintHandler : INotificationHandler>
+ where TKey : ISpecialInterface
+ where TValue : struct
+ {
+ private readonly NotificationResult _result;
+
+ public MultiParamConstraintHandler(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, MultiParamNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add($"MultiParam<{typeof(TKey).Name},{typeof(TValue).Name}>");
+ return default;
+ }
+ }
+
+ // Handler with partial constraints (only on one parameter)
+ public class PartialConstraintHandler : INotificationHandler>
+ where TKey : ISpecialInterface
+ {
+ private readonly NotificationResult _result;
+
+ public PartialConstraintHandler(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, MultiParamNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add($"PartialConstraint<{typeof(TKey).Name},{typeof(TValue).Name}>");
+ return default;
+ }
+ }
+
+ [Fact]
+ public async Task Notification_MultipleTypeParameters_AllConstraintsMustBeSatisfied()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(MultiParamConstraintHandler<,>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke - both constraints satisfied (ISpecialInterface + struct)
+ await mediator.Publish(new MultiParamNotification(new ClassWithInterface(), 42));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("MultiParam", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke - first constraint satisfied but second not (string is not struct)
+ await mediator.Publish(new MultiParamNotification(new ClassWithInterface(), "test"));
+ Assert.Empty(result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke - second constraint satisfied but first not
+ await mediator.Publish(new MultiParamNotification(new ClassWithoutInterface(), 42));
+ Assert.Empty(result.HandlersCalled);
+ }
+
+ [Fact]
+ public async Task Notification_MultipleTypeParameters_PartialConstraintsWork()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(PartialConstraintHandler<,>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke - first parameter satisfies constraint, second has no constraint
+ await mediator.Publish(new MultiParamNotification(new ClassWithInterface(), "test"));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("PartialConstraint", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should invoke - works with value types too
+ await mediator.Publish(new MultiParamNotification(new ClassWithInterface(), 42));
+ Assert.Single(result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke - first parameter doesn't satisfy constraint
+ await mediator.Publish(new MultiParamNotification(new ClassWithoutInterface(), "test"));
+ Assert.Empty(result.HandlersCalled);
+ }
+
+ [Fact]
+ public async Task Notification_MultipleTypeParameters_MultipleHandlersWithDifferentConstraints()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(MultiParamConstraintHandler<,>)); // Both constrained
+ b.AddNotificationHandler(typeof(PartialConstraintHandler<,>)); // Only first constrained
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Both handlers should match
+ await mediator.Publish(new MultiParamNotification(new ClassWithInterface(), 42));
+ Assert.Equal(2, result.HandlersCalled.Count);
+ Assert.Contains("MultiParam", result.HandlersCalled);
+ Assert.Contains("PartialConstraint", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Only partial constraint handler should match (string is not struct)
+ await mediator.Publish(new MultiParamNotification(new ClassWithInterface(), "test"));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("PartialConstraint", result.HandlersCalled);
+ }
+
+ #endregion
+
+ #region Generic Parameter Order Tests
+
+ public record OrderedNotification(T1 First, T2 Second) : INotification;
+
+ // Handler with specific order of constraints
+ public class OrderedConstraintHandler : INotificationHandler>
+ where T1 : struct
+ where T2 : class
+ {
+ private readonly NotificationResult _result;
+
+ public OrderedConstraintHandler(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, OrderedNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add($"Ordered<{typeof(T1).Name},{typeof(T2).Name}>");
+ return default;
+ }
+ }
+
+ [Fact]
+ public async Task Notification_GenericParameterOrder_ConstraintsApplyToCorrectParameters()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(OrderedConstraintHandler<,>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke - correct order: struct, class
+ await mediator.Publish(new OrderedNotification(42, "test"));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("Ordered", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke - wrong order: class, struct
+ await mediator.Publish(new OrderedNotification("test", 42));
+ Assert.Empty(result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke - both reference types
+ await mediator.Publish(new OrderedNotification("test", new object()));
+ Assert.Empty(result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke - both value types
+ await mediator.Publish(new OrderedNotification(42, 3.14));
+ Assert.Empty(result.HandlersCalled);
+ }
+
+ #endregion
+
+ #region Complex Nested Scenarios
+
+ public record ComplexNestedNotification(Dictionary> Data) : INotification;
+
+ public class ComplexNestedHandler : INotificationHandler>
+ where T : BaseClass
+ {
+ private readonly NotificationResult _result;
+
+ public ComplexNestedHandler(NotificationResult result)
+ {
+ _result = result;
+ }
+
+ public ValueTask Handle(IServiceProvider provider, ComplexNestedNotification notification, CancellationToken cancellationToken)
+ {
+ _result.HandlersCalled.Add($"ComplexNested<{typeof(T).Name}>");
+ return default;
+ }
+ }
+
+ [Fact]
+ public async Task Notification_ComplexNestedGenerics_ConstraintValidationWorks()
+ {
+ var result = new NotificationResult();
+
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(typeof(ComplexNestedHandler<>));
+ })
+ .AddSingleton(result)
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should invoke - DerivedClass satisfies BaseClass constraint
+ await mediator.Publish(new ComplexNestedNotification(new Dictionary>()));
+ Assert.Single(result.HandlersCalled);
+ Assert.Contains("ComplexNested", result.HandlersCalled);
+
+ result.HandlersCalled.Clear();
+
+ // Should NOT invoke - string doesn't inherit from BaseClass
+ await mediator.Publish(new ComplexNestedNotification(new Dictionary>()));
+ Assert.Empty(result.HandlersCalled);
+ }
+
+ #endregion
+
+ #region Request Multi-Parameter Tests
+
+ public record MultiParamRequest(T1 Key, T2 Value) : IRequest;
+
+ public class MultiParamRequestHandler : IRequestHandler, string>
+ where T1 : ISpecialInterface
+ where T2 : struct
+ {
+ public ValueTask Handle(IServiceProvider provider, MultiParamRequest request, CancellationToken cancellationToken)
+ {
+ return new ValueTask($"{typeof(T1).Name}-{typeof(T2).Name}");
+ }
+ }
+
+ [Fact]
+ public async Task Request_MultipleTypeParameters_AllConstraintsMustBeSatisfied()
+ {
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddRequestHandler(typeof(MultiParamRequestHandler<,>));
+ })
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should work - both constraints satisfied
+ var result = await mediator.Send(new MultiParamRequest(new ClassWithInterface(), 42));
+ Assert.Equal("ClassWithInterface-Int32", result);
+
+ // Should throw - first constraint not satisfied
+ await Assert.ThrowsAsync(async () =>
+ await mediator.Send(new MultiParamRequest(new ClassWithoutInterface(), 42)));
+
+ // Should throw - second constraint not satisfied
+ await Assert.ThrowsAsync(async () =>
+ await mediator.Send(new MultiParamRequest(new ClassWithInterface(), "test")));
+ }
+
+ #endregion
+
+ #region Stream Request Multi-Parameter Tests
+
+ public record MultiParamStreamRequest(T1 Key, T2 Value) : IStreamRequest;
+
+ public class MultiParamStreamRequestHandler : IStreamRequestHandler, string>
+ where T1 : ISpecialInterface
+ where T2 : BaseClass
+ {
+#pragma warning disable CS1998
+ public async IAsyncEnumerable Handle(IServiceProvider provider, MultiParamStreamRequest request, [EnumeratorCancellation] CancellationToken cancellationToken)
+#pragma warning restore CS1998
+ {
+ yield return $"{typeof(T1).Name}-{typeof(T2).Name}";
+ }
+ }
+
+ [Fact]
+ public async Task StreamRequest_MultipleTypeParameters_AllConstraintsMustBeSatisfied()
+ {
+ await using var provider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddStreamRequestHandler(typeof(MultiParamStreamRequestHandler<,>));
+ })
+ .BuildServiceProvider();
+
+ var mediator = provider.GetRequiredService();
+
+ // Should work - both constraints satisfied
+ var result = await mediator.CreateStream(new MultiParamStreamRequest(new ClassWithInterface(), new DerivedClass()))
+ .FirstOrDefaultAsync();
+ Assert.Equal("ClassWithInterface-DerivedClass", result);
+
+ // Should throw - first constraint not satisfied
+ await Assert.ThrowsAsync(async () =>
+ {
+ await foreach (var item in mediator.CreateStream(new MultiParamStreamRequest(new ClassWithoutInterface(), new DerivedClass())))
+ {
+ // Should not reach here
+ }
+ });
+
+ // Should throw - second constraint not satisfied
+ await Assert.ThrowsAsync(async () =>
+ {
+ await foreach (var item in mediator.CreateStream(new MultiParamStreamRequest(new ClassWithInterface(), new ClassWithoutInterface())))
+ {
+ // Should not reach here
+ }
+ });
+ }
+
+ #endregion
+}
+
diff --git a/tests/Mediator.DependencyInjection.Tests/Generics/NotificationGenericTest.cs b/tests/Mediator.DependencyInjection.Tests/Generics/NotificationGenericTest.cs
index bf64590..1397444 100644
--- a/tests/Mediator.DependencyInjection.Tests/Generics/NotificationGenericTest.cs
+++ b/tests/Mediator.DependencyInjection.Tests/Generics/NotificationGenericTest.cs
@@ -1,4 +1,7 @@
-using System.Threading.Tasks;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;
@@ -51,4 +54,144 @@ await provider
.GetRequiredService()
.Publish(new GenericNotification(expected));
}
+
+ [Fact]
+ public async Task TestGenericRegistration()
+ {
+ var handler = Substitute.For>>();
+
+ var serviceProvider = new ServiceCollection()
+ .AddMediator(b => b.AddNotificationHandler(handler))
+ .BuildServiceProvider();
+
+ var mediator = serviceProvider.GetRequiredService();
+
+ await mediator.Publish(new GenericNotification("test"));
+ await mediator.Publish(new GenericNotification(5));
+
+ Assert.Single(handler.ReceivedCalls());
+ }
+
+ [Fact]
+ public async Task TestGenericRegistrationNamespace()
+ {
+ var handler = Substitute.For>>();
+ var ns = new MediatorNamespace("test");
+
+ var serviceProvider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNamespace(ns, inner =>
+ {
+ inner.AddNotificationHandler(handler);
+ });
+ })
+ .BuildServiceProvider();
+
+ var mediator = serviceProvider.GetRequiredService();
+
+ await mediator.Publish(ns, new GenericNotification("test"));
+ await mediator.Publish(ns, new GenericNotification(5));
+
+ Assert.Single(handler.ReceivedCalls());
+ }
+
+ [Fact]
+ public async Task TestNestedGenericRegistration()
+ {
+ var handler = Substitute.For>>>();
+
+ var serviceProvider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNotificationHandler(handler);
+ })
+ .BuildServiceProvider();
+
+ var mediator = serviceProvider.GetRequiredService();
+
+ await mediator.Publish(new GenericNotification>("test"));
+ await mediator.Publish(new GenericNotification>(5));
+
+ Assert.Single(handler.ReceivedCalls());
+ }
+
+ [Fact]
+ public async Task TestNestedGenericRegistrationNamespace()
+ {
+ var handler = Substitute.For>>>();
+ var ns = new MediatorNamespace("test");
+
+ var serviceProvider = new ServiceCollection()
+ .AddMediator(b =>
+ {
+ b.AddNamespace(ns, inner =>
+ {
+ inner.AddNotificationHandler(handler);
+ });
+ })
+ .BuildServiceProvider();
+
+ var mediator = serviceProvider.GetRequiredService();
+
+ await mediator.Publish(ns, new GenericNotification>("test"));
+ await mediator.Publish(ns, new GenericNotification