EBus is a lightweight mediator library for .NET that provides in-process messaging for commands/queries (CQRS) and notifications (events) without any external dependencies. It supports:
- Request/Response (
IRequest<TResponse>/IRequestHandler<TRequest, TResponse>) - Notifications (
INotification/INotificationHandler<TNotification>) - Pipeline Behaviors (
IPipelineBehavior<TRequest, TResponse>) - Assembly-scanning registration through a single
AddEBus(...)call
EBus resolves handlers and behaviors at runtime using generic interfaces and C# dynamic binding—no hard-coded "Handle" strings.
- Single-method
Send- Call
await mediator.Send(request) - The compiler infers
TResponsefromrequest : IRequest<TResponse>.
- Call
- Publish/Subscribe Notifications
- Call
await mediator.Publish(notification) - All
INotificationHandler<TNotification>instances run in parallel.
- Call
- Pipeline Behaviors
- Implement
IPipelineBehavior<TRequest, TResponse>, and EBus wraps behaviors around handler invocation automatically.
- Implement
- Assembly Scanning
- One-line registration:
services.AddEBus(Assembly.GetExecutingAssembly());
- Scans for all
IRequestHandler<,>,INotificationHandler<>, andIPipelineBehavior<,>.
- One-line registration:
- Lightweight & No External Dependencies
- Built purely on
Microsoft.Extensions.DependencyInjectionand .NET Core 9.
- Built purely on
Install the latest stable package from NuGet:
dotnet add package EBus --version 1.0.0Or, if referencing a locally built .nupkg:
<PackageReference Include="EBus" Version="1.0.0" />In any .NET Core / .NET 6+ application (Console, ASP.NET Core, Worker Service, etc.), register EBus by scanning one or more assemblies:
using System.Reflection;
using EBus.Registration;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
// Scan the current assembly for IRequestHandler<,>, INotificationHandler<>, IPipelineBehavior<,>
services.AddEBus(Assembly.GetExecutingAssembly());
// (Optional) Scan additional assemblies:
// services.AddEBus(
// Assembly.GetExecutingAssembly(),
// typeof(SomeExternalType).Assembly
// );
var serviceProvider = services.BuildServiceProvider();This single call does all of the following:
- Registers
IMediator → Mediator - Registers every
IRequestHandler<TRequest, TResponse>found - Registers every
INotificationHandler<TNotification>found - Registers every
IPipelineBehavior<TRequest, TResponse>found
-
Define a Request/Command/Query by implementing
IRequest<TResponse>:using EBus.Abstractions; public class CreateOrderCommand : IRequest<OrderDto> { public int CustomerId { get; set; } public decimal Total { get; set; } }
-
Implement the Handler by implementing
IRequestHandler<TRequest, TResponse>:using EBus.Abstractions; using System.Threading; using System.Threading.Tasks; public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderDto> { public Task<OrderDto> Handle( CreateOrderCommand request, CancellationToken cancellationToken) { // ...create order logic... var newOrder = new OrderDto { Id = 1234, CustomerId = request.CustomerId, Total = request.Total }; return Task.FromResult(newOrder); } }
-
Send the Request via
IMediator:var mediator = serviceProvider.GetRequiredService<IMediator>(); var dto = await mediator.Send(new CreateOrderCommand { CustomerId = 42, Total = 99.50m }); // dto is OrderDto
-
Define a Notification/Event by implementing
INotification:using EBus.Abstractions; public class OrderPlacedNotification : INotification { public int OrderId { get; set; } }
-
Implement Notification Handlers by implementing
INotificationHandler<TNotification>:using EBus.Abstractions; using System.Threading; using System.Threading.Tasks; public class SendOrderConfirmationHandler : INotificationHandler<OrderPlacedNotification> { public Task Handle( OrderPlacedNotification notification, CancellationToken cancellationToken) { Console.WriteLine($"Sending confirmation for Order {notification.OrderId}"); return Task.CompletedTask; } } public class UpdateInventoryHandler : INotificationHandler<OrderPlacedNotification> { public Task Handle( OrderPlacedNotification notification, CancellationToken cancellationToken) { Console.WriteLine($"Updating inventory for Order {notification.OrderId}"); return Task.CompletedTask; } }
-
Publish the Notification:
await mediator.Publish(new OrderPlacedNotification { OrderId = dto.Id });
All registered INotificationHandler<OrderPlacedNotification> instances run in parallel (Task.WhenAll).
You can insert cross-cutting logic (e.g., logging, validation) by implementing IPipelineBehavior<TRequest, TResponse>:
using EBus.Abstractions;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
public class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request,
CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
_logger.LogInformation("Handling {RequestType}", typeof(TRequest).Name);
var response = await next().ConfigureAwait(false);
_logger.LogInformation("Handled {RequestType}", typeof(TRequest).Name);
return response;
}
}- Registration:
If you place this class in any scanned assembly,AddEBus(...)will automatically register it as
IPipelineBehavior<TRequest, TResponse>for allTRequest/TResponse. - Execution Order:
Behaviors are invoked in registration order, wrapping around the final handler. Each behavior receives a delegate (next) to call the next behavior or handler.
using EBus.Abstractions;
using EBus.Registration;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using System.Threading.Tasks;
public class ProductDto { /* ... */ }
public class GetProductQuery : IRequest<ProductDto>
{
public int ProductId { get; set; }
}
public class GetProductHandler : IRequestHandler<GetProductQuery, ProductDto>
{
public Task<ProductDto> Handle(GetProductQuery request, CancellationToken cancellationToken)
{
// Load product from database or repository...
var product = new ProductDto { /* fill properties */ };
return Task.FromResult(product);
}
}
public class Program
{
public static async Task Main(string[] args)
{
var services = new ServiceCollection();
services.AddEBus(Assembly.GetExecutingAssembly());
var provider = services.BuildServiceProvider();
var mediator = provider.GetRequiredService<IMediator>();
var query = new GetProductQuery { ProductId = 100 };
ProductDto product = await mediator.Send(query);
Console.WriteLine($"Retrieved product with ID: {product?.Id}");
}
}using EBus.Abstractions;
using EBus.Registration;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using System.Threading.Tasks;
public class OrderPlacedNotification : INotification
{
public int OrderId { get; set; }
}
public class SendEmailHandler : INotificationHandler<OrderPlacedNotification>
{
public Task Handle(OrderPlacedNotification notification, CancellationToken cancellationToken)
{
Console.WriteLine($"Sending email for Order {notification.OrderId}");
return Task.CompletedTask;
}
}
public class UpdateAnalyticsHandler : INotificationHandler<OrderPlacedNotification>
{
public Task Handle(OrderPlacedNotification notification, CancellationToken cancellationToken)
{
Console.WriteLine($"Updating analytics for Order {notification.OrderId}");
return Task.CompletedTask;
}
}
public class Program
{
public static async Task Main(string[] args)
{
var services = new ServiceCollection();
services.AddEBus(Assembly.GetExecutingAssembly());
var provider = services.BuildServiceProvider();
var mediator = provider.GetRequiredService<IMediator>();
await mediator.Publish(new OrderPlacedNotification { OrderId = 200 });
// Both SendEmailHandler and UpdateAnalyticsHandler run in parallel.
}
}If your handlers are spread across several class libraries, pass multiple assemblies:
using EBus.Registration;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
var services = new ServiceCollection();
// Scan current assembly + external assemblies
services.AddEBus(
Assembly.GetExecutingAssembly(),
typeof(SomeExternalHandler).Assembly,
typeof(AnotherModuleMarker).Assembly
);
var provider = services.BuildServiceProvider();
var mediator = provider.GetRequiredService<IMediator>();Contributions are welcome! Please follow these guidelines:
- Fork the Repository
- Create a Feature Branch (
git checkout -b feature/YourFeatureName) - Commit Your Changes (
git commit -m "Add some feature") - Push to Your Fork (
git push origin feature/YourFeatureName) - Open a Pull Request against the
mainbranch
Include:
- A clear description of your change.
- Any relevant examples or tests.
- Updates to this README.md if needed.
EBus is licensed under the MIT License. Feel free to use, modify, and distribute under the terms of MIT.