Skip to content

Commit 8422f89

Browse files
committed
Fix issue with untyped handler
1 parent c07ae53 commit 8422f89

File tree

4 files changed

+43
-24
lines changed

4 files changed

+43
-24
lines changed

samples/CleanArchitectureSample/src/Common.Module/Middleware/ValidationMiddleware.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
1+
using System.Collections.Concurrent;
12
using Foundatio.Mediator;
23
using MiniValidation;
34

45
namespace Common.Module.Middleware;
56

7+
/// <summary>
8+
/// Apply this attribute to a message to skip validation.
9+
/// </summary>
10+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)]
11+
public sealed class SkipValidationAttribute : Attribute { }
12+
613
[Middleware(5)]
714
public static class ValidationMiddleware
815
{
9-
public static HandlerResult Before(IValidatable message)
16+
private static readonly ConcurrentDictionary<Type, bool> _skipValidationCache = new();
17+
18+
public static HandlerResult Before(object message)
1019
{
20+
// Skip validation if the message is decorated with [SkipValidation] (memoized)
21+
if (_skipValidationCache.GetOrAdd(message.GetType(), static t =>
22+
t.GetCustomAttributes(typeof(SkipValidationAttribute), false).Length > 0))
23+
return HandlerResult.Continue();
24+
1125
if (MiniValidator.TryValidate(message, out var errors))
1226
return HandlerResult.Continue();
1327

@@ -16,5 +30,3 @@ public static HandlerResult Before(IValidatable message)
1630
return Result.Invalid(validationErrors);
1731
}
1832
}
19-
20-
public interface IValidatable { }

samples/CleanArchitectureSample/src/Orders.Module/Messages/OrderMessages.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.ComponentModel.DataAnnotations;
2-
using Common.Module.Middleware;
32
using Foundatio.Mediator;
43
using Orders.Module.Domain;
54

@@ -17,16 +16,17 @@ public record CreateOrder(
1716

1817
[Required(ErrorMessage = "Description is required")]
1918
[StringLength(200, MinimumLength = 5, ErrorMessage = "Description must be between 5 and 200 characters")]
20-
string Description) : IValidatable, ICommand<Result<Order>>;
19+
string Description) : ICommand<Result<Order>>;
2120

2221
public record UpdateOrder(
2322
[Required] string OrderId,
2423
decimal? Amount,
2524
string? Description,
26-
OrderStatus? Status) : IValidatable, ICommand<Result<Order>>;
25+
OrderStatus? Status) : ICommand<Result<Order>>;
2726

28-
public record DeleteOrder([Required] string OrderId) : IValidatable, ICommand<Result>;
27+
public record DeleteOrder([Required] string OrderId) : ICommand<Result>;
2928

3029
// Queries
31-
public record GetOrder([Required] string OrderId) : IValidatable, IQuery<Result<Order>>;
30+
public record GetOrder([Required] string OrderId) : IQuery<Result<Order>>;
31+
3232
public record GetOrders() : IQuery<Result<List<Order>>>;

samples/CleanArchitectureSample/src/Products.Module/Messages/ProductMessages.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.ComponentModel.DataAnnotations;
2-
using Common.Module.Middleware;
32
using Foundatio.Mediator;
43
using Products.Module.Domain;
54

@@ -20,18 +19,19 @@ public record CreateProduct(
2019
decimal Price,
2120

2221
[Range(0, 1000000, ErrorMessage = "Stock quantity must be between 0 and 1,000,000")]
23-
int StockQuantity = 0) : IValidatable, ICommand<Result<Product>>;
22+
int StockQuantity = 0) : ICommand<Result<Product>>;
2423

2524
public record UpdateProduct(
2625
[Required] string ProductId,
2726
string? Name,
2827
string? Description,
2928
decimal? Price,
3029
int? StockQuantity,
31-
ProductStatus? Status) : IValidatable, ICommand<Result<Product>>;
30+
ProductStatus? Status) : ICommand<Result<Product>>;
3231

