1. Introduction
EventBus is a powerful tool for decoupling communication between modules. With CodeWF.EventBus, we can easily implement the CQRS pattern and subscribe to/publish events through a clear and concise interface. Next, we will discuss in detail how to use this library to handle events.
CQRS, short for Command Query Responsibility Segregation, is a software architecture pattern that separates the responsibilities of commands (write operations) and queries (read operations) in a system to improve performance, scalability, and responsiveness.
CodeWF.EventBus is suitable for in-process event delivery (no external dependencies) and has similar functionality to MediatR. MediatR is designed primarily for ASP.NET Core and is more feature-rich. The advantages of CodeWF.EventBus are:
- Small and flexible, designed for use in various template projects such as WPF, WinForms, Avalonia UI, ASP.NET Core, etc.
- Supports projects using any
IOCcontainer. - References MASA Framework to enhance event handling capabilities, allowing a class to define multiple event handling methods:
2. Usage Guide
2.1. Registering the EventBus
2.1.1. MS.DI Container
Mainly for ASP.NET Core applications, such as MVC, Razor Pages, Blazor Server templates. Search for the NuGet package CodeWF.AspNetCore.EventBus and install the latest version. After installation, add the following code in Program:
// ....
// 1. Register EventBus: inject classes marked with the `EventHandler` attribute as singletons into the IOC container
builder.Services.AddEventBus();
var app = builder.Build();
// ...
// 2. Retrieve classes already injected into the IOC container and associate handler methods with the EventBus
app.UseEventBus();
// ...
- The
AddEventBusmethod scans the provided assembly list and injects classes that have methods marked with theEventHandlerattribute inside a class marked with theEventattribute as singletons into the IOC container. - The
UseEventBusmethod retrieves instances of classes injected in the previous step via the IOC container and registers their event handler methods in the event management queue. When an event is published, the event handler methods are looked up from the queue and invoked to achieve event notification.
2.1.2. DryIOC Container
If using the DryIoc container, for example in WPF/Avalonia UI projects using the DryIoc container of the Prism framework, search for the NuGet package CodeWF.DryIoc.EventBus and install the latest version. After installation, add the following code in the RegisterTypes method:
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
IContainer? container = containerRegistry.GetContainer();
// ...
// Register EventBus
containerRegistry.AddEventBus();
// ...
// Use EventBus
container.UseEventBus();
}
2.1.3. Any IOC Container
For projects using other IOC containers, search for the NuGet package CodeWF.IOC.EventBus and install the latest version. After installation, modify the registration code according to the IOC container's specific APIs for singleton registration and service retrieval.
The above ASP.NET Core example for registering the EventBus can be changed to:
// ....
// 1. Register EventBus: inject classes marked with the `EventHandler` attribute as singletons into the IOC container
EventBusExtensions.AddEventBus(
(t1, t2) => builder.Services.AddSingleton(t1, t2),
t => builder.Services.AddSingleton(t),
Assembly.GetExecutingAssembly());
var app = builder.Build();
// ...
// 2. Retrieve classes already injected into the IOC container and associate handler methods with the EventBus
EventBusExtensions.UseEventBus(t => app.Services.GetRequiredService(t), Assembly.GetExecutingAssembly());
// ...
The principle of supporting any IOC container lies in the AddEventBus and UseEventBus methods:
using CodeWF.EventBus;
using System.Reflection;
namespace CodeWF.IOC.EventBus
{
public static class EventBusExtensions
{
public static void AddEventBus(Action<Type, Type> addSingleton1,
Action<Type> addSingleton2, params Assembly[] assemblies)
{
addSingleton1(typeof(IEventBus), typeof(CodeWF.EventBus.EventBus));
var allAssemblies = assemblies.Concat(new[] { Assembly.GetCallingAssembly() }).ToArray();
CodeWF.EventBus.EventBusExtensions.HandleEventObject(type => addSingleton2(type),
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
allAssemblies);
}
public static void UseEventBus(Func<Type, object> resolveAction, params Assembly[] assemblies)
{
if (!(resolveAction(typeof(IEventBus)) is IEventBus messenger))
{
throw new InvalidOperationException("Please call AddEventBus before calling UseEventBus");
}
var allAssemblies = assemblies.Concat(new[] { Assembly.GetCallingAssembly() }).ToArray();
CodeWF.EventBus.EventBusExtensions.HandleEventObject(
type => messenger.Subscribe(resolveAction(type)),
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, allAssemblies);
CodeWF.EventBus.EventBusExtensions.HandleEventObject(type => messenger.Subscribe(type),
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, allAssemblies);
}
}
}
using System;
using System.Linq;
using System.Reflection;
namespace CodeWF.EventBus
{
public static class EventBusExtensions
{
public static void HandleEventObject(Action<Type> handleRecipient, BindingFlags findHandlerMethodBindingFlags,
Assembly[] assemblies)
{
foreach (var assembly in assemblies)
{
var types = assembly.GetTypes()
.Where(t => t.IsClass
&& !t.IsAbstract
&& t.GetCustomAttributes<EventAttribute>().Any()
&& t.GetMethods(findHandlerMethodBindingFlags)
.Any(m =>
m.GetCustomAttributes<EventHandlerAttribute>().Any()));
foreach (var type in types)
{
handleRecipient(type);
}
}
}
}
}
2.1.4. No IOC Container Used
Default WPF, WinForms, Avalonia UI, and console applications do not include any IOC container. For such projects, we don't need the EventBus service registration steps.
Search for NuGet package CodeWF.EventBus and install the latest version. After installation, functionality is the same as using an IOC container, except for the lack of auto-subscription via IOC injection. Please continue reading for the specific differences.
2.2. Defining Events
Here, we use CQRS to structure our application's business logic. In the CQRS pattern, queries are separated from other business operations. If you are unfamiliar with CQRS, you can refer to this article: https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs
2.2.1. Defining Commands
In the CQRS pattern, commands represent write operations. Define command classes that inherit from the Command class:
public class CreateProductCommand : Command
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class CreateProductSuccessCommand : Command
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class DeleteProductCommand : Command
{
public Guid ProductId { get; set; }
}
2.2.2. Defining Queries
In the CQRS pattern, queries represent read operations. Queries expect a response and are suitable for request/response scenarios. With queries, the caller only needs to know about using ProductQuery, ProductsQuery, without worrying about needing IProductService, ICategoryService etc. to obtain query results.
Define query classes that inherit from Query<T>:
public class ProductQuery : Query<ProductItemDto>
{
public Guid ProductId { get; set; }
public override ProductItemDto Result { get; set; }
}
public class ProductsQuery : Query<List<ProductItemDto>>
{
public string Name { get; set; }
public override List<ProductItemDto> Result { get; set; }
}
Query<T>: T represents the type of the query response. Use the Result property in XXXQuery to hold the result after the query is published.
Query inherits from Command and includes a Result property:
public abstract class Query<TResult> : Command
{
public abstract TResult Result { get; set; }
}
2.3. Subscribing to Events (Subscribe)
2.3.1. Automatic Subscription
Automatic subscription can only be used in programs that employ an IOC container, such as ASP.NET Core applications.
Typically, event handlers are encapsulated into a separate class, as shown below:
[Event]
public class CommandAndQueryHandler(IEventBus eventBus, IProductService productService)
{
[EventHandler]
private async Task ReceiveCreateProductCommandAsync(CreateProductCommand command)
{
var isAddSuccess = await productService.AddProductAsync(new CreateProductRequest()
{ Name = command.Name, Price = command.Price });
if (isAddSuccess)
{
await eventBus.PublishAsync(new CreateProductSuccessCommand()
{ Name = command.Name, Price = command.Price });
}
else
{
Console.WriteLine("Create product fail");
}
}
[EventHandler(Order = 2)]
private async Task ReceiveCreateProductSuccessCommandSendEmailAsync(CreateProductSuccessCommand command)
{
Console.WriteLine($"Now send email notify create product success, name is = {command.Name}");
await Task.CompletedTask;
}
[EventHandler(Order = 1)]
private async Task ReceiveCreateProductSuccessCommandSendSmsAsync(CreateProductSuccessCommand command)
{
Console.WriteLine($"Now send sms notify create product success, name is = {command.Name}");
await Task.CompletedTask;
}
[EventHandler(Order = 3)]
private void ReceiveCreateProductSuccessCommandCallPhone(CreateProductSuccessCommand command)
{
Console.WriteLine($"Now call phone notify create product success, name is = {command.Name}");
}
[EventHandler]
private async Task ReceiveDeleteProductCommandAsync(DeleteProductCommand command)
{
var isRemoveSuccess = await productService.RemoveProductAsync(command.ProductId);
Console.WriteLine(isRemoveSuccess ? "Remote product success" : "Remote product fail");
}
[EventHandler]
private async Task ReceiveProductQueryAsync(ProductQuery query)
{
var product = await productService.QueryProductAsync(query.ProductId);
query.Result = product;
}
[EventHandler]
private async Task ReceiveAutoProductsQueryAsync(ProductsQuery query)
{
var products = await productService.QueryProductsAsync(query.Name);
query.Result = products;
}
[EventHandler]
private static async Task ReceiveAutoProductsQueryAsync2(ProductsQuery query)
{
Console.WriteLine("Test auto subscribe static method");
}
}
- The class
CommandAndQueryHandleris marked with theEventattribute, indicating that it can be injected as a singleton into theIOCcontainer. - Methods marked with the
EventHandlerattribute have the ability to handle events. Such a method can only have one event-type parameter; if the method is asynchronous, it must returnTask(no generic Task is allowed; if added, it will be ignored). Static event handling methods are also supported.
Programs using an IOC container will automatically inject classes marked with the Event attribute as singletons. When the EventBus receives an event notification, it automatically finds methods marked with the EventHandler attribute and invokes them to achieve event notification.
2.3.2. Manual Subscription
For classes not marked with the Event attribute, event handlers can be registered manually. Below is an example of manual registration when not using an IOC container (the core is EventBus.Default):
internal class CommandAndQueryHandler
{
internal void ManuSubscribe()
{
EventBus.Default.Subscribe<DeleteProductCommand>(ReceiveDeleteProductCommandAsync);
EventBus.Default.Subscribe<ProductQuery>(ReceiveProductQueryAsync);
EventBus.Default.Subscribe<ProductsQuery>(ReceiveAutoProductsQueryAsync2);
}
public async Task ReceiveDeleteProductCommandAsync(DeleteProductCommand command)
{
}
public async Task ReceiveProductQueryAsync(ProductQuery query)
{
}
private static async Task ReceiveAutoProductsQueryAsync2(ProductsQuery query)
{
}
}
The above step-by-step registration can be tedious. It can be simplified:
internal class CommandAndQueryHandler
{
internal CommandAndQueryHandler()
{
EventBus.Default.Subscribe(this);
}
[EventHandler(Order = 2)]
public async Task ReceiveCreateProductSuccessCommandSendEmailAsync(CreateProductSuccessCommand command)
{
}
// ... Omitted N event handling methods; EventBus.Default.Subscribe(this) can auto-bind them
}
When using an IOC container, you can inject the IEventBus service to replace EventBus.Default, as shown in the example code below:
public class EventBusTestViewModel : ViewModelBase
{
private readonly IEventBus _eventBus;
public MessageTestViewModel(IEventBus eventBus)
{
_eventBus = eventBus;
_eventBus.Subscribe(this);
}
[EventHandler]
public async Task ReceiveDeleteProductCommandAsync(DeleteProductCommand command)
{
var isRemoveSuccess = await productService.RemoveProductAsync(command.ProductId);
Console.WriteLine(isRemoveSuccess ? "Remote product success" : "Remote product fail");
}
}
EventBus is the default implementation of the IEventBus interface, and EventBus.Default is a singleton reference, so either can be used interchangeably. When injected via IOC, both IEventBus and EventBus are registered as singletons, making them equivalent.
Manual subscription can be used in WPF's XxxViewModel (as shown above) or in services with other lifetimes within the IOC container:
public class TimeService : ITimeService
{
private readonly IEventBus _eventBus;
public TimeService(IEventBus eventBus)
{
_eventBus = eventBus;
_eventBus.Subscribe(this);
}
[EventHandler]
public async Task ReceiveDeleteProductCommandAsync(DeleteProductCommand command)
{
var isRemoveSuccess = await productService.RemoveProductAsync(command.ProductId);
Console.WriteLine(isRemoveSuccess ? "Remote product success" : "Remote product fail");
}
}
Manual registration can be used in situations where singleton injection is not possible or necessary, as a supplement.
2.4. Publishing Events
Publishing commands and queries uses the same interface. Use the Publish and PublishAsync methods of IEventBus or EventBus.Default:
_messenger.Publish(this, new DeleteProductCommand { ProductId = id });
var query = new ProductQuery { ProductId = id };
await _messenger.PublishAsync(this, query);
Console.WriteLine($"Query product with ID {id} result: {query.Result}");
Using publish in an B/S controller Action:
[ApiController]
[Route("[controller]")]
public class EventController : ControllerBase
{
private readonly ILogger<EventController> _logger;
private readonly IEventBus _eventBus;
public EventController(ILogger<EventController> logger, IEventBus eventBus)
{
_logger = logger;
_eventBus = eventBus;
}
[HttpPost("/add")]
public async Task AddAsync([FromBody] CreateProductRequest request)
{
await _eventBus.PublishAsync(new CreateProductCommand { Name = request.Name, Price = request.Price });
}
[HttpDelete("/delete")]
public async Task DeleteAsync([FromQuery] Guid id)
{
await _eventBus.PublishAsync(new DeleteProductCommand { ProductId = id });
}
[HttpGet("/get")]
public async Task<ProductItemDto> GetAsync([FromQuery] Guid id)
{
var query = new ProductQuery { ProductId = id };
await _eventBus.PublishAsync(query);
return query.Result;
}
[HttpGet("/list")]
public async Task<List<ProductItemDto>> ListAsync([FromQuery] string? name)
{
var query = new ProductsQuery { Name = name };
await _eventBus.PublishAsync(query);
return query.Result;
}
}
Using in WPF/Avalonia UI's XXXViewModel:
public class EventBusTestViewModel : ViewModelBase
{
private readonly IEventBus _eventBus;
public MessageTestViewModel(IEventBus eventBus)
{
_eventBus = eventBus;
}
public async Task ExecuteEventBusAsync()
{
await _eventBus.PublishAsync(this, new TestMessage(nameof(MessageTestViewModel), TestClass.CurrentTime()));
}
}
2.5. Unsubscribing from Events
In practice, you may need to unsubscribe at the appropriate time (e.g., when a service is disposed) to avoid memory leaks:
- Unregister a specific handler:
Messenger.Default.Unsubscribe<CreateProductMessage>(this, ReceiveManuCreateProductMessage) - Unregister all handlers of a specific class:
Messenger.Default.Unsubscribe(this)
3. Core Interface Description
public interface IEventBus
{
void Subscribe<T>() where T : class;
void Subscribe(Type type);
void Subscribe(object recipient);
void Subscribe<TCommand>(Action<TCommand> action) where TCommand : Command;
void Subscribe<TCommand>(Func<TCommand, Task> asyncAction) where TCommand : Command;
void Unsubscribe<T>() where T : class;
void Unsubscribe(object recipient);
void Unsubscribe<TCommand>(Action<TCommand> action) where TCommand : Command;
void Unsubscribe<TCommand>(Func<TCommand, Task> asyncAction) where TCommand : Command;
void Publish<TCommand>(TCommand command) where TCommand : Command;
Task PublishAsync<TCommand>(TCommand command) where TCommand : Command;
}
Subscribe<T>(): Subscribes to static event handling methods in a class.Subscribe(Type type): Subscribes to static event handling methods in the specified class type.Subscribe(object recipient): Subscribes to instance member event handling methods of the specified recipient.Subscribe<TCommand>(Action<TCommand> action): Subscribes to regular event handling methods, including static event handling methods.Subscribe<TCommand>(Func<TCommand, Task> asyncAction): Subscribes to asynchronous event handling methods, including static asynchronous event handling methods.Unsubscribe<T>(): Unsubscribes static event handling methods in a class.Unsubscribe(object recipient): Unsubscribes instance member event handling methods of the specified recipient.Unsubscribe<TCommand>(Action<TCommand> action): Unsubscribes regular event handling methods, including static event handling methods.Unsubscribe<TCommand>(Func<TCommand, Task> asyncAction): Unsubscribes asynchronous event handling methods, including static asynchronous event handling methods.Publish<TCommand>(TCommand command): Publishes a command or query synchronously.PublishAsync<TCommand>(TCommand command): Publishes a command or query asynchronously.
4. Summary
CodeWF.EventBus provides a compact and flexible EventBus implementation that supports the CQRS pattern and is suitable for various project templates such as Avalonia UI, WPF, WinForms, ASP.NET Core, etc. With simple subscribe and publish operations, you can easily achieve decoupling and communication between modules. Ordered event handling ensures that events are processed properly.
For the specific implementation of the EventBus, please refer to the CodeWF.EventBus source code: https://github.com/dotnet9/CodeWF.EventBus . For usage examples, see:
- Unit tests: CodeWF.EventBus.Tests
- AvaloniaUI + Prism: CodeWF.EventBus
- Web API: WebAPIDemo , CodeWF
Open-source projects referenced during development:
We hope this guide helps you better use CodeWF.EventBus to handle events in your application.