Skip to content

Event Module

A. Shafie edited this page May 9, 2025 · 11 revisions

Event Module

Core Concepts

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

Event Contracts

LiteBus provides two approaches for defining events:

IEvent Interface

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

POCO Events

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

Main Event Handlers

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

Pre-handlers execute before all main event handlers, useful for logging, enrichment, or validation:

Type-specific pre-handler:

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

Global pre-handler:

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

Post-handlers execute after all main event handlers have completed:

Type-specific post-handler:

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

Global post-handler:

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

Error handlers catch and process exceptions thrown during event handling:

Type-specific error handler:

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

Global error handler:

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

Event Mediator/Publisher

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.

Usage Examples

// 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");

Advanced Features

Event Mediation Settings

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).

Handler Filtering

LiteBus allows you to filter which handlers are executed for an event:

Tag-Based Filtering

[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"] }
});

Predicate-Based Filtering

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

Generic Events

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.

Execution Context

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

Handler Ordering

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
}

Aborting Event Execution

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.

Best Practices

  1. Design events as immutable - Events represent something that has happened, so their data should not change
  2. Use past tense for event names - Events represent completed actions (UserRegistered not RegisterUser)
  3. Make event handlers idempotent - The same event might be processed multiple times in distributed systems
  4. Keep events focused - Each event should represent a single, specific occurrence
  5. Include relevant context - Events should contain all data handlers need to process them
  6. Consider event versioning - Have a strategy for handling schema changes in long-lived events
  7. Use appropriate error handling - Define whether event failures should be retried or logged
  8. Consider async event processing - For long-running handlers, process events asynchronously
Clone this wiki locally