Extensibility
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.
// The base notification message contract
public interface INotification { }
// The handler contract
public interface INotificationHandler<in TNotification> : 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. Custom mediators on IMessageMediator use MessageMediationRequest<TMessage, TResult> (replacing MessageMediationRequest); pass CancellationToken to Mediate, not inside the request bag. See Migration Guide v6.
// 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 request = new MessageMediationRequest<INotification, Task>
{
MessageMediationStrategy = mediationStrategy,
MessageResolveStrategy = resolveStrategy,
};
return _messageMediator.Mediate(notification, request, cancellationToken);
}
}
// The module builder for registering types
public sealed class NotificationModuleBuilder
{
private readonly IMessageRegistry _messageRegistry;
public NotificationModuleBuilder(IMessageRegistry messageRegistry) => _messageRegistry = messageRegistry;
public NotificationModuleBuilder Register<T>()
{
var type = typeof(T);
var isNotification = typeof(INotification).IsAssignableFrom(type);
var isHandler = type.GetInterfaces().Any(candidate =>
candidate.IsGenericType &&
candidate.GetGenericTypeDefinition() == typeof(INotificationHandler<>));
if (!isNotification && !isHandler)
{
throw new LiteBusNotSupportedException($"Type '{type}' is not a notification or notification handler.");
}
_messageRegistry.Register(type);
return this;
}
}
// The module itself, which handles DI registration
internal sealed class NotificationModule : IModule, IRequires<MessageModule>
{
private readonly Action<NotificationModuleBuilder> _builderAction;
public NotificationModule(Action<NotificationModuleBuilder> builderAction) => _builderAction = builderAction;
public void Build(IModuleConfiguration configuration)
{
// Run the user's configuration
var messageRegistry = configuration.GetOrCreateContext(() => new MessageRegistry());
_builderAction(new NotificationModuleBuilder(messageRegistry));
// 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 ILiteBusBuilder AddNotifications(
this ILiteBusBuilder builder,
Action<NotificationModuleBuilder> builderAction)
{
builder.Modules.Register(new NotificationModule(builderAction));
return builder;
}
}The application still calls AddMessaging(...). The graph validates IRequires<MessageModule> after the callback, independent of declaration order. A semantic abstraction should describe notifications and handlers only; do not expose registration-only marker interfaces through domain contracts.
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
- Build on
IMessageMediator: Your custom mediators should delegate the core mediation logic to the built-inIMessageMediatorand only provide the custom strategy and options. - Follow Existing Patterns: Model your custom modules and extensions on the existing LiteBus architecture for consistency.
- Register module services: Your
IModuleimplementation registers all services your module needs with theIDependencyRegistry.