Skip to content

Commit d045918

Browse files
committed
BugFix: Constraint test
1 parent 9e1e33a commit d045918

16 files changed

Lines changed: 1301 additions & 38 deletions

src/Directory.Build.props

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<Project>
22

33
<PropertyGroup>
4-
<Version>1.0.0-alpha.23</Version>
5-
<PackageVersion>1.0.0-alpha.23</PackageVersion>
4+
<Version>1.0.0-alpha.24</Version>
5+
<PackageVersion>1.0.0-alpha.24</PackageVersion>
66
<Authors>Zapto</Authors>
77
<RepositoryUrl>https://github.com/zapto-dev/Mediator</RepositoryUrl>
88
<Copyright>Copyright © 2025 Zapto</Copyright>
@@ -19,4 +19,8 @@
1919
<ExtensionVersion>9.0.0</ExtensionVersion>
2020
</PropertyGroup>
2121

22+
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0'">
23+
<ExtensionVersion>10.0.0</ExtensionVersion>
24+
</PropertyGroup>
25+
2226
</Project>

src/Mediator.DependencyInjection/Generic/Handlers/GenericNotificationHandler.cs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ internal sealed class GenericNotificationCache<TNotification> : INotificationCac
3939
{
4040
public List<Type>? HandlerTypes { get; set; }
4141

42+
public List<GenericNotificationRegistration>? MatchingRegistrations { get; set; }
43+
4244
public List<IHandlerRegistration> Registrations { get; } = new();
4345

4446
public SemaphoreSlim Lock { get; } = new(1, 1);
@@ -95,17 +97,32 @@ public async ValueTask<bool> Handle(IServiceProvider provider, TNotification not
9597
var genericType = notificationType.GetGenericTypeDefinition();
9698
var handlerTypes = new List<Type>();
9799

98-
foreach (var registration in _enumerable)
100+
// Cache matching registrations on first call to avoid enumerating on every Handle call
101+
if (_cache.MatchingRegistrations == null)
102+
{
103+
_cache.MatchingRegistrations = GenericTypeHelper.CacheMatchingRegistrations(
104+
_enumerable,
105+
r => r.NotificationType,
106+
genericType);
107+
}
108+
109+
foreach (var registration in _cache.MatchingRegistrations)
99110
{
100-
if (registration.NotificationType != genericType)
111+
Type type;
112+
if (registration.HandlerType.IsGenericType)
113+
{
114+
if (!GenericTypeHelper.CanMakeGenericType(registration.HandlerType, arguments))
115+
{
116+
continue;
117+
}
118+
119+
type = registration.HandlerType.MakeGenericType(arguments);
120+
}
121+
else
101122
{
102-
continue;
123+
type = registration.HandlerType;
103124
}
104125

105-
var type = registration.HandlerType.IsGenericType
106-
? registration.HandlerType.MakeGenericType(arguments)
107-
: registration.HandlerType;
108-
109126
var handler = _serviceProvider.GetRequiredService(type);
110127

111128
await ((INotificationHandler<TNotification>)handler!).Handle(provider, notification, ct);

src/Mediator.DependencyInjection/Generic/Handlers/GenericRequestHandler.cs

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ public GenericRequestRegistration(Type requestType, Type? responseType, Type han
2828
internal sealed class GenericRequestCache<TRequest, TResponse>
2929
{
3030
public Type? RequestHandlerType { get; set; }
31+
32+
public List<GenericRequestRegistration>? MatchingRegistrations { get; set; }
3133
}
3234

3335
internal sealed class GenericRequestCache<TRequest>
3436
{
3537
public Type? RequestHandlerType { get; set; }
38+
39+
public List<GenericRequestRegistration>? MatchingRegistrations { get; set; }
3640
}
3741

3842
internal sealed class GenericRequestHandler<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
@@ -86,17 +90,37 @@ public async ValueTask<TResponse> Handle(IServiceProvider provider, TRequest req
8690
responseType = responseType.GetGenericTypeDefinition();
8791
}
8892

89-
foreach (var registration in _enumerable)
93+
// Cache matching registrations on first call to avoid enumerating on every Handle call
94+
if (_cache.MatchingRegistrations == null)
95+
{
96+
_cache.MatchingRegistrations = GenericTypeHelper.CacheMatchingRegistrations(
97+
_enumerable,
98+
r => r.RequestType,
99+
requestType);
100+
}
101+
102+
foreach (var registration in _cache.MatchingRegistrations)
90103
{
91-
if (registration.RequestType != requestType ||
92-
registration.ResponseType is not null && registration.ResponseType != responseType)
104+
if (registration.ResponseType is not null && registration.ResponseType != responseType)
93105
{
94106
continue;
95107
}
96108

97-
var type = registration.HandlerType.IsGenericType
98-
? registration.HandlerType.MakeGenericType(arguments)
99-
: registration.HandlerType;
109+
Type type;
110+
if (registration.HandlerType.IsGenericType)
111+
{
112+
// Check if the generic arguments satisfy the handler's constraints
113+
if (!GenericTypeHelper.CanMakeGenericType(registration.HandlerType, arguments))
114+
{
115+
continue;
116+
}
117+
118+
type = registration.HandlerType.MakeGenericType(arguments);
119+
}
120+
else
121+
{
122+
type = registration.HandlerType;
123+
}
100124

101125
var handler = (IRequestHandler<TRequest, TResponse>) _serviceProvider.GetRequiredService(type);
102126

@@ -168,15 +192,38 @@ public async ValueTask Handle(IServiceProvider provider, TRequest request, Cance
168192

169193
requestType = requestType.GetGenericTypeDefinition();
170194

171-
foreach (var registration in _enumerable)
195+
// Cache matching registrations on first call to avoid enumerating on every Handle call
196+
if (_cache.MatchingRegistrations == null)
172197
{
173-
if (registration.RequestType != requestType ||
174-
registration.ResponseType != typeof(Unit))
198+
_cache.MatchingRegistrations = GenericTypeHelper.CacheMatchingRegistrations(
199+
_enumerable,
200+
r => r.RequestType,
201+
requestType);
202+
}
203+
204+
foreach (var registration in _cache.MatchingRegistrations)
205+
{
206+
if (registration.ResponseType != typeof(Unit))
175207
{
176208
continue;
177209
}
178210

179-
var type = registration.HandlerType.MakeGenericType(arguments);
211+
Type type;
212+
if (registration.HandlerType.IsGenericType)
213+
{
214+
// Check if the generic arguments satisfy the handler's constraints
215+
if (!GenericTypeHelper.CanMakeGenericType(registration.HandlerType, arguments))
216+
{
217+
continue;
218+
}
219+
220+
type = registration.HandlerType.MakeGenericType(arguments);
221+
}
222+
else
223+
{
224+
type = registration.HandlerType;
225+
}
226+
180227
var handler = (IRequestHandler<TRequest>) _serviceProvider.GetRequiredService(type);
181228

182229
_cache.RequestHandlerType = type;

src/Mediator.DependencyInjection/Generic/Handlers/GenericStreamRequestHandler.cs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public GenericStreamRequestRegistration(Type requestType, Type? responseType, Ty
2727
internal sealed class GenericStreamRequestCache<TRequest, TResponse>
2828
{
2929
public Type? RequestHandlerType { get; set; }
30+
31+
public List<GenericStreamRequestRegistration>? MatchingRegistrations { get; set; }
3032
}
3133

3234
internal sealed class GenericStreamRequestHandler<TRequest, TResponse> : IStreamRequestHandler<TRequest, TResponse>
@@ -80,17 +82,37 @@ public IAsyncEnumerable<TResponse> Handle(IServiceProvider provider, TRequest re
8082
responseType = responseType.GetGenericTypeDefinition();
8183
}
8284

83-
foreach (var registration in _enumerable)
85+
// Cache matching registrations on first call to avoid enumerating on every Handle call
86+
if (_cache.MatchingRegistrations == null)
8487
{
85-
if (registration.RequestType != requestType ||
86-
registration.ResponseType is not null && registration.ResponseType != responseType)
88+
_cache.MatchingRegistrations = GenericTypeHelper.CacheMatchingRegistrations(
89+
_enumerable,
90+
r => r.RequestType,
91+
requestType);
92+
}
93+
94+
foreach (var registration in _cache.MatchingRegistrations)
95+
{
96+
if (registration.ResponseType is not null && registration.ResponseType != responseType)
8797
{
8898
continue;
8999
}
90100

91-
var type = registration.HandlerType.IsGenericType
92-
? registration.HandlerType.MakeGenericType(arguments)
93-
: registration.HandlerType;
101+
Type type;
102+
if (registration.HandlerType.IsGenericType)
103+
{
104+
// Check if the generic arguments satisfy the handler's constraints
105+
if (!GenericTypeHelper.CanMakeGenericType(registration.HandlerType, arguments))
106+
{
107+
continue;
108+
}
109+
110+
type = registration.HandlerType.MakeGenericType(arguments);
111+
}
112+
else
113+
{
114+
type = registration.HandlerType;
115+
}
94116

95117
var handler = (IStreamRequestHandler<TRequest, TResponse>) _serviceProvider.GetRequiredService(type);
96118

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace Zapto.Mediator;
5+
6+
internal static class GenericTypeHelper
7+
{
8+
/// <summary>
9+
/// Checks if a generic type definition can be instantiated with the given type arguments
10+
/// by validating all generic constraints.
11+
/// </summary>
12+
public static bool CanMakeGenericType(Type genericTypeDefinition, Type[] typeArguments)
13+
{
14+
if (!genericTypeDefinition.IsGenericTypeDefinition)
15+
{
16+
return false;
17+
}
18+
19+
var genericParams = genericTypeDefinition.GetGenericArguments();
20+
21+
if (genericParams.Length != typeArguments.Length)
22+
{
23+
return false;
24+
}
25+
26+
// Try to actually make the generic type to let the CLR validate constraints
27+
// This is the most reliable way to check all constraints including complex ones
28+
try
29+
{
30+
var constructedType = genericTypeDefinition.MakeGenericType(typeArguments);
31+
return constructedType != null;
32+
}
33+
catch (ArgumentException)
34+
{
35+
// MakeGenericType throws ArgumentException when constraints are violated
36+
return false;
37+
}
38+
catch (NotSupportedException)
39+
{
40+
// MakeGenericType throws NotSupportedException for certain invalid scenarios
41+
return false;
42+
}
43+
}
44+
45+
/// <summary>
46+
/// Caches generic registrations for a specific type to avoid repeated enumeration.
47+
/// </summary>
48+
public static List<T> CacheMatchingRegistrations<T>(
49+
IEnumerable<T> registrations,
50+
Func<T, Type> getNotificationType,
51+
Type targetGenericType)
52+
{
53+
var cached = new List<T>();
54+
foreach (var registration in registrations)
55+
{
56+
if (getNotificationType(registration) == targetGenericType)
57+
{
58+
cached.Add(registration);
59+
}
60+
}
61+
return cached;
62+
}
63+
}
64+

src/Mediator.DependencyInjection/Generic/MediatorBuilder.NotificationHandler.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public IMediatorBuilder AddNotificationHandler(
1212
Type handlerType,
1313
RegistrationScope scope = RegistrationScope.Transient)
1414
{
15-
if (notificationType.IsGenericType)
15+
if (handlerType.IsGenericTypeDefinition)
1616
{
1717
_services.Add(new ServiceDescriptor(handlerType, handlerType, GetLifetime(scope)));
1818
_services.AddSingleton(new GenericNotificationRegistration(notificationType, handlerType));
@@ -35,7 +35,8 @@ public IMediatorBuilder AddNotificationHandler(Type handlerType, RegistrationSco
3535
{
3636
var notificationType = type.GetGenericArguments()[0];
3737

38-
if (notificationType.IsGenericType)
38+
// Only convert to generic type definition if the handler itself is an open generic
39+
if (handlerType.IsGenericTypeDefinition && notificationType.IsGenericType)
3940
{
4041
notificationType = notificationType.GetGenericTypeDefinition();
4142
}

src/Mediator.DependencyInjection/Mediator.DependencyInjection.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
4+
<TargetFrameworks>netstandard2.0;net8.0;net9.0;net10.0</TargetFrameworks>
55
<AssemblyName>Zapto.Mediator.DependencyInjection</AssemblyName>
66
<RootNamespace>Zapto.Mediator</RootNamespace>
77
<LangVersion>10</LangVersion>

src/Mediator.Hosting/Mediator.Hosting.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
4+
<TargetFrameworks>netstandard2.0;net8.0;net9.0;net10.0</TargetFrameworks>
55
<AssemblyName>Zapto.Mediator.Hosting</AssemblyName>
66
<RootNamespace>Zapto.Mediator</RootNamespace>
77
<LangVersion>10</LangVersion>

src/Mediator/Mediator.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
</ItemGroup>
1919

2020
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
21-
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.3" />
22-
<PackageReference Include="System.Memory" Version="4.6.1" />
23-
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.1" />
21+
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.1" />
22+
<PackageReference Include="System.Memory" Version="4.6.3" />
23+
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
2424
</ItemGroup>
2525

2626
</Project>

0 commit comments

Comments
 (0)