Skip to content

Commit b198132

Browse files
committed
Cleanup result API. Fix a few other issues.
1 parent ae9ee36 commit b198132

File tree

11 files changed

+155
-253
lines changed

11 files changed

+155
-253
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,12 @@ The source generator provides compile-time errors for:
375375
- Ability to call `AddMediator` to entry assembly and have it register handlers in all assemblies
376376
- [ ] Clean architecture sample app
377377
- [ ] Modular monolith architecture sample app
378+
- [ ] Switch source generator package name back to Foundatio.Mediator (maybe Foundatio.Mediator.Abstractions)
379+
- [ ] Figure out issue with props / targets files not being included
380+
- [ ] See if we can support streaming with IAsyncEnumerable
381+
- [ ] Talk about lifetime. Handlers aren't registered in DI by default and are singleton instances. Just add your handler or services to DI if you want a different behavior.
382+
- [ ] Add GeneratedCodeAttribute
383+
- [ ] Talk about for tuple returns / cascading messages, if a middleware short circuits the response, the value will be returned as the first tuple item and all others will be null or default.
378384

379385
## 📄 License
380386

samples/ConsoleSample/Handlers/Handlers.cs

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,13 @@ public class OrderHandler
2323
{
2424
private static readonly Dictionary<string, Order> _orders = new();
2525
private readonly ILogger<OrderHandler> _logger;
26-
private readonly IMediator _mediator;
2726

28-
public OrderHandler(ILogger<OrderHandler> logger, IMediator mediator)
27+
public OrderHandler(ILogger<OrderHandler> logger)
2928
{
3029
_logger = logger;
31-
_mediator = mediator;
3230
}
3331

34-
public async Task<Result<Order>> HandleAsync(CreateOrder command)
32+
public async Task<(Result<Order> Order, OrderCreated? Event)> HandleAsync(CreateOrder command)
3533
{
3634
_logger.LogInformation("Creating order for customer {CustomerId} with amount {Amount}",
3735
command.CustomerId, command.Amount);
@@ -41,10 +39,9 @@ public async Task<Result<Order>> HandleAsync(CreateOrder command)
4139

4240
_orders[orderId] = order;
4341

44-
// Publish event for other handlers to react
45-
await _mediator.PublishAsync(new OrderCreated(orderId, command.CustomerId, command.Amount, order.CreatedAt));
42+
await Task.CompletedTask; // Simulate async operation
4643

47-
return Result<Order>.Created(order, $"/orders/{orderId}");
44+
return (Result<Order>.Created(order, $"/orders/{orderId}"), new OrderCreated(orderId, command.CustomerId, command.Amount, order.CreatedAt));
4845
}
4946

5047
public Result<Order> Handle(GetOrder query)
@@ -53,19 +50,19 @@ public Result<Order> Handle(GetOrder query)
5350

5451
if (!_orders.TryGetValue(query.OrderId, out var order))
5552
{
56-
return Result<Order>.NotFound($"Order {query.OrderId} not found");
53+
return Result.NotFound($"Order {query.OrderId} not found");
5754
}
5855

5956
return order; // Implicit conversion to Result<Order>
6057
}
6158

62-
public async Task<Result<Order>> HandleAsync(UpdateOrder command)
59+
public async Task<(Result<Order> Order, OrderUpdated? Event)> HandleAsync(UpdateOrder command)
6360
{
6461
_logger.LogInformation("Updating order {OrderId}", command.OrderId);
6562

6663
if (!_orders.TryGetValue(command.OrderId, out var existingOrder))
6764
{
68-
return Result<Order>.NotFound($"Order {command.OrderId} not found");
65+
return (Result.NotFound($"Order {command.OrderId} not found"), null);
6966
}
7067

7168
var updatedOrder = existingOrder with
@@ -77,26 +74,24 @@ public async Task<Result<Order>> HandleAsync(UpdateOrder command)
7774

7875
_orders[command.OrderId] = updatedOrder;
7976

80-
// Publish event
81-
await _mediator.PublishAsync(new OrderUpdated(command.OrderId, updatedOrder.Amount, updatedOrder.UpdatedAt.Value));
77+
await Task.CompletedTask; // Simulate async operation
8278

83-
return updatedOrder;
79+
return (updatedOrder, new OrderUpdated(command.OrderId, updatedOrder.Amount, updatedOrder.UpdatedAt.Value));
8480
}
8581

86-
public async Task<Result> HandleAsync(DeleteOrder command)
82+
public async Task<(Result Order, OrderDeleted? Event)> HandleAsync(DeleteOrder command)
8783
{
8884
_logger.LogInformation("Deleting order {OrderId}", command.OrderId);
8985

9086
if (!_orders.ContainsKey(command.OrderId))
9187
{
92-
return Result.NotFound($"Order {command.OrderId} not found");
88+
return (Result.NotFound($"Order {command.OrderId} not found"), null);
9389
}
9490

9591
_orders.Remove(command.OrderId);
9692

97-
// Publish event
98-
await _mediator.PublishAsync(new OrderDeleted(command.OrderId, DateTime.UtcNow));
93+
await Task.CompletedTask; // Simulate async operation
9994

100-
return Result.NoContent();
95+
return (Result.NoContent(), new OrderDeleted(command.OrderId, DateTime.UtcNow));
10196
}
10297
}

