-
Couldn't load subscription status.
- Fork 12
Polymorphic Dispatch
Polymorphic dispatch is an advanced feature of LiteBus that allows a single handler to process a hierarchy of related messages. By creating a handler for a base message type, you can automatically handle any message that derives from it.
Polymorphic dispatch leverages C# contravariance (in keyword) in handler interfaces. It allows you to write common, cross-cutting logic once and apply it to an entire family of commands, queries, or events.
Important: This feature applies to pre-handlers, post-handlers, and error handlers. It does not apply to main handlers, as they are resolved to the most specific message type to ensure a single, definitive handler for each message.
Consider a set of commands that all relate to auditable actions.
First, define a base class or interface that all related messages will implement.
// Base interface for all auditable commands
public interface IAuditableCommand : ICommand
{
Guid CorrelationId { get; }
string UserId { get; }
}
// Concrete commands that implement the base interface
public sealed class CreateProductCommand : IAuditableCommand, ICommand<Guid>
{
// ... properties
}
public sealed class DeleteUserCommand : IAuditableCommand, ICommand
{
// ... properties
}Now, create a single pre-handler that targets the base IAuditableCommand interface.
// This single pre-handler will run for ANY command implementing IAuditableCommand.
public sealed class AuditingPreHandler : ICommandPreHandler<IAuditableCommand>
{
private readonly IAuditLogger _auditLogger;
public AuditingPreHandler(IAuditLogger auditLogger)
{
_auditLogger = auditLogger;
}
public Task PreHandleAsync(IAuditableCommand command, CancellationToken cancellationToken = default)
{
// Log that an auditable action is about to occur.
_auditLogger.Log(
$"Audit: Action of type '{command.GetType().Name}' initiated by user '{command.UserId}'."
);
return Task.CompletedTask;
}
}When you send a derived command, LiteBus automatically discovers and executes the handler for the base type.
// Sending a CreateProductCommand...
await _commandMediator.SendAsync(new CreateProductCommand { ... });
// ...will trigger AuditingPreHandler.
// Sending a DeleteUserCommand...
await _commandMediator.SendAsync(new DeleteUserCommand { ... });
// ...will also trigger AuditingPreHandler.This behavior is enabled by the contravariant type parameter (in TMessage) on the handler interfaces:
// The 'in' keyword allows a handler for a base type to accept a derived type.
public interface IAsyncMessagePreHandler<in TMessage> { ... }
public interface IAsyncMessagePostHandler<in TMessage> { ... }When LiteBus resolves handlers, its ActualTypeOrFirstAssignableTypeMessageResolveStrategy finds handlers for both the concrete message type and any of its base types or implemented interfaces.
-
Auditing: Create a single post-handler for an
IAuditableinterface to log all state-changing operations. -
Authorization: Implement a single pre-handler for a
ISecuredOperationinterface to check user permissions for a family of related commands or queries. -
Validation: A pre-handler for a base
IPaginatedQuerycould validate that pagination parameters (PageNumber,PageSize) are within valid ranges for all queries that support pagination. -
Tenant Isolation: A pre-handler for an
ITenantSpecificinterface can ensure the request is scoped to the correct tenant.
- Define Clear Base Contracts: The base message interface or class should define the common data needed by the polymorphic handler.
- Use for Cross-Cutting Concerns: Polymorphic dispatch is ideal for logic that applies uniformly across a set of related messages, such as security, logging, or validation.
- Remember the Scope: This feature is for pre-handlers, post-handlers, and error handlers. Each concrete command, query, or event must still have its own specific main handler.