WHIZ080: Multiple Handlers for RPC Message¶
Verified by tests
ReceptorDiscoveryGeneratorTests — library CI run #31657041675 (2026-08-13)
Severity: Warning Category: Handler Validation
Description¶
This warning is reported when multiple async receptor implementations (IReceptor<TMessage, TResponse>) are found for a message type that returns a response (RPC pattern). Since RPC calls expect a single response, having multiple handlers creates ambiguity about which response to return.
Void receptors (IReceptor<TMessage>, event-style dispatch) and sync receptors (ISyncReceptor) are excluded — multiple handlers are allowed for those.
Note: This diagnostic is disabled by default pending implementation of key-based RPC handler selection.
Diagnostic Message¶
The second placeholder lists the conflicting handler class names:
Multiple handlers found for 'GetOrderQuery' which returns a response (found: OrderQueryReceptor, CachedOrderQueryReceptor), but RPC requires exactly one handler
Understanding RPC vs Event Patterns¶
RPC Pattern (Single Handler Expected)¶
RPC (Remote Procedure Call) pattern uses IReceptor<TMessage, TResponse> where a response is returned:
RPC Pattern (Single Handler Expected)
// Query expecting a single response
public record GetOrderQuery(Guid OrderId);
public record OrderDto(Guid Id, string Status, decimal Total);
// Single handler expected
public class OrderQueryReceptor : IReceptor<GetOrderQuery, OrderDto> {
public ValueTask<OrderDto> HandleAsync(GetOrderQuery query, CancellationToken cancellationToken = default) {
// Return the order data
return ValueTask.FromResult(new OrderDto(...));
}
}
Event Pattern (Multiple Handlers Allowed)¶
For void receptors (event handlers), multiple handlers are expected:
Event Pattern (Multiple Handlers Allowed)
// Event broadcast to multiple handlers - OK
public record OrderPlaced(Guid OrderId) : IEvent;
public class InventoryReceptor : IReceptor<OrderPlaced> {
public ValueTask HandleAsync(OrderPlaced @event, CancellationToken cancellationToken = default) => ...;
}
public class NotificationReceptor : IReceptor<OrderPlaced> {
public ValueTask HandleAsync(OrderPlaced @event, CancellationToken cancellationToken = default) => ...;
}
Why Multiple RPC Handlers Are Problematic¶
When you have multiple handlers for an RPC message:
- Ambiguous Response - Which handler's response should be returned?
- Unpredictable Behavior - Handler execution order is not guaranteed
- Contract Violation - Caller expects one definitive response
How to Fix¶
Option 1: Remove Duplicate Handlers¶
Keep only one handler for the RPC message:
Option 1: Remove Duplicate Handlers
// Keep only one handler
public class OrderQueryReceptor : IReceptor<GetOrderQuery, OrderDto> {
public ValueTask<OrderDto> HandleAsync(GetOrderQuery query, CancellationToken cancellationToken = default) => ...;
}
// Remove or consolidate the second handler
// public class CachedOrderQueryReceptor : IReceptor<GetOrderQuery, OrderDto> { }
Option 2: Use Decorator Pattern¶
If you need caching or cross-cutting concerns, use the decorator pattern:
Option 2: Use Decorator Pattern
public class CachedOrderQueryReceptor : IReceptor<GetOrderQuery, OrderDto> {
private readonly OrderQueryReceptor _inner;
private readonly ICache _cache;
public async ValueTask<OrderDto> HandleAsync(GetOrderQuery query, CancellationToken cancellationToken = default) {
var cached = await _cache.GetAsync<OrderDto>(query.OrderId);
if (cached != null) return cached;
var result = await _inner.HandleAsync(query, cancellationToken);
await _cache.SetAsync(query.OrderId, result);
return result;
}
}
Option 3: Use Different Message Types¶
If handlers serve different purposes, use distinct message types:
Option 3: Use Different Message Types
// Separate queries for different purposes
public record GetOrderQuery(Guid OrderId);
public record GetOrderWithDetailsQuery(Guid OrderId);
public class OrderQueryReceptor : IReceptor<GetOrderQuery, OrderDto> { }
public class OrderDetailsReceptor : IReceptor<GetOrderWithDetailsQuery, OrderDetailsDto> { }
Future: Key-Based Handler Selection¶
A future Whizbang release will support key-based RPC handler selection using [RpcKey]:
Future: Key-Based Handler Selection
// Future syntax (not yet implemented)
[RpcKey("default")]
public class DefaultOrderReceptor : IReceptor<GetOrderQuery, OrderDto> { }
[RpcKey("cached")]
public class CachedOrderReceptor : IReceptor<GetOrderQuery, OrderDto> { }
// Caller specifies which handler to use
var result = await dispatcher.InvokeAsync<GetOrderQuery, OrderDto>(query, rpcKey: "cached");
Configuration¶
This diagnostic is disabled by default (isEnabledByDefault: false). It is reported at compilation level with no source location, so a per-file .editorconfig section ([*.cs]) cannot enable it — use a global analyzer config file instead:
Configuration
Note that <WarningsAsErrors> alone does not enable a disabled-by-default diagnostic — it only changes the severity of diagnostics that are already being reported.
Related Concepts¶
- LocalInvoke - The RPC dispatch method that expects a single response
- Receptors - Handler implementations for messages
- Dispatcher - Routes messages to appropriate handlers
See Also¶
- Receptors - Receptor implementation patterns
- Dispatcher - Message routing
- CQRS Pattern - Command Query Responsibility Segregation