samples/ConsoleSample/SampleRunner.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,15 @@ private async Task RunOrderCrudExamples()
7777
}
7878
else
7979
{
80-
Console.WriteLine($"❌ Failed to create order: {string.Join(", ", createResult.Errors)}\n");
80+
Console.WriteLine($"❌ Failed to create order: {createResult.Message}\n");
8181
}
8282

8383
// Demonstrate validation errors
8484
Console.WriteLine("🚫 Testing validation errors...");
8585
var invalidResult = await _mediator.InvokeAsync<Result<Order>>(new CreateOrder("", -100m, "Invalid order"));
8686
if (!invalidResult.IsSuccess)
8787
{
88-
Console.WriteLine($"❌ Validation failed as expected: {string.Join(", ", invalidResult.Errors)}\n");
88+
Console.WriteLine($"❌ Validation failed as expected: {invalidResult.Message}\n");
8989
}
9090
}
9191

src/Foundatio.Mediator.SourceGenerator/HandlerGenerator.cs

Lines changed: 20 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,11 @@ private static void GenerateHandleMethod(IndentedStringBuilder source, HandlerIn
183183
result = handler.HasReturnValue ? $" ({handler.ReturnType.UnwrappedFullName}){m.Middleware.Identifier.ToCamelCase()}Result.Value!" : "";
184184
if (handler.ReturnType.IsResult)
185185
{
186-
result = $" {m.Middleware.Identifier.ToCamelCase()}Result.Value is Foundatio.Mediator.Result result ? ({handler.ReturnType.UnwrappedFullName})result : ({handler.ReturnType.UnwrappedFullName}?){m.Middleware.Identifier.ToCamelCase()}Result.Value ?? default({handler.ReturnType.UnwrappedFullName})!";
186+
result = $" {m.Middleware.Identifier.ToCamelCase()}Result.Value is Foundatio.Mediator.Result result ? ({handler.ReturnType.UnwrappedFullName})result : ({handler.ReturnType.UnwrappedFullName}?){m.Middleware.Identifier.ToCamelCase()}Result.Value!";
187+
}
188+
else if (handler.ReturnType.IsTuple)
189+
{
190+
result = $" (({m.Middleware.Identifier.ToCamelCase()}Result.Value is Foundatio.Mediator.Result result ? ({handler.ReturnType.TupleItems.First().TypeFullName})result : ({handler.ReturnType.TupleItems.First().TypeFullName}?){m.Middleware.Identifier.ToCamelCase()}Result.Value!), {String.Join(", ", handler.ReturnType.TupleItems.Skip(1).Select(i => i.IsNullable ? "null" : "default"))})";
187191
}
188192
source.AppendLine($"if ({m.Middleware.Identifier.ToCamelCase()}Result.IsShortCircuited)");
189193
source.AppendLine("{");
@@ -296,13 +300,13 @@ private static void GenerateUntypedHandleMethod(IndentedStringBuilder source, Ha
296300
{
297301
if (!r.IsSuccess)
298302
{
299-
throw new InvalidCastException($"Handler returned failed result with status {r.Status} for requested type { responseType?.Name ?? "null" }");
303+
throw new InvalidCastException($"Handler returned failed result with status {r.Status} for requested type { responseType?.Name ?? "null" }");
300304
}
301305
302306
var resultValue = r.GetValue();
303307
if (resultValue != null && responseType.IsAssignableFrom(resultValue.GetType()))
304308
{
305-
return resultValue;
309+
return resultValue;
306310
}
307311
}
308312
""");
@@ -341,7 +345,7 @@ private static void GenerateInterceptorMethod(IndentedStringBuilder source, Hand
341345
{
342346
string interceptorMethod = $"Intercept{methodName}{methodIndex}";
343347
string handlerMethod = GetHandlerMethodName(handler);
344-
bool methodIsAsync = methodName.EndsWith("Async");
348+
bool methodIsAsync = methodName.EndsWith("Async") || handler.IsAsync;
345349

346350
foreach (var callSite in callSites)
347351
{
@@ -366,7 +370,17 @@ private static void GenerateInterceptorMethod(IndentedStringBuilder source, Hand
366370
source.AppendLine($"var result = {asyncModifier}{handlerMethod}(mediator, typedMessage, cancellationToken);");
367371
source.AppendLine();
368372

369-
GenerateOptimizedTupleHandling(source, handler, responseType);
373+
var returnItem = handler.ReturnType.TupleItems.FirstOrDefault(i => i.TypeFullName == responseType.FullName);
374+
var publishItems = handler.ReturnType.TupleItems.Except([returnItem]);
375+
376+
foreach (var publishItem in publishItems)
377+
{
378+
source.AppendLineIf($"if (result.{publishItem.Name} != null)", publishItem.IsNullable);
379+
source.AppendIf(" ", publishItem.IsNullable).AppendLine($"await mediator.PublishAsync(result.{publishItem.Name}, cancellationToken);");
380+
}
381+
source.AppendLineIf(publishItems.Any());
382+
383+
source.AppendLine($"return result.{returnItem.Name};");
370384
}
371385
else
372386
{
@@ -493,87 +507,9 @@ private static void GenerateGetOrCreateHandler(IndentedStringBuilder source, Han
493507
""");
494508
}
495509

