Skip to content

Commit 95bd54e

Browse files
committed
Short circuit benchmark
1 parent c7a4c35 commit 95bd54e

9 files changed

Lines changed: 143 additions & 12 deletions

File tree

.github/scripts/Update-BenchmarkDocs.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ CreateOrder returns an Order and publishes OrderCreatedEvent to 2 handlers. Foun
169169
170170
$(Build-Table $groups['CascadingMessages'])
171171
172-
### Short-Circuit Middleware (Foundatio Only)
172+
### Short-Circuit Middleware
173173
174-
Middleware returns cached result; handler is never invoked. Useful for caching or authorization.
174+
Middleware returns cached result; handler is never invoked. Each library uses its idiomatic short-circuit approach (IPipelineBehavior, HandlerResult.ShortCircuit, HandlerContinuation.Stop, etc.).
175175
176176
$(Build-Table $groups['ShortCircuit'])
177177

benchmarks/Foundatio.Mediator.Benchmarks/CoreBenchmarks.cs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public class CoreBenchmarks
3636
private readonly PingCommand _pingCommand = new("test-123");
3737
private readonly GetOrder _getOrder = new(42);
3838
private readonly GetOrderWithDependencies _getOrderWithDependencies = new(42);
39-
private readonly GetOrderShortCircuit _getOrderShortCircuit = new(42);
39+
private readonly GetCachedOrder _getCachedOrder = new(42);
4040
private readonly UserRegisteredEvent _userRegisteredEvent = new("User-456", "test@example.com");
4141
private readonly CreateOrder _createOrder = new(123, 99.99m);
4242

@@ -46,6 +46,7 @@ public class CoreBenchmarks
4646
private readonly MediatorNetGetOrderWithDependencies _mediatorNetGetOrderWithDependencies = new(42);
4747
private readonly MediatorNetUserRegisteredEvent _mediatorNetUserRegisteredEvent = new("User-456", "test@example.com");
4848
private readonly MediatorNetCreateOrder _mediatorNetCreateOrder = new(123, 99.99m);
49+
private readonly MediatorNetGetCachedOrder _mediatorNetGetCachedOrder = new(42);
4950

5051
[GlobalSetup]
5152
public void Setup()
@@ -69,6 +70,8 @@ public void Setup()
6970
cfg.RegisterServicesFromAssemblyContaining<CoreBenchmarks>();
7071
// Register timing behavior for GetOrderWithDependencies to match Foundatio's middleware
7172
cfg.AddBehavior<MediatR.IPipelineBehavior<GetOrderWithDependencies, Order>, Handlers.MediatR.TimingBehavior<GetOrderWithDependencies, Order>>();
73+
// Register short-circuit behavior for GetCachedOrder
74+
cfg.AddBehavior<MediatR.IPipelineBehavior<GetCachedOrder, Order>, Handlers.MediatR.ShortCircuitBehavior>();
7275
// Use parallel notification publisher to match Foundatio/MassTransit behavior
7376
cfg.NotificationPublisher = new MediatR.NotificationPublishers.TaskWhenAllPublisher();
7477
});
@@ -88,13 +91,16 @@ public void Setup()
8891
cfg.AddConsumer<Handlers.MassTransit.MassTransitCreateOrderConsumer>();
8992
cfg.AddConsumer<Handlers.MassTransit.MassTransitOrderCreatedConsumer1>();
9093
cfg.AddConsumer<Handlers.MassTransit.MassTransitOrderCreatedConsumer2>();
94+
cfg.AddConsumer<Handlers.MassTransit.MassTransitShortCircuitConsumer>();
9195
});
9296
_masstransitServices = masstransitServices.BuildServiceProvider();
9397
_masstransitMediator = _masstransitServices.GetRequiredService<MassTransit.Mediator.IMediator>();
9498

