Introduction
The event system is a powerful tool for designing software architectures. It promotes decoupling by allowing systems to communicate through events, rather than direct method calls.
The ABP framework supports this pattern via its Local Event Bus. This allows services to publish and subscribe to in-process events, making it ideal when both publisher and subscriber are running within the same application process.
Official Example: Publish a local event when a product's stock count changes.
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Local;
namespace AbpDemo
{
public class MyService : ITransientDependency
{
private readonly ILocalEventBus _localEventBus;
public MyService(ILocalEventBus localEventBus)
{
_localEventBus = localEventBus;
}
public virtual async Task ChangeStockCountAsync(Guid productId, int newCount)
{
// TODO: Your business logic here...
// Publish the event
await _localEventBus.PublishAsync(
new StockCountChangedEvent
{
ProductId = productId,
NewCount = newCount
}
);
}
}
}
The PublishAsync
method accepts a single parameter—the event object—which holds the relevant event data. This object is typically a simple POCO (Plain Old CLR Object):
using System;
namespace AbpDemo
{
public class StockCountChangedEvent
{
public Guid ProductId { get; set; }
public int NewCount { get; set; }
}
}
Note: Even if you don't need to carry any data, you must still define an empty event class.
Problem
The example above illustrates how to use the local event bus effectively. However, in real-world business applications, this approach lacks observability. It does not log any details when events are handled. If an exception occurs during event handling, the application may silently fail without leaving any trace.
Solution
Step 1: Define a Wrapper Interface
We introduce a new interface, IAuditedLocalEventBus
, which adds logging support:
public interface IAuditedLocalEventBus
{
Task PublishAsync<TEvent>(TEvent eventData) where TEvent : class;
}
Step 2: Implement the Interface
Here’s an implementation that wraps the built-in ILocalEventBus
and adds logging:
public class AuditedLocalEventBus : IAuditedLocalEventBus, ITransientDependency
{
private readonly ILocalEventBus _localEventBus;
private readonly ILogger<AuditedLocalEventBus> _logger;
public AuditedLocalEventBus(
ILocalEventBus localEventBus,
ILogger<AuditedLocalEventBus> logger)
{
_localEventBus = localEventBus;
_logger = logger;
}
public async Task PublishAsync<TEvent>(TEvent eventData) where TEvent : class
{
_logger.LogInformation("Publishing event: {EventType}; Payload: {Payload}",
typeof(TEvent).FullName, JsonSerializer.Serialize(eventData));
await _localEventBus.PublishAsync(eventData);
_logger.LogInformation("Event published: {EventType}; Payload: {Payload}",
typeof(TEvent).FullName, JsonSerializer.Serialize(eventData));
}
}
Step 3: Define a Custom Handler Interface
To enable logging inside the event handlers themselves, define a base interface:
public interface IAuditedLocalEventHandler<TEvent> : ILocalEventHandler<TEvent>, ITransientDependency
{
async Task ILocalEventHandler<TEvent>.HandleEventAsync(TEvent eventData)
{
await AuditedLocalEventExecutor.ExecuteAsync(
this,
eventData,
async () => await HandleEventExtAsync(eventData)
);
}
Task HandleEventExtAsync(TEvent eventData);
}
Step 4: Implement the Execution Helper
Here is a utility class to encapsulate the logging logic:
public static class AuditedLocalEventExecutor
{
public static async Task ExecuteAsync<TEvent>(
object handlerInstance,
TEvent eventData,
Func<Task> action)
{
var logger = GetLogger(handlerInstance);
var handlerName = handlerInstance.GetType().Name;
var eventName = typeof(TEvent).Name;
var stopwatch = Stopwatch.StartNew();
try
{
logger?.LogInformation("Handling event [{Event}] by {Handler}", eventName, handlerName);
await action();
stopwatch.Stop();
logger?.LogInformation("Handled successfully [{Event}] by {Handler}, Time: {Elapsed}ms", eventName, handlerName, stopwatch.ElapsedMilliseconds);
}
catch (Exception ex)
{
stopwatch.Stop();
logger?.LogError(ex, "Failed to handle [{Event}] by {Handler}, Time: {Elapsed}ms", eventName, handlerName, stopwatch.ElapsedMilliseconds);
throw;
}
}
private static ILogger? GetLogger(object handler)
{
var loggerType = typeof(ILogger<>).MakeGenericType(handler.GetType());
return ServiceLocator.ServiceProvider?.GetService(loggerType) as ILogger;
}
}
Step 5: Create an Audited Handler
Now you can implement event handlers using the audited interface:
using System.Threading.Tasks;
namespace AbpDemo
{
public class MyHandler : IAuditedLocalEventHandler<StockCountChangedEvent>
{
public async Task HandleEventExtAsync(StockCountChangedEvent eventData)
{
// TODO: Your event handling logic
}
}
}
Summary
The ABP Local Event Bus provides a great in-process messaging mechanism. However, adding logging and observability is essential for maintaining robust applications. By introducing a wrapper and a logging-aware event handler base interface, we can ensure every event publish and handle operation is traceable and debuggable—making your event-driven system production-ready.
Top comments (0)