Skip to content

Extensibility

A. Shafie edited this page Sep 26, 2025 · 3 revisions

Extensibility

LiteBus is designed with a modular and extensible architecture. While the built-in Command, Query, and Event modules cover most use cases, you can create your own custom modules, mediation strategies, and message resolution logic to tailor the framework to your specific needs.

Creating Custom Modules

A custom module allows you to introduce a new type of message with its own contracts and mediation logic. Let's create a simple "Notification" module as an example.

1. Define Abstractions

First, define the core interfaces for your module.

// A marker for all registrable types in this module
public interface IRegistrableNotificationConstruct { }

// The base notification message contract
public interface INotification : IRegistrableNotificationConstruct { }

// The handler contract
public interface INotificationHandler<in TNotification> : IRegistrableNotificationConstruct, IAsyncMessageHandler<TNotification>
    where TNotification : INotification
{
}

// The mediator contract
public interface INotificationMediator
{
    Task SendAsync(INotification notification, CancellationToken cancellationToken = default);
}

2. Implement the Module Logic

Create the IModule implementation, a builder, and the mediator.

// The mediator that will orchestrate the process
internal sealed class NotificationMediator : INotificationMediator
{
    private readonly IMessageMediator _messageMediator;

    public NotificationMediator(IMessageMediator messageMediator)
    {
        _messageMediator = messageMediator;
    }

    public Task SendAsync(INotification notification, CancellationToken cancellationToken = default)
    {
        // For notifications, we might want to broadcast to all handlers, like events.
        var mediationStrategy = new AsyncBroadcastMediationStrategy<INotification>(new EventMediationSettings());
        var resolveStrategy = new ActualTypeOrFirstAssignableTypeMessageResolveStrategy();
        var options = new MediateOptions<INotification, Task>
        {
            MessageMediationStrategy = mediationStrategy,
            MessageResolveStrategy = resolveStrategy,
            CancellationToken = cancellationToken
        };
        return _messageMediator.Mediate(notification, options);
    }
}

// The module builder for registering types
public sealed class NotificationModuleBuilder
{
    private readonly IMessageRegistry _messageRegistry;
    public NotificationModuleBuilder(IMessageRegistry messageRegistry) => _messageRegistry = messageRegistry;
    public NotificationModuleBuilder Register<T>() where T : IRegistrableNotificationConstruct => _messageRegistry.Register(typeof(T));
}

// The module itself, which handles DI registration
internal sealed class NotificationModule : IModule
{
    private readonly Action<NotificationModuleBuilder> _builderAction;

    public NotificationModule(Action<NotificationModuleBuilder> builderAction) => _builderAction = builderAction;

    public void Build(IModuleConfiguration configuration)
    {
        // Run the user's configuration
        _builderAction(new NotificationModuleBuilder(MessageRegistryAccessor.Instance));

        // Register the module's services
        configuration.DependencyRegistry.Register(new DependencyDescriptor(typeof(INotificationMediator), typeof(NotificationMediator)));
    }
}

3. Create the Extension Method

Finally, create an extension method to make registration easy.

public static class ModuleRegistryExtensions
{
    public static IModuleRegistry AddNotificationModule(this IModuleRegistry moduleRegistry, Action<NotificationModuleBuilder> builderAction)
    {
        if (!moduleRegistry.IsModuleRegistered<MessageModule>())
        {
            moduleRegistry.Register(new MessageModule(_ => { }));
        }
        moduleRegistry.Register(new NotificationModule(builderAction));
        return moduleRegistry;
    }
}

Custom Mediation Strategies

You can change how messages are processed by implementing IMessageMediationStrategy<TMessage, TMessageResult>. For example, you could create a strategy that retries failed handlers.

public class RetryMediationStrategy<TMessage> : IMessageMediationStrategy<TMessage, Task> where TMessage : notnull
{
    private readonly int _retryCount;

    public RetryMediationStrategy(int retryCount = 3) => _retryCount = retryCount;

    public async Task Mediate(TMessage message, IMessageDependencies deps, IExecutionContext context)
    {
        await deps.RunAsyncPreHandlers(message);

        var handler = deps.MainHandlers.Single().Handler.Value;

        for (int i = 0; i < _retryCount; i++)
        {
            try
            {
                await (Task)handler.Handle(message);
                break; // Success
            }
            catch (Exception) when (i < _retryCount - 1)
            {
                await Task.Delay(100 * (i + 1)); // Exponential backoff
            }
        }

        await deps.RunAsyncPostHandlers(message, null);
    }
}

Custom Message Resolution

You can change how LiteBus finds handlers for a message by implementing IMessageResolveStrategy. This is an advanced scenario, useful if you have custom rules for matching messages to handlers (e.g., based on versioning attributes).

Best Practices for Extensibility

  1. Build on IMessageMediator: Your custom mediators should delegate the core mediation logic to the built-in IMessageMediator and only provide the custom strategy and options.
  2. Follow Existing Patterns: Model your custom modules and extensions on the existing LiteBus architecture for consistency.
  3. Ensure DI Registration: Your IModule implementation is responsible for registering all necessary services for your module with the IDependencyRegistry.
Clone this wiki locally