Skip to content

Commit 39c8eb8

Browse files
committed
Add cascading message support
1 parent 1438b0b commit 39c8eb8

File tree

5 files changed

+98
-22
lines changed

5 files changed

+98
-22
lines changed

samples/ConsoleSample/SampleRunner.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ private async Task RunSingleHandlerInvokeSamples()
7777
{
7878
Console.WriteLine("4️⃣ Testing Invoke with Single Handler...\n");
7979

80-
var uniqueCommand = new CreateOrder("ORD-001", "CUST-123", 299.99m, "Wireless Headphones");
81-
string processResult = await _mediator.InvokeAsync<string>(uniqueCommand);
82-
Console.WriteLine($"Process result: {processResult}");
80+
var createOrder = new CreateOrder("ORD-001", "CUST-123", 299.99m, "Wireless Headphones");
81+
var order = await _mediator.InvokeAsync<Order>(createOrder);
82+
Console.WriteLine($"Process result: {order.OrderId}");
8383

8484
Console.WriteLine("✅ Single handler invoke test completed!\n");
8585
}

src/Foundatio.Mediator.SourceGenerator/HandlerWrapperGenerator.cs

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public static string GenerateHandlerWrapper(HandlerInfo handler, string wrapperC
3030
.AppendLine("using System.Collections.Generic;")
3131
.AppendLine("using System.Linq;")
3232
.AppendLine("using System.Reflection;")
33+
.AppendLine("using System.Runtime.CompilerServices;")
3334
.AppendLine("using System.Threading;")
3435
.AppendLine("using System.Threading.Tasks;")
3536
.AppendLine("using Microsoft.Extensions.DependencyInjection;")
@@ -121,6 +122,13 @@ private static void GenerateStronglyTypedMethod(IndentedStringBuilder source, Ha
121122

122123
using (source.Indent())
123124
{
125+
// Get mediator for cascading messages if needed
126+
bool needsMediator = IsTupleReturnType(handler.ReturnTypeName);
127+
if (needsMediator)
128+
{
129+
source.AppendLine("var mediator = serviceProvider.GetRequiredService<IMediator>();");
130+
}
131+
124132
if (middlewares.Any())
125133
{
126134
// Generate middleware-aware execution
@@ -324,8 +332,7 @@ private static void GenerateAsyncHandleMethod(IndentedStringBuilder source, Hand
324332
using (source.Indent())
325333
{
326334
// Cast message to expected type and call strongly typed method
327-
source.AppendLine($"var typedMessage = ({handler.MessageTypeName})message;")
328-
.AppendLine("var serviceProvider = ((Mediator)mediator).ServiceProvider;");
335+
source.AppendLine($"var typedMessage = ({handler.MessageTypeName})message;");
329336

330337
bool hasReturnValue = handler.ReturnTypeName != "void" &&
331338
handler.ReturnTypeName != "System.Threading.Tasks.Task" &&
@@ -335,12 +342,12 @@ private static void GenerateAsyncHandleMethod(IndentedStringBuilder source, Hand
335342

336343
if (hasReturnValue)
337344
{
338-
source.AppendLine($"var result = await {stronglyTypedMethodName}(typedMessage, serviceProvider, cancellationToken);");
345+
source.AppendLine($"var result = await {stronglyTypedMethodName}(typedMessage, mediator.ServiceProvider, cancellationToken);");
339346

340347
// Handle tuple return values and cascading
341348
if (IsTupleReturnType(handler.ReturnTypeName))
342349
{
343-
source.AppendLine("return await HandleTupleResult(mediator, result, responseType, cancellationToken);");
350+
source.AppendLine("return await PublishCascadingMessagesAsync(mediator, result, responseType);");
344351
}
345352
else
346353
{
@@ -397,7 +404,7 @@ private static void GenerateAsyncHandleMethod(IndentedStringBuilder source, Hand
397404
else
398405
{
399406
// Handler returns void
400-
source.AppendLine($"await {stronglyTypedMethodName}(typedMessage, serviceProvider, cancellationToken);")
407+
source.AppendLine($"await {stronglyTypedMethodName}(typedMessage, mediator.ServiceProvider, cancellationToken);")
401408
.AppendLine("return new object();");
402409
}
403410
}
@@ -413,8 +420,7 @@ private static void GenerateSyncHandleMethod(IndentedStringBuilder source, Handl
413420
using (source.Indent())
414421
{
415422
// Cast message to expected type and call strongly typed method
416-
source.AppendLine($"var typedMessage = ({handler.MessageTypeName})message;")
417-
.AppendLine("var serviceProvider = ((Mediator)mediator).ServiceProvider;");
423+
source.AppendLine($"var typedMessage = ({handler.MessageTypeName})message;");
418424

419425
bool hasReturnValue = handler.ReturnTypeName != "void" &&
420426
handler.ReturnTypeName != "System.Threading.Tasks.Task" &&
@@ -424,7 +430,7 @@ private static void GenerateSyncHandleMethod(IndentedStringBuilder source, Handl
424430

425431
if (hasReturnValue)
426432
{
427-
source.AppendLine($"var result = {stronglyTypedMethodName}(typedMessage, serviceProvider, cancellationToken);");
433+
source.AppendLine($"var result = {stronglyTypedMethodName}(typedMessage, mediator.ServiceProvider, cancellationToken);");
428434

429435
// Handle tuple return values and cascading
430436
if (IsTupleReturnType(handler.ReturnTypeName))
@@ -488,7 +494,7 @@ private static void GenerateSyncHandleMethod(IndentedStringBuilder source, Handl
488494
else
489495
{
490496
// Handler returns void
491-
source.AppendLine($"{stronglyTypedMethodName}(typedMessage, serviceProvider, cancellationToken);")
497+
source.AppendLine($"{stronglyTypedMethodName}(typedMessage, mediator.ServiceProvider, cancellationToken);")
492498
.AppendLine("return new object();");
493499
}
494500
}
@@ -566,14 +572,21 @@ private static void GenerateInterceptorMethod(IndentedStringBuilder source, Hand
566572

567573
using (source.Indent())
568574
{
569-
source.AppendLine($"var typedMessage = ({handler.MessageTypeName})message;")
570-
.AppendLine("var serviceProvider = ((Mediator)mediator).ServiceProvider;");
575+
source.AppendLine($"var typedMessage = ({handler.MessageTypeName})message;");
571576

572577
// Generate the appropriate method call based on async/sync combinations
573-
string awaitKeyword = (interceptorIsAsync && wrapperIsAsync) ? "await " : "";
574-
string returnKeyword = isGeneric ? "return " : "";
575-
576-
source.AppendLine($"{returnKeyword}{awaitKeyword}{stronglyTypedMethodName}(typedMessage, serviceProvider, cancellationToken);");
578+
if (isGeneric && IsTupleReturnType(handler.ReturnTypeName))
579+
{
580+
// For generic calls with tuple return types, handle cascading messages
581+
source.AppendLine($"var result = {(interceptorIsAsync && wrapperIsAsync ? "await " : "")}{stronglyTypedMethodName}(typedMessage, mediator.ServiceProvider, cancellationToken);")
582+
.AppendLine($"return ({expectedResponseTypeName}){(interceptorIsAsync ? "await " : "")}PublishCascadingMessagesAsync(mediator, result, typeof({expectedResponseTypeName}));");
583+
}
584+
else
585+
{
586+
string awaitKeyword = (interceptorIsAsync && wrapperIsAsync) ? "await " : "";
587+
string returnKeyword = isGeneric ? "return " : "";
588+
source.AppendLine($"{returnKeyword}{awaitKeyword}{stronglyTypedMethodName}(typedMessage, mediator.ServiceProvider, cancellationToken);");
589+
}
577590
}
578591

579592
source.AppendLine("}");
@@ -1148,14 +1161,34 @@ private static void AddGeneratedFileHeader(IndentedStringBuilder source)
11481161
private static void GenerateHandleTupleResult(IndentedStringBuilder source)
11491162
{
11501163
source.AppendLine()
1151-
.AppendLine("private static async ValueTask<object?> HandleTupleResult(IMediator mediator, object tupleResult, Type? responseType, CancellationToken cancellationToken)")
1164+
.AppendLine("private static async ValueTask<object?> PublishCascadingMessagesAsync(IMediator mediator, object? result, Type? responseType)")
11521165
.AppendLine("{");
11531166

11541167
using (source.Indent())
11551168
{
1156-
source.AppendLine("// TODO: Implement tuple extraction and cascading message publishing")
1157-
.AppendLine("// For now, return the tuple as-is")
1158-
.AppendLine("return tupleResult;");
1169+
source.AppendLine("if (result == null)")
1170+
.AppendLine(" return null;")
1171+
.AppendLine()
1172+
.AppendLine("// Check if result is a tuple using ITuple interface")
1173+
.AppendLine("if (result is not ITuple tuple)")
1174+
.AppendLine(" return result;")
1175+
.AppendLine()
1176+
.AppendLine("object? foundResult = null;")
1177+
.AppendLine()
1178+
.AppendLine("for (int i = 0; i < tuple.Length; i++)")
1179+
.AppendLine("{")
1180+
.AppendLine(" var item = tuple[i];")
1181+
.AppendLine(" if (item != null && responseType != null && responseType.IsAssignableFrom(item.GetType()))")
1182+
.AppendLine(" {")
1183+
.AppendLine(" foundResult = item;")
1184+
.AppendLine(" }")
1185+
.AppendLine(" else if (item != null)")
1186+
.AppendLine(" {")
1187+
.AppendLine(" await mediator.PublishAsync(item, CancellationToken.None);")
1188+
.AppendLine(" }")
1189+
.AppendLine("}")
1190+
.AppendLine()
1191+
.AppendLine("return foundResult;");
11591192
}
11601193

11611194
source.AppendLine("}");

src/Foundatio.Mediator.SourceGenerator/MediatorValidator.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,24 @@ public static void ValidateHandlerConfiguration(List<HandlerInfo> handlers, Sour
5454
}
5555
}
5656

57+
// Check for tuple return handlers that must be async
58+
foreach (var handler in messageHandlers)
59+
{
60+
if (IsTupleReturnType(handler.ReturnTypeName) && !handler.IsAsync)
61+
{
62+
var descriptor = new DiagnosticDescriptor(
63+
"FMED013",
64+
"Tuple returning handler must be async",
65+
"Handler '{0}.{1}' returns a tuple type '{2}' but is not async. Tuple returning handlers must be async to support cascading message publishing.",
66+
"Foundatio.Mediator",
67+
DiagnosticSeverity.Error,
68+
isEnabledByDefault: true);
69+
70+
var diagnostic = Diagnostic.Create(descriptor, Location.None, handler.HandlerTypeName, handler.MethodName, handler.ReturnTypeName);
71+
context.ReportDiagnostic(diagnostic);
72+
}
73+
}
74+
5775
// Note: We don't validate sync/async mismatch here at generation time
5876
// Instead, we'll generate all methods and let the compiler handle missing implementations
5977
// This allows consumers to use only async methods when appropriate
@@ -182,6 +200,21 @@ private static void ValidateInvokeCall(CallSiteInfo callSite, List<HandlerInfo>
182200
}
183201
}
184202

203+
// Check for sync call with tuple-returning handler
204+
if (!callSite.IsAsync && IsTupleReturnType(handler.ReturnTypeName))
205+
{
206+
var descriptor = new DiagnosticDescriptor(
207+
"FMED014",
208+
"Sync call with tuple-returning handler",
209+
"Sync {0} call for message type '{1}' but handler '{2}.{3}' returns a tuple type '{4}'. Tuple-returning handlers require async calls to support cascading message publishing. Use {0}Async instead.",
210+
"Foundatio.Mediator",
211+
DiagnosticSeverity.Error,
212+
isEnabledByDefault: true);
213+
214+
var diagnostic = Diagnostic.Create(descriptor, callSite.Location, callSite.MethodName, callSite.MessageTypeName, handler.HandlerTypeName, handler.MethodName, handler.ReturnTypeName);
215+
context.ReportDiagnostic(diagnostic);
216+
}
217+
185218
// For generic Invoke<TResponse> calls, validate return type compatibility
186219
if (!String.IsNullOrEmpty(callSite.ExpectedResponseTypeName))
187220
{
@@ -283,4 +316,9 @@ private static bool IsMiddlewareApplicableToHandler(MiddlewareInfo middleware, H
283316

284317
return false;
285318
}
319+
320+
private static bool IsTupleReturnType(string returnTypeName)
321+
{
322+
return returnTypeName.Contains("ValueTuple") || returnTypeName.Contains("Tuple") || returnTypeName.StartsWith("(");
323+
}
286324
}

src/Foundatio.Mediator/IMediator.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ namespace Foundatio.Mediator;
66
/// </summary>
77
public interface IMediator
88
{
9+
/// <summary>
10+
/// Gets the service provider used for dependency injection.
11+
/// </summary>
12+
IServiceProvider ServiceProvider { get; }
13+
914
/// <summary>
1015
/// Asynchronously invokes exactly one handler for the specified message.
1116
/// </summary>

tests/Foundatio.Mediator.Tests/CascadingMessagesTest.cs

Whitespace-only changes.

0 commit comments

Comments
 (0)