9599
// Setup Mediator.SourceGenerator (MediatorNet)
96100
var mediatorNetServices = new ServiceCollection();
97101
mediatorNetServices.AddSingleton<IOrderService, OrderService>();
102+
// Register short-circuit behavior before AddMediator so it's available in the pipeline
103+
mediatorNetServices.AddSingleton<MediatorLib.IPipelineBehavior<MediatorNetGetCachedOrder, Order>, MediatorNetShortCircuitBehavior>();
98104
mediatorNetServices.AddMediator(options =>
99105
{
100106
options.ServiceLifetime = ServiceLifetime.Singleton;
@@ -119,7 +125,10 @@ public void Setup()
119125
.IncludeType<Handlers.Wolverine.WolverineQueryWithDependenciesHandler>()
120126
.IncludeType<Handlers.Wolverine.WolverineCreateOrderHandler>()
121127
.IncludeType<Handlers.Wolverine.WolverineOrderCreatedHandler1>()
122-
.IncludeType<Handlers.Wolverine.WolverineOrderCreatedHandler2>();
128+
.IncludeType<Handlers.Wolverine.WolverineOrderCreatedHandler2>()
129+
.IncludeType<Handlers.Wolverine.WolverineShortCircuitHandler>();
130+
// Register short-circuit middleware for GetCachedOrder
131+
opts.Policies.ForMessagesOfType<GetCachedOrder>().AddMiddleware(typeof(Handlers.Wolverine.WolverineShortCircuitMiddleware));
123132
})
124133
.Build();
125134
_wolverineHost.Start();
@@ -329,11 +338,37 @@ public async Task<Order> MediatorNet_CascadingMessages()
329338
return await _mediatorNetMediator.Send(_mediatorNetCreateOrder);
330339
}
331340

332-
// Scenario 6: Short-circuit middleware - Foundatio-only feature
333-
// Middleware returns cached result, handler is never invoked
341+
// Scenario 6: Short-circuit middleware - returns cached result, handler is never invoked
342+
// Tests the cost of short-circuiting via middleware (caching, validation, auth, etc.)
334343
[Benchmark]
335344
public async Task<Order> Foundatio_ShortCircuit()
336345
{
337-
return await _foundatioMediator.InvokeAsync<Order>(_getOrderShortCircuit);
346+
return await _foundatioMediator.InvokeAsync<Order>(_getCachedOrder);
347+
}
348+
349+
[Benchmark]
350+
public async Task<Order> MediatR_ShortCircuit()
351+
{
352+
return await _mediatrMediator.Send(_getCachedOrder);
353+
}
354+
355+
[Benchmark]
356+
public async Task<Order> MassTransit_ShortCircuit()
357+
{
358+
var client = _masstransitMediator.CreateRequestClient<GetCachedOrder>();
359+
var response = await client.GetResponse<Order>(_getCachedOrder);
360+
return response.Message;
361+
}
362+
363+
[Benchmark]
364+
public async Task<Order?> Wolverine_ShortCircuit()
365+
{
366+
return await _wolverineBus.InvokeAsync<Order>(_getCachedOrder);
367+
}
368+
369+
[Benchmark]
370+
public async Task<Order> MediatorNet_ShortCircuit()
371+
{
372+
return await _mediatorNetMediator.Send(_mediatorNetGetCachedOrder);
338373
}
339374
}