496-
private static void GenerateOptimizedTupleHandling(IndentedStringBuilder source, HandlerInfo handler, TypeSymbolInfo responseType)
497-
{
498-
var tupleFields = handler.ReturnType.TupleItems.ToList();
499-
500-
if (tupleFields.Count == 0)
501-
{
502-
source.AppendLine($"return default({responseType.FullName});");
503-
return;
504-
}
505-
506-
int returnItemIndex = -1;
507-
var publishItems = new List<int>();
508-
509-
for (int i = 0; i < tupleFields.Count; i++)
510-
{
511-
var tupleItem = tupleFields[i];
512-
string fieldType = tupleItem.TypeFullName;
513-
514-
if (fieldType == responseType.FullName)
515-
{
516-
if (returnItemIndex == -1)
517-
{
518-
returnItemIndex = i;
519-
}
520-
else
521-
{
522-
publishItems.Add(i);
523-
}
524-
}
525-
else
526-
{
527-
publishItems.Add(i);
528-
}
529-
}
530-
531-
foreach (int publishIndex in publishItems)
532-
{
533-
var tupleItem = tupleFields[publishIndex];
534-
string itemAccess = $"result.{tupleItem.Name}";
535-
536-
bool needsNullCheck = tupleItem.IsNullable;
537-
538-
if (responseType.IsTask)
539-
{
540-
if (needsNullCheck)
541-
{
542-
source.AppendLine($"if ({itemAccess} != null) await mediator.PublishAsync({itemAccess}, cancellationToken);");
543-
}
544-
else
545-
{
546-
source.AppendLine($"await mediator.PublishAsync({itemAccess}, cancellationToken);");
547-
}
548-
}
549-
else
550-
{
551-
if (needsNullCheck)
552-
{
553-
source.AppendLine($"if ({itemAccess} != null) mediator.PublishAsync({itemAccess}, CancellationToken.None).GetAwaiter().GetResult();");
554-
}
555-
else
556-
{
557-
source.AppendLine($"mediator.PublishAsync({itemAccess}, CancellationToken.None).GetAwaiter().GetResult();");
558-
}
559-
}
560-
}
561-
562-
source.AppendLine();
563-
if (returnItemIndex >= 0)
564-
{
565-
var returnTupleItem = tupleFields[returnItemIndex];
566-
source.AppendLine($"return result.{returnTupleItem.Name};");
567-
}
568-
else
569-
{
570-
source.AppendLine($"return default({responseType.UnwrappedFullName})!;");
571-
}
572-
}
573-
574510
public static string GetHandlerClassName(HandlerInfo handler)
575511
{
576-
return $"{handler.Identifier}_{handler.MessageType.Identifier}_Wrapper";
512+
return $"{handler.Identifier}_{handler.MessageType.Identifier}_Handler";
577513
}
578514

579515
public static string GetHandlerMethodName(HandlerInfo handler)

src/Foundatio.Mediator.SourceGenerator/Models/HandlerInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal readonly record struct HandlerInfo
1010
public TypeSymbolInfo MessageType { get; init; }
1111
public bool HasReturnValue => !ReturnType.IsVoid;
1212
public TypeSymbolInfo ReturnType { get; init; }
13-
public bool IsAsync => ReturnType.IsTask || Middleware.Any(m => m.IsAsync);
13+
public bool IsAsync => ReturnType.IsTask || ReturnType.IsTuple || Middleware.Any(m => m.IsAsync);
1414
public bool IsStatic { get; init; }
1515
public EquatableArray<ParameterInfo> Parameters { get; init; }
1616
public EquatableArray<CallSiteInfo> CallSites { get; init; }

src/Foundatio.Mediator.SourceGenerator/Utility/TypeExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ internal static EquatableArray<TupleItemInfo> GetTupleItems(this ITypeSymbol typ
154154
{
155155
Name = e.Name ?? e.CorrespondingTupleField!.Name,
156156
Field = e.CorrespondingTupleField!.Name,
157+
IsNullable = e.Type.IsNullable(compilation),
157158
TypeFullName = e.Type.ToDisplayString()
158159
}).ToArray());
159160
}

src/Foundatio.Mediator/IResult.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,19 @@ public interface IResult
2525
/// </summary>
2626
/// <returns>The result value.</returns>
2727
object? GetValue();
28+
29+
/// <summary>
30+
/// Gets the status of the result.
31+
/// </summary>
32+
string Message { get; }
33+
34+
/// <summary>
35+
/// Gets the location of a newly created resource (for Created status).
36+
/// </summary>
37+
string Location { get; }
38+
39+
/// <summary>
40+
/// Gets the validation errors associated with the result.
41+
/// </summary>
42+
IEnumerable<ValidationError> ValidationErrors { get; }
2843
}

0 commit comments

Comments
 (0)