-
Notifications
You must be signed in to change notification settings - Fork 10
Event Module
Events in LiteBus represent notifications that something has occurred within the system. Unlike commands and queries, events:
- Can be handled by multiple handlers (or none at all)
- Are typically named using past tense verbs (e.g.,
ProductCreatedEvent
) - Do not expect a return value from handlers
- Can be processed asynchronously
Key event characteristics:
- Represent something that has already happened
- Facilitate loose coupling between components
- Enable event-driven architectures
- Allow for fan-out notification patterns
LiteBus provides two approaches for defining events:
public interface IEvent : IRegistrableEventConstruct
{
}
Using this interface explicitly marks a class as an event.
public class ProductCreatedEvent : IEvent
{
public Guid ProductId { get; init; }
public string ProductName { get; init; }
public decimal Price { get; init; }
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
}
LiteBus also supports Plain Old CLR Objects (POCOs) as events, allowing you to use domain events without depending on the LiteBus framework:
// No dependency on IEvent - pure domain event
public class ProductStockLevelChanged
{
public Guid ProductId { get; init; }
public int OldStockLevel { get; init; }
public int NewStockLevel { get; init; }
public DateTime ChangedAt { get; init; } = DateTime.UtcNow;
}
This flexibility allows you to keep your domain model clean without reference to infrastructure concerns.
Event handlers process events and execute associated business logic:
public class NotifyCustomersAboutNewProductHandler : IEventHandler<ProductCreatedEvent>
{
private readonly IEmailService _emailService;
public NotifyCustomersAboutNewProductHandler(IEmailService emailService)
{
_emailService = emailService;
}
public async Task HandleAsync(ProductCreatedEvent @event, CancellationToken cancellationToken = default)
{
await _emailService.SendNewProductNotificationAsync(
@event.ProductId,
@event.ProductName,
cancellationToken);
}
}
Multiple handlers can be registered for the same event type:
public class UpdateSearchIndexHandler : IEventHandler<ProductCreatedEvent>
{
private readonly ISearchIndexClient _searchClient;
public UpdateSearchIndexHandler(ISearchIndexClient searchClient)
{
_searchClient = searchClient;
}
public async Task HandleAsync(ProductCreatedEvent @event, CancellationToken cancellationToken = default)
{
await _searchClient.IndexProductAsync(
@event.ProductId,
@event.ProductName,
cancellationToken);
}
}
Pre-handlers execute before all main event handlers, useful for logging, enrichment, or validation:
public class ProductEventEnrichmentHandler : IEventPreHandler<ProductCreatedEvent>
{
private readonly IProductRepository _repository;
public ProductEventEnrichmentHandler(IProductRepository repository)
{
_repository = repository;
}
public async Task PreHandleAsync(ProductCreatedEvent @event, CancellationToken cancellationToken = default)
{
// Attach additional product data to event context for handlers to access
var product = await _repository.GetByIdAsync(@event.ProductId, cancellationToken);
AmbientExecutionContext.Current.Items["ProductCategory"] = product?.Category;
}
}
public class EventLoggingPreHandler : IEventPreHandler
{
private readonly ILogger<EventLoggingPreHandler> _logger;
public EventLoggingPreHandler(ILogger<EventLoggingPreHandler> logger)
{
_logger = logger;
}
public Task PreHandleAsync(IEvent @event, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Event of type {@EventType} received at {Timestamp}",
@event.GetType().Name, DateTime.UtcNow);
return Task.CompletedTask;
}
}
Post-handlers execute after all main event handlers have completed:
public class ProductEventCompletionNotifier : IEventPostHandler<ProductCreatedEvent>
{
private readonly ILogger<ProductEventCompletionNotifier> _logger;
public ProductEventCompletionNotifier(ILogger<ProductEventCompletionNotifier> logger)
{
_logger = logger;
}
public Task PostHandleAsync(ProductCreatedEvent @event, object? messageResult,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("All handlers for product creation {ProductId} completed",
@event.ProductId);
return Task.CompletedTask;
}
}
public class EventMetricsPostHandler : IEventPostHandler
{
private readonly IMetricsRecorder _metrics;
public EventMetricsPostHandler(IMetricsRecorder metrics)
{
_metrics = metrics;
}
public Task PostHandleAsync(IEvent @event, object? messageResult,
CancellationToken cancellationToken = default)
{
_metrics.RecordEventProcessed(@event.GetType().Name);
return Task.CompletedTask;
}
}
Error handlers catch and process exceptions thrown during event handling:
public class ProductEventErrorHandler : IEventErrorHandler<ProductCreatedEvent>
{
private readonly ILogger<ProductEventErrorHandler> _logger;
public ProductEventErrorHandler(ILogger<ProductEventErrorHandler> logger)
{
_logger = logger;
}
public Task HandleErrorAsync(ProductCreatedEvent @event, object? messageResult,
Exception exception, CancellationToken cancellationToken = default)
{
_logger.LogError(exception, "Error processing product creation event for {ProductId}",
@event.ProductId);
return Task.CompletedTask;
}
}
public class GlobalEventErrorHandler : IEventErrorHandler
{
private readonly ILogger<GlobalEventErrorHandler> _logger;
public GlobalEventErrorHandler(ILogger<GlobalEventErrorHandler> logger)
{
_logger = logger;
}
public Task HandleErrorAsync(IEvent @event, object? messageResult,
Exception exception, CancellationToken cancellationToken = default)
{
_logger.LogError(exception, "Error processing event of type {EventType}",
@event.GetType().Name);
return Task.CompletedTask;
}
}
LiteBus provides two interfaces for publishing events:
// The full interface
public interface IEventMediator
{
Task PublishAsync(IEvent @event, EventMediationSettings? eventMediationSettings = null,
CancellationToken cancellationToken = default);
Task PublishAsync<TEvent>(TEvent @event, EventMediationSettings? eventMediationSettings = null,
CancellationToken cancellationToken = default) where TEvent : notnull;
}
// A semantic alias for IEventMediator
public interface IEventPublisher : IEventMediator
{
}
The IEventPublisher
interface is functionally identical to IEventMediator
but provides more semantic clarity.
// Publishing an IEvent object
await _eventMediator.PublishAsync(new ProductCreatedEvent
{
ProductId = product.Id,
ProductName = product.Name,
Price = product.Price
});
// Publishing a POCO event
await _eventMediator.PublishAsync(new ProductStockLevelChanged
{
ProductId = product.Id,
OldStockLevel = oldStock,
NewStockLevel = newStock
});
// Publishing an event with a specific tag
await _eventMediator.PublishAsync(new ProductCreatedEvent { /* ... */ }, "Marketing");
The EventMediationSettings
class controls event publication behavior:
// Create settings
var settings = new EventMediationSettings
{
// Throw if no handler exists for the event
ThrowIfNoHandlerFound = true,
Filters =
{
// Only handlers with specific tags
Tags = ["Marketing", "Notifications"],
// Filter handlers by predicate
HandlerPredicate = type => type.IsAssignableTo(typeof(IHighPriorityEventHandler))
}
};
// Publish with settings
await _eventMediator.PublishAsync(new ProductCreatedEvent { /* ... */ }, settings);
By default, ThrowIfNoHandlerFound
is false
, which means events with no handlers will be silently accepted (useful for optional handling scenarios).
LiteBus allows you to filter which handlers are executed for an event:
[HandlerTag("Marketing")]
public class MarketingNotificationHandler : IEventHandler<ProductCreatedEvent>
{
// Only executed when "Marketing" tag is included
}
[HandlerTag("Inventory")]
public class InventoryUpdateHandler : IEventHandler<ProductCreatedEvent>
{
// Only executed when "Inventory" tag is included
}
// Execute only marketing handlers
await _eventMediator.PublishAsync(new ProductCreatedEvent { /* ... */ }, "Marketing");
// Or using settings:
await _eventMediator.PublishAsync(new ProductCreatedEvent { /* ... */ }, new EventMediationSettings
{
Filters = { Tags = ["Marketing"] }
});
// Filter by implementing a marker interface
public interface IHighPriorityEventHandler {}
public class CriticalNotificationHandler : IEventHandler<SystemAlertEvent>, IHighPriorityEventHandler
{
// This handler implements the marker interface
}
// Only execute high priority handlers
await _eventMediator.PublishAsync(new SystemAlertEvent { /* ... */ }, new EventMediationSettings
{
Filters = { HandlerPredicate = type => type.IsAssignableTo(typeof(IHighPriorityEventHandler)) }
});
LiteBus supports generic events for flexible event modeling. When using generic events, the corresponding handlers must also be generic with matching type parameters:
// Generic event
public class EntityCreatedEvent<TEntity> : IEvent where TEntity : class
{
public required TEntity Entity { get; init; }
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
}
// Generic handler - must match the generic structure of the event
public class EntityCreatedHandler<TEntity> : IEventHandler<EntityCreatedEvent<TEntity>>
where TEntity : class
{
public Task HandleAsync(EntityCreatedEvent<TEntity> @event, CancellationToken cancellationToken = default)
{
var entity = @event.Entity;
// Process entity creation
return Task.CompletedTask;
}
}
// Registration in module configuration
services.AddLiteBus(configuration =>
{
configuration.AddEventModule(builder =>
{
// Register the generic handler
builder.Register(typeof(EntityCreatedHandler<>));
});
});
// Usage
await _eventMediator.PublishAsync(new EntityCreatedEvent<Product>
{
Entity = product
});
Note that you cannot currently use a non-generic handler with a generic event. For example, this will cause an error:
// This approach will NOT work
public class ProductCreatedHandler : IEventHandler<EntityCreatedEvent<Product>>
{
public Task HandleAsync(EntityCreatedEvent<Product> @event, CancellationToken cancellationToken = default)
{
// This will cause a runtime error
return Task.CompletedTask;
}
}
If you need specific handling for different entity types, use the generic handler pattern shown above, or create separate concrete event types for each entity.
Events have access to the same execution context as commands and queries:
// Access execution context in any handler
var executionContext = AmbientExecutionContext.Current;
This allows for:
- Sharing data between event handlers
- Accessing tags used when publishing the event
- Aborting event handling in specific conditions
Control the execution sequence of event handlers using the HandlerOrder
attribute:
[HandlerOrder(1)]
public class LoggingHandler : IEventHandler<PaymentReceivedEvent>
{
// Executes first
}
[HandlerOrder(2)]
public class EmailNotificationHandler : IEventHandler<PaymentReceivedEvent>
{
// Executes second
}
[HandlerOrder(3)]
public class AnalyticsHandler : IEventHandler<PaymentReceivedEvent>
{
// Executes third
}
This applies to pre-handlers and post-handlers as well:
[HandlerOrder(1)]
public class SecurityPreHandler : IEventPreHandler<UserLoggedInEvent>
{
// Executes first
}
[HandlerOrder(2)]
public class AuditPreHandler : IEventPreHandler<UserLoggedInEvent>
{
// Executes second
}
Event execution can be aborted in pre-handlers:
public class EventThrottlingHandler : IEventPreHandler<FrequentNotificationEvent>
{
private readonly IThrottleService _throttleService;
public EventThrottlingHandler(IThrottleService throttleService)
{
_throttleService = throttleService;
}
public Task PreHandleAsync(FrequentNotificationEvent @event, CancellationToken cancellationToken = default)
{
if (_throttleService.ShouldThrottle(@event.NotificationType))
{
// Abort event handling - no handlers will be executed
AmbientExecutionContext.Current.Abort();
}
return Task.CompletedTask;
}
}
When event execution is aborted, no main handlers or post-handlers are executed.
- Design events as immutable - Events represent something that has happened, so their data should not change
-
Use past tense for event names - Events represent completed actions (
UserRegistered
notRegisterUser
) - Make event handlers idempotent - The same event might be processed multiple times in distributed systems
- Keep events focused - Each event should represent a single, specific occurrence
- Include relevant context - Events should contain all data handlers need to process them
- Consider event versioning - Have a strategy for handling schema changes in long-lived events
- Use appropriate error handling - Define whether event failures should be retried or logged
- Consider async event processing - For long-running handlers, process events asynchronously
- Core Concepts
- Event Contracts
- Event Handlers
- Event Mediator/Publisher
- Advanced Features
- Best Practices
- Execution Context
- Handler Tags
- Handler Ordering
- Testing with LiteBus
- Performance Considerations
- Best Practices