benchmarks/Foundatio.Mediator.Benchmarks/Handlers/Foundatio/FoundatioHandlers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public async Task HandleAsync(OrderCreatedEvent notification, CancellationToken
9191
// Scenario 6: Short-circuit handler (never actually called due to ShortCircuitMiddleware)
9292
public class FoundatioShortCircuitHandler
9393
{
94-
public Task<Order> HandleAsync(GetOrderShortCircuit query, CancellationToken cancellationToken = default)
94+
public Task<Order> HandleAsync(GetCachedOrder query, CancellationToken cancellationToken = default)
9595
{
9696
// This should never be called - middleware short-circuits before reaching handler
9797
throw new InvalidOperationException("Short-circuit middleware should have prevented this call");

benchmarks/Foundatio.Mediator.Benchmarks/Handlers/MassTransit/MassTransitHandlers.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,17 @@ public async Task Consume(ConsumeContext<OrderCreatedEvent> context)
8989
await Task.CompletedTask;
9090
}
9191
}
92+
93+
// Scenario 6: Short-circuit handler - MassTransit doesn't have separate middleware for in-memory mediator,
94+
// so we simulate short-circuit by returning cached value immediately in the consumer.
95+
// Note: In real MassTransit usage with transport, you'd use a filter to short-circuit.
96+
public class MassTransitShortCircuitConsumer : IConsumer<GetCachedOrder>
97+
{
98+
private static readonly Order _cachedOrder = new(999, 49.99m, DateTime.UtcNow);
99+
100+
public async Task Consume(ConsumeContext<GetCachedOrder> context)
101+
{
102+
// Immediately respond with cached value - simulates cache hit
103+
await context.RespondAsync(_cachedOrder);
104+
}
105+
}

benchmarks/Foundatio.Mediator.Benchmarks/Handlers/MediatR/MediatRHandlers.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,27 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
123123
}
124124
}
125125
}
126+
127+
// Scenario 6: Short-circuit handler - MediatR uses IPipelineBehavior to short-circuit
128+
[FoundatioIgnore]
129+
public class MediatRShortCircuitHandler : IRequestHandler<GetCachedOrder, Order>
130+
{
131+
public Task<Order> Handle(GetCachedOrder request, CancellationToken cancellationToken)
132+
{
133+
// This should never be called - pipeline behavior short-circuits before reaching handler
134+
throw new InvalidOperationException("Short-circuit behavior should have prevented this call");
135+
}
136+
}
137+
138+
// MediatR short-circuit behavior - returns cached value without calling handler
139+
[FoundatioIgnore]
140+
public class ShortCircuitBehavior : IPipelineBehavior<GetCachedOrder, Order>
141+
{
142+
private static readonly Order _cachedOrder = new(999, 49.99m, DateTime.UtcNow);
143+
144+
public Task<Order> Handle(GetCachedOrder request, RequestHandlerDelegate<Order> next, CancellationToken cancellationToken)
145+
{
146+
// Short-circuit by returning cached value - never calls next()
147+
return Task.FromResult(_cachedOrder);
148+
}
149+
}

benchmarks/Foundatio.Mediator.Benchmarks/Handlers/MediatorNet/MediatorNetHandlers.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public record MediatorNetCreateOrder(int CustomerId, decimal Amount) : MediatorL
2424
[FoundatioIgnore]
2525
public record MediatorNetOrderCreatedEvent(int OrderId, int CustomerId) : MediatorLib.INotification;
2626

27+
[FoundatioIgnore]
28+
public record MediatorNetGetCachedOrder(int Id) : MediatorLib.IQuery<Order>;
29+
2730
// Scenario 1: Command handler (InvokeAsync without response)
2831
[FoundatioIgnore]
2932
public class MediatorNetCommandHandler : MediatorLib.ICommandHandler<MediatorNetPingCommand>
@@ -122,3 +125,27 @@ public ValueTask Handle(MediatorNetOrderCreatedEvent notification, CancellationT
122125
return ValueTask.CompletedTask;
123126
}
124127
}
128+
129+
// Scenario 6: Short-circuit handler - MediatorNet uses IPipelineBehavior to short-circuit
130+
[FoundatioIgnore]
131+
public class MediatorNetShortCircuitHandler : MediatorLib.IQueryHandler<MediatorNetGetCachedOrder, Order>
132+
{
133+
public ValueTask<Order> Handle(MediatorNetGetCachedOrder query, CancellationToken cancellationToken)
134+
{
135+
// This should never be called - pipeline behavior short-circuits before reaching handler
136+
throw new InvalidOperationException("Short-circuit behavior should have prevented this call");
137+
}
138+
}
139+
140+
// MediatorNet short-circuit behavior - returns cached value without calling handler
141+
[FoundatioIgnore]
142+
public class MediatorNetShortCircuitBehavior : MediatorLib.IPipelineBehavior<MediatorNetGetCachedOrder, Order>
143+
{
144+
private static readonly Order _cachedOrder = new(999, 49.99m, DateTime.UtcNow);
145+
146+
public ValueTask<Order> Handle(MediatorNetGetCachedOrder message, MediatorLib.MessageHandlerDelegate<MediatorNetGetCachedOrder, Order> next, CancellationToken cancellationToken)
147+
{
148+
// Short-circuit by returning cached value - never calls next()
149+
return ValueTask.FromResult(_cachedOrder);
150+
}
151+
}

