DEV Community

Jaythawme
Jaythawme

Posted on

ABP vNext Local Event Bus with Logging

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
                }
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)