Skip to content

Commit 056e8df

Browse files
committed
Don't use generic classes
1 parent 62fe18d commit 056e8df

File tree

6 files changed

+273
-1
lines changed

6 files changed

+273
-1
lines changed

src/Foundatio.Mediator/HandlerGenerator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ private static bool IsInterceptorsEnabled(AnalyzerConfigOptionsProvider provider
100100
if (HasFoundatioIgnoreAttribute(classSymbol))
101101
return null;
102102

103+
// Skip handler classes that have generic type parameters
104+
if (classSymbol.IsGenericType)
105+
return null;
106+
103107
// Find all handler methods in this class
104108
var handlerMethods = classSymbol.GetMembers()
105109
.OfType<IMethodSymbol>()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#nullable enable
2+
using System;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace Foundatio.Mediator
8+
{
9+
internal static class AnotherTestMessageHandler_Handle_AnotherTestMessage_StaticWrapper
10+
{
11+
public static string Handle(Foundatio.Mediator.Tests.AnotherTestMessage message, IServiceProvider serviceProvider, CancellationToken cancellationToken)
12+
{
13+
var handlerInstance = GetOrCreateHandler(serviceProvider);
14+
return handlerInstance.Handle(message);
15+
}
16+
17+
public static object UntypedHandle(IMediator mediator, object message, CancellationToken cancellationToken, Type? responseType)
18+
{
19+
var typedMessage = (Foundatio.Mediator.Tests.AnotherTestMessage)message;
20+
var serviceProvider = ((Mediator)mediator).ServiceProvider;
21+
var result = Handle(typedMessage, serviceProvider, cancellationToken);
22+
return result ?? new object();
23+
}
24+
25+
private static Foundatio.Mediator.Tests.AnotherTestMessageHandler? _handler;
26+
private static readonly object _lock = new object();
27+
28+
private static Foundatio.Mediator.Tests.AnotherTestMessageHandler GetOrCreateHandler(IServiceProvider serviceProvider)
29+
{
30+
if (_handler != null)
31+
return _handler;
32+
33+
var handlerFromDI = serviceProvider.GetService<Foundatio.Mediator.Tests.AnotherTestMessageHandler>();
34+
if (handlerFromDI != null)
35+
return handlerFromDI;
36+
37+
lock (_lock)
38+
{
39+
if (_handler != null)
40+
return _handler;
41+
42+
_handler = ActivatorUtilities.CreateInstance<Foundatio.Mediator.Tests.AnotherTestMessageHandler>(serviceProvider);
43+
return _handler;
44+
}
45+
}
46+
47+
private static readonly System.Collections.Concurrent.ConcurrentDictionary<System.Type, object> _middlewareCache = new();
48+
49+
private static T GetOrCreateMiddleware<T>(IServiceProvider serviceProvider) where T : class
50+
{
51+
// Check cache first - if it's there, it means it's not registered in DI
52+
if (_middlewareCache.TryGetValue(typeof(T), out var cachedInstance))
53+
return (T)cachedInstance;
54+
55+
// Try to get from DI - if registered, always use DI (respects service lifetime)
56+
var middlewareFromDI = serviceProvider.GetService<T>();
57+
if (middlewareFromDI != null)
58+
return middlewareFromDI;
59+
60+
// Not in DI, create and cache our own instance
61+
return (T)_middlewareCache.GetOrAdd(typeof(T), type =>
62+
ActivatorUtilities.CreateInstance<T>(serviceProvider));
63+
}
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#nullable enable
2+
using System;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace Foundatio.Mediator
8+
{
9+
internal static class ConcreteTestMessageHandler_HandleAsync_TestMessage_StaticWrapper
10+
{
11+
public static async System.Threading.Tasks.Task HandleAsync(Foundatio.Mediator.Tests.TestMessage message, IServiceProvider serviceProvider, CancellationToken cancellationToken)
12+
{
13+
var handlerInstance = GetOrCreateHandler(serviceProvider);
14+
await handlerInstance.HandleAsync(message, cancellationToken);
15+
}
16+
17+
public static async ValueTask<object> UntypedHandleAsync(IMediator mediator, object message, CancellationToken cancellationToken, Type? responseType)
18+
{
19+
var typedMessage = (Foundatio.Mediator.Tests.TestMessage)message;
20+
var serviceProvider = ((Mediator)mediator).ServiceProvider;
21+
await HandleAsync(typedMessage, serviceProvider, cancellationToken);
22+
return new object();
23+
}
24+
25+
private static Foundatio.Mediator.Tests.ConcreteTestMessageHandler? _handler;
26+
private static readonly object _lock = new object();
27+
28+
private static Foundatio.Mediator.Tests.ConcreteTestMessageHandler GetOrCreateHandler(IServiceProvider serviceProvider)
29+
{
30+
if (_handler != null)
31+
return _handler;
32+
33+
var handlerFromDI = serviceProvider.GetService<Foundatio.Mediator.Tests.ConcreteTestMessageHandler>();
34+
if (handlerFromDI != null)
35+
return handlerFromDI;
36+
37+
lock (_lock)
38+
{
39+
if (_handler != null)
40+
return _handler;
41+
42+
_handler = ActivatorUtilities.CreateInstance<Foundatio.Mediator.Tests.ConcreteTestMessageHandler>(serviceProvider);
43+
return _handler;
44+
}
45+
}
46+
47+
private static readonly System.Collections.Concurrent.ConcurrentDictionary<System.Type, object> _middlewareCache = new();
48+
49+
private static T GetOrCreateMiddleware<T>(IServiceProvider serviceProvider) where T : class
50+
{
51+
// Check cache first - if it's there, it means it's not registered in DI
52+
if (_middlewareCache.TryGetValue(typeof(T), out var cachedInstance))
53+
return (T)cachedInstance;
54+
55+
// Try to get from DI - if registered, always use DI (respects service lifetime)
56+
var middlewareFromDI = serviceProvider.GetService<T>();
57+
if (middlewareFromDI != null)
58+
return middlewareFromDI;
59+
60+
// Not in DI, create and cache our own instance
61+
return (T)_middlewareCache.GetOrAdd(typeof(T), type =>
62+
ActivatorUtilities.CreateInstance<T>(serviceProvider));
63+
}
64+
}
65+
}

tests/Foundatio.Mediator.Tests/Generated/Foundatio.Mediator/Foundatio.Mediator.HandlerGenerator/ServiceCollectionExtensions.g.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@ public static IServiceCollection AddMediator(this IServiceCollection services)
184184
PartiallyIgnoredHandler_HandleAsync_ValidMethodMessage_StaticWrapper.UntypedHandleAsync,
185185
null,
186186
true));
187+
services.AddKeyedSingleton<HandlerRegistration>("Foundatio.Mediator.Tests.TestMessage",
188+
new HandlerRegistration(
189+
"Foundatio.Mediator.Tests.TestMessage",
190+
ConcreteTestMessageHandler_HandleAsync_TestMessage_StaticWrapper.UntypedHandleAsync,
191+
null,
192+
true));
193+
services.AddKeyedSingleton<HandlerRegistration>("Foundatio.Mediator.Tests.AnotherTestMessage",
194+
new HandlerRegistration(
195+
"Foundatio.Mediator.Tests.AnotherTestMessage",
196+
(mediator, message, cancellationToken, responseType) => new ValueTask<object>(AnotherTestMessageHandler_Handle_AnotherTestMessage_StaticWrapper.UntypedHandle(mediator, message, cancellationToken, responseType)),
197+
AnotherTestMessageHandler_Handle_AnotherTestMessage_StaticWrapper.UntypedHandle,
198+
false));
187199
services.AddKeyedSingleton<HandlerRegistration>("Foundatio.Mediator.Tests.INotification",
188200
new HandlerRegistration(
189201
"Foundatio.Mediator.Tests.INotification",
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using Foundatio.Xunit;
2+
using Xunit;
3+
using Xunit.Abstractions;
4+
5+
namespace Foundatio.Mediator.Tests;
6+
7+
// Test message types
8+
public class TestMessage
9+
{
10+
public string Value { get; set; } = string.Empty;
11+
}
12+
13+
public class AnotherTestMessage
14+
{
15+
public string Data { get; set; } = string.Empty;
16+
}
17+
18+
// Generic handler class - this should be completely ignored by the source generator
19+
public class GenericHandler<T>
20+
{
21+
public async Task HandleAsync(T message, CancellationToken cancellationToken = default)
22+
{
23+
// This handler should be ignored because the class has generic type parameters
24+
await Task.CompletedTask;
25+
}
26+
27+
public void Handle(T message)
28+
{
29+
// This handler should also be ignored
30+
}
31+
}
32+
33+
// Another generic handler class with multiple type parameters
34+
public class MultiGenericHandler<T, U>
35+
{
36+
public async Task HandleAsync(T message, CancellationToken cancellationToken = default)
37+
{
38+
// This handler should be ignored because the class has generic type parameters
39+
await Task.CompletedTask;
40+
}
41+
42+
public U Handle(T message)
43+
{
44+
// This handler should also be ignored
45+
return default(U)!;
46+
}
47+
}
48+
49+
// Non-generic handler class - this should work normally
50+
public class ConcreteTestMessageHandler
51+
{
52+
public static readonly List<string> ReceivedMessages = new();
53+
54+
public async Task HandleAsync(TestMessage message, CancellationToken cancellationToken = default)
55+
{
56+
ReceivedMessages.Add($"Handled: {message.Value}");
57+
await Task.CompletedTask;
58+
}
59+
}
60+
61+
// Non-generic handler class - this should also work normally
62+
public class AnotherTestMessageHandler
63+
{
64+
public static readonly List<string> ReceivedMessages = new();
65+
66+
public string Handle(AnotherTestMessage message)
67+
{
68+
var result = $"Handled: {message.Data}";
69+
ReceivedMessages.Add(result);
70+
return result;
71+
}
72+
}
73+
74+
public class GenericHandlerClassTest : TestWithLoggingBase
75+
{
76+
public GenericHandlerClassTest(ITestOutputHelper output) : base(output) { }
77+
78+
[Fact]
79+
public void GenericHandlerClasses_ShouldBeIgnored()
80+
{
81+
// This test primarily exists to trigger the source generator analysis
82+
// The key test is that there should be NO source generation for:
83+
// - GenericHandler<T>
84+
// - MultiGenericHandler<T, U>
85+
//
86+
// But source generation SHOULD work for:
87+
// - ConcreteTestMessageHandler
88+
// - AnotherTestMessageHandler
89+
90+
var testMessage = new TestMessage { Value = "test" };
91+
Assert.NotNull(testMessage);
92+
93+
var anotherMessage = new AnotherTestMessage { Data = "data" };
94+
Assert.NotNull(anotherMessage);
95+
96+
// If the source generator worked correctly:
97+
// - Generic handler classes are ignored (no compilation errors, no handlers generated)
98+
// - Non-generic handler classes have handlers generated
99+
100+
// Clear any previous results
101+
ConcreteTestMessageHandler.ReceivedMessages.Clear();
102+
AnotherTestMessageHandler.ReceivedMessages.Clear();
103+
104+
// The actual functionality test would require DI setup which is tested in integration tests
105+
// This test ensures the code compiles without errors, which validates that:
106+
// 1. Generic handler classes don't cause source generation issues
107+
// 2. Non-generic handler classes are still processed correctly
108+
}
109+
110+
[Fact]
111+
public void NonGenericHandlers_ShouldStillWork()
112+
{
113+
// This verifies that our change to ignore generic handler classes
114+
// doesn't break the processing of normal, non-generic handler classes
115+
116+
var testMessage = new TestMessage { Value = "test-value" };
117+
var anotherMessage = new AnotherTestMessage { Data = "test-data" };
118+
119+
// These should both be valid message types that can have handlers
120+
Assert.NotNull(testMessage);
121+
Assert.Equal("test-value", testMessage.Value);
122+
123+
Assert.NotNull(anotherMessage);
124+
Assert.Equal("test-data", anotherMessage.Data);
125+
}
126+
}

tests/Foundatio.Mediator.Tests/GenericMessageTypeTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public void GenericMethodHandler_ShouldBeSkipped()
120120
}
121121

122122
[Fact]
123-
public async Task PolymorphicMessageHandling_ShouldCallAllApplicableHandlers()
123+
public void PolymorphicMessageHandling_ShouldCallAllApplicableHandlers()
124124
{
125125
// Clear any previous test results
126126
NotificationHandler.ReceivedMessages.Clear();

0 commit comments

Comments
 (0)