-
Notifications
You must be signed in to change notification settings - Fork 12
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.
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.
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);
}
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)));
}
}
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;
}
}
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);
}
}
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).
-
Build on
IMessageMediator
: Your custom mediators should delegate the core mediation logic to the built-inIMessageMediator
and only provide the custom strategy and options. - Follow Existing Patterns: Model your custom modules and extensions on the existing LiteBus architecture for consistency.
-
Ensure DI Registration: Your
IModule
implementation is responsible for registering all necessary services for your module with theIDependencyRegistry
.