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>(5)); + + Assert.Single(handler.ReceivedCalls()); + } + + [Fact] + public async Task TestGenericConstraintRegistrationNamespace() + { + var serviceProvider = new ServiceCollection() + .AddMediator(b => + { + b.AddNotificationHandler(typeof(GenericNotificationHandlerConstraintInterfaceA<>)); + }) + .BuildServiceProvider(); + + var mediator = serviceProvider.GetRequiredService(); + + await mediator.Publish(new GenericNotification(new ClassImplementingA())); + await mediator.Publish(new GenericNotification(new ClassImplementingB())); + } + + public class GenericNotificationHandlerConstraintInterfaceA + : INotificationHandler> where T : IInterfaceA + { + public int Count { get; set; } + + public ValueTask Handle(IServiceProvider provider, GenericNotification notification, CancellationToken cancellationToken) + { + Count++; + return default; + } + } + + [Fact] + public async Task TestGenericDirect() + { + var serviceProvider = new ServiceCollection() + .AddMediator(b => + { + b.AddNotificationHandler(); + }) + .BuildServiceProvider(); + + var mediator = serviceProvider.GetRequiredService(); + + await mediator.Publish(new GenericNotification(new ClassImplementingA())); + await mediator.Publish(new GenericNotification(new ClassImplementingB())); + } + + public class NotificationHandlerDirect : INotificationHandler> + { + public int Count { get; set; } + + public ValueTask Handle(IServiceProvider provider, GenericNotification notification, CancellationToken cancellationToken) + { + Count++; + return default; + } + } } diff --git a/tests/Mediator.DependencyInjection.Tests/Generics/Notifications/GenericNotification.cs b/tests/Mediator.DependencyInjection.Tests/Generics/Notifications/GenericNotification.cs index 4c730bf..607ad11 100644 --- a/tests/Mediator.DependencyInjection.Tests/Generics/Notifications/GenericNotification.cs +++ b/tests/Mediator.DependencyInjection.Tests/Generics/Notifications/GenericNotification.cs @@ -2,4 +2,17 @@ namespace Mediator.DependencyInjection.Tests.Generics; -public record GenericNotification(T Value) : INotification; \ No newline at end of file +public record GenericNotification(T Value) : INotification; + +public interface IInterface; +public interface IInterfaceA : IInterface; +public interface IInterfaceB : IInterface; + +public class ClassImplementingInterface : IInterface; +public class ClassImplementingA : IInterfaceA; +public class ClassImplementingB : IInterfaceB; + +public record Wrapper(T Value) +{ + public static implicit operator Wrapper(T value) => new(value); +} \ No newline at end of file diff --git a/tests/Mediator.DependencyInjection.Tests/Generics/RequestGenericTest.cs b/tests/Mediator.DependencyInjection.Tests/Generics/RequestGenericTest.cs index 4b1ef47..78e2903 100644 --- a/tests/Mediator.DependencyInjection.Tests/Generics/RequestGenericTest.cs +++ b/tests/Mediator.DependencyInjection.Tests/Generics/RequestGenericTest.cs @@ -113,4 +113,18 @@ public async Task TestVoidRequestHandler() Assert.True(handler.CallCount > 0, "Handler was not called"); } + + [Fact] + public async Task TestThrowNotFound() + { + var handler = Substitute.For>>(); + + var serviceProvider = new ServiceCollection() + .AddMediator(b => b.AddRequestHandler(handler)) + .BuildServiceProvider(); + + var mediator = serviceProvider.GetRequiredService(); + + await Assert.ThrowsAsync(async () => await mediator.Send(new GenericVoidRequest(1))); + } } diff --git a/tests/Mediator.DependencyInjection.Tests/Mediator.DependencyInjection.Tests.csproj b/tests/Mediator.DependencyInjection.Tests/Mediator.DependencyInjection.Tests.csproj index f2e9020..f5c19b8 100644 --- a/tests/Mediator.DependencyInjection.Tests/Mediator.DependencyInjection.Tests.csproj +++ b/tests/Mediator.DependencyInjection.Tests/Mediator.DependencyInjection.Tests.csproj @@ -1,7 +1,7 @@ - net472;net8.0;net9.0 + net472;net8.0;net9.0;net10.0 12 enable false @@ -17,7 +17,7 @@ - + diff --git a/tests/Mediator.Hosting.Tests/Mediator.Hosting.Tests.csproj b/tests/Mediator.Hosting.Tests/Mediator.Hosting.Tests.csproj index d7e6a24..0017f19 100644 --- a/tests/Mediator.Hosting.Tests/Mediator.Hosting.Tests.csproj +++ b/tests/Mediator.Hosting.Tests/Mediator.Hosting.Tests.csproj @@ -1,7 +1,7 @@  - net472;net8.0;net9.0 + net472;net8.0;net9.0;net10.0 12 enable false diff --git a/tests/Mediator.SourceGenerator.Tests/Mediator.SourceGenerator.Tests.csproj b/tests/Mediator.SourceGenerator.Tests/Mediator.SourceGenerator.Tests.csproj index ba53727..bc9abf9 100644 --- a/tests/Mediator.SourceGenerator.Tests/Mediator.SourceGenerator.Tests.csproj +++ b/tests/Mediator.SourceGenerator.Tests/Mediator.SourceGenerator.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable false