Skip to content

Polymorphic Dispatch

A. Shafie edited this page Sep 26, 2025 · 1 revision

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.

What is Polymorphic Dispatch?

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.

How It Works

Consider a set of commands that all relate to auditable actions.

1. Define a Base Message Type

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
}

2. Create a Handler for the Base Type

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;
    }
}

3. Mediation

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.

Technical Explanation

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.

Use Cases

  1. Auditing: Create a single post-handler for an IAuditable interface to log all state-changing operations.
  2. Authorization: Implement a single pre-handler for a ISecuredOperation interface to check user permissions for a family of related commands or queries.
  3. Validation: A pre-handler for a base IPaginatedQuery could validate that pagination parameters (PageNumber, PageSize) are within valid ranges for all queries that support pagination.
  4. Tenant Isolation: A pre-handler for an ITenantSpecific interface can ensure the request is scoped to the correct tenant.

Best Practices

  1. Define Clear Base Contracts: The base message interface or class should define the common data needed by the polymorphic handler.
  2. 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.
  3. 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.
Clone this wiki locally