benchmarks/Foundatio.Mediator.Benchmarks/Handlers/Wolverine/WolverineHandlers.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,28 @@ public Task Handle(OrderCreatedEvent notification)
8787
return Task.CompletedTask;
8888
}
8989
}
90+
91+
// Scenario 6: Short-circuit - Wolverine uses Before middleware with HandlerContinuation.Stop
92+
[FoundatioIgnore]
93+
public class WolverineShortCircuitHandler
94+
{
95+
public Task<Order> Handle(GetCachedOrder query)
96+
{
97+
// This should never be called - middleware short-circuits before reaching handler
98+
throw new InvalidOperationException("Short-circuit middleware should have prevented this call");
99+
}
100+
}
101+
102+
// Wolverine short-circuit middleware - uses HandlerContinuation to stop processing
103+
[FoundatioIgnore]
104+
public class WolverineShortCircuitMiddleware
105+
{
106+
private static readonly Order _cachedOrder = new(999, 49.99m, DateTime.UtcNow);
107+
108+
// Wolverine Before method with tuple return for short-circuit
109+
public static (HandlerContinuation, Order) Before(GetCachedOrder message)
110+
{
111+
// Short-circuit by returning Stop with the cached value
112+
return (HandlerContinuation.Stop, _cachedOrder);
113+
}
114+
}

benchmarks/Foundatio.Mediator.Benchmarks/Messages/BenchmarkMessages.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,11 @@ public record UserRegisteredEvent(string UserId, string Email) : MediatR.INotifi
1919
public record CreateOrder(int CustomerId, decimal Amount) : IRequest<Order>;
2020
public record OrderCreatedEvent(int OrderId, int CustomerId) : MediatR.INotification;
2121

22-
// Scenario 6: Short-circuit middleware (cache hit simulation) - Foundatio only
23-
public record GetOrderShortCircuit(int Id);
22+
// Scenario 6: Short-circuit / Cache-hit - tests middleware that returns early without calling handler
23+
// Each library implements this with their idiomatic approach:
24+
// - Foundatio: HandlerResult.ShortCircuit(value) in middleware
25+
// - MediatR: IPipelineBehavior returns cached value directly
26+
// - Wolverine: HandlerContinuation.Stop with cached value
27+
// - MediatorNet: IPipelineBehavior returns cached value directly
28+
// - MassTransit: Filter returns without calling next.Send()
29+
public record GetCachedOrder(int Id) : IRequest<Order>;

benchmarks/Foundatio.Mediator.Benchmarks/Middleware/BenchmarkMiddleware.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ public void Finally(GetOrderWithDependencies message, Stopwatch? stopwatch, Hand
2525

2626
/// <summary>
2727
/// Short-circuit middleware that immediately returns a cached result without calling the handler.
28-
/// This demonstrates Foundatio's unique short-circuit capability.
28+
/// This demonstrates middleware returning early (cache hit, validation success with cached result, etc.)
2929
/// </summary>
3030
public class ShortCircuitMiddleware
3131
{
3232
private static readonly Order _cachedOrder = new(999, 49.99m, DateTime.UtcNow);
3333

34-
public HandlerResult Before(GetOrderShortCircuit message)
34+
public HandlerResult Before(GetCachedOrder message)
3535
{
3636
// Always short-circuit with cached result - simulates cache hit scenario
3737
return HandlerResult.ShortCircuit(_cachedOrder);

0 commit comments

Comments
 (0)