33-
public record DeleteProduct([Required] string ProductId) : IValidatable, ICommand<Result>;
32+
public record DeleteProduct([Required] string ProductId) : ICommand<Result>;
3433

3534
// Queries
36-
public record GetProduct([Required] string ProductId) : IValidatable, IQuery<Result<Product>>;
35+
public record GetProduct([Required] string ProductId) : IQuery<Result<Product>>;
36+
3737
public record GetProducts() : IQuery<Result<List<Product>>>;

src/Foundatio.Mediator/HandlerGenerator.cs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ private static void GenerateHandleItemMethods(IndentedStringBuilder source, Hand
324324
/// <param name="resultVar">Variable name to store the handler result.</param>
325325
/// <param name="messageVar">Variable name containing the typed message (default: "message").</param>
326326
/// <param name="targetTupleIndex">For tuple return types, the index of the tuple item this method returns (0 = Item1, 1 = Item2, etc.). -1 for non-tuple handlers.</param>
327-
private static void EmitHandlerInvocationCode(IndentedStringBuilder source, HandlerInfo handler, GeneratorConfiguration configuration, string resultVar, string messageVar = "message", int targetTupleIndex = 0)
327+
private static void EmitHandlerInvocationCode(IndentedStringBuilder source, HandlerInfo handler, GeneratorConfiguration configuration, string resultVar, string messageVar = "message", int targetTupleIndex = 0, bool isUntypedMethod = false)
328328
{
329329
var variables = new Dictionary<string, string> { ["System.IServiceProvider"] = "serviceProvider" };
330330

@@ -359,7 +359,7 @@ private static void EmitHandlerInvocationCode(IndentedStringBuilder source, Hand
359359
else
360360
{
361361
// Original code path - no Execute middleware
362-
EmitPipelineCode(source, handler, beforeMiddleware, afterMiddleware, finallyMiddleware, configuration, variables, resultVar, messageVar, targetTupleIndex, requiresTryCatch);
362+
EmitPipelineCode(source, handler, beforeMiddleware, afterMiddleware, finallyMiddleware, configuration, variables, resultVar, messageVar, targetTupleIndex, requiresTryCatch, isUntypedMethod: isUntypedMethod);
363363
}
364364
}
365365

@@ -396,8 +396,8 @@ private static void EmitExecuteMiddlewareChain(
396396
source.AppendLine($"{handler.ReturnType.UnwrappedFullName}{(allowNull ? "?" : "")} innerResult = default;");
397397
}
398398

399-
// Emit the full pipeline inside the delegate
400-
EmitPipelineCode(source, handler, beforeMiddleware, afterMiddleware, finallyMiddleware, configuration, innerVariables, "innerResult", messageVar, targetTupleIndex, requiresTryCatch, insideExecuteDelegate: true);
399+
// Emit the full pipeline inside the delegate - isUntypedMethod is false here because we're inside a delegate that returns object?
400+
EmitPipelineCode(source, handler, beforeMiddleware, afterMiddleware, finallyMiddleware, configuration, innerVariables, "innerResult", messageVar, targetTupleIndex, requiresTryCatch, insideExecuteDelegate: true, isUntypedMethod: false);
401401

402402
source.AppendLine(handler.HasReturnValue ? "return innerResult;" : "return null;");
403403
source.DecrementIndent();
@@ -483,6 +483,8 @@ private static string BuildExecuteParameters(IndentedStringBuilder source, Equat
483483
/// </summary>
484484
/// <param name="insideExecuteDelegate">When true, we're inside an Execute middleware delegate and short-circuit
485485
/// returns should return the full tuple type (not just one item) since the delegate returns object?.</param>
486+
/// <param name="isUntypedMethod">When true, we're generating UntypedHandleAsync which returns ValueTask&lt;object?&gt;,
487+
/// so void handlers should return null instead of just return.</param>
486488
private static void EmitPipelineCode(
487489
IndentedStringBuilder source,
488490
HandlerInfo handler,
@@ -495,7 +497,8 @@ private static void EmitPipelineCode(
495497
string messageVar,
496498
int targetTupleIndex,
497499
bool requiresTryCatch,
498-
bool insideExecuteDelegate = false)
500+
bool insideExecuteDelegate = false,
501+
bool isUntypedMethod = false)
499502
{
500503
// Main execution with optional try-catch-finally
501504
if (requiresTryCatch)
@@ -510,7 +513,7 @@ private static void EmitPipelineCode(
510513
variables["System.Exception"] = "exception";
511514
}
512515

513-
EmitBeforeMiddlewareCalls(source, beforeMiddleware, handler, variables, messageVar, targetTupleIndex, insideExecuteDelegate);
516+
EmitBeforeMiddlewareCalls(source, beforeMiddleware, handler, variables, messageVar, targetTupleIndex, insideExecuteDelegate, isUntypedMethod);
514517
EmitHandlerInvocation(source, handler, variables, resultVar, messageVar);
515518
EmitAfterMiddlewareCalls(source, afterMiddleware, variables, messageVar);
516519

@@ -632,7 +635,8 @@ private static void EmitBeforeMiddlewareCalls(
632635
Dictionary<string, string> variables,
633636
string messageVar,
634637
int targetTupleIndex,
635-
bool insideExecuteDelegate = false)
638+
bool insideExecuteDelegate = false,
639+
bool isUntypedMethod = false)
636640
{
637641
foreach (var m in beforeMiddleware)
638642
{
@@ -645,7 +649,7 @@ private static void EmitBeforeMiddlewareCalls(
645649

646650
if (m.Method.ReturnType.IsHandlerResult)
647651
{
648-
EmitShortCircuitCheck(source, m, handler, targetTupleIndex, insideExecuteDelegate);
652+
EmitShortCircuitCheck(source, m, handler, targetTupleIndex, insideExecuteDelegate, isUntypedMethod);
649653
}
650654
}
651655
source.AppendLineIf(beforeMiddleware.Any());
@@ -656,7 +660,8 @@ private static void EmitShortCircuitCheck(
656660
(MiddlewareMethodInfo Method, MiddlewareInfo Middleware) m,
657661
HandlerInfo handler,
658662
int targetTupleIndex,
659-
bool insideExecuteDelegate = false)
663+
bool insideExecuteDelegate = false,
664+
bool isUntypedMethod = false)
660665
{
661666
string resultVarName = $"{m.Middleware.Identifier.ToCamelCase()}Result";
662667
string valueAccess = m.Method.ReturnType.IsGeneric ? $"{resultVarName}.Value" : $"{resultVarName}.Value!";
@@ -682,7 +687,9 @@ private static void EmitShortCircuitCheck(
682687
}
683688
else
684689
{
685-
source.AppendLine(" return;");
690+
// For void handlers: UntypedHandleAsync returns ValueTask<object?> so needs "return null"
691+
// Typed HandleAsync returns ValueTask so needs just "return"
692+
source.AppendLine(isUntypedMethod ? " return null;" : " return;");
686693
}
687694

688695
source.AppendLine("}");
@@ -870,7 +877,7 @@ private static void GenerateUntypedHandleMethod(IndentedStringBuilder source, Ha
870877
source.AppendLine("var serviceProvider = (System.IServiceProvider)mediator;");
871878

872879
// Emit the handler invocation code - use "typedMessage" since we cast the object message above
873-
EmitHandlerInvocationCode(source, handler, configuration, "result", "typedMessage");
880+
EmitHandlerInvocationCode(source, handler, configuration, "result", "typedMessage", isUntypedMethod: true);
874881

875882
// For tuple returns, use PublishCascadingMessagesAsync for runtime dispatch
876883
if (handler.ReturnType.IsTuple)

0 commit comments

Comments
 (0)