Softalleys.Utilities.Events
1.1.1
dotnet add package Softalleys.Utilities.Events --version 1.1.1
NuGet\Install-Package Softalleys.Utilities.Events -Version 1.1.1
<PackageReference Include="Softalleys.Utilities.Events" Version="1.1.1" />
<PackageVersion Include="Softalleys.Utilities.Events" Version="1.1.1" />
<PackageReference Include="Softalleys.Utilities.Events" />
paket add Softalleys.Utilities.Events --version 1.1.1
#r "nuget: Softalleys.Utilities.Events, 1.1.1"
#:package Softalleys.Utilities.Events@1.1.1
#addin nuget:?package=Softalleys.Utilities.Events&version=1.1.1
#tool nuget:?package=Softalleys.Utilities.Events&version=1.1.1
Softalleys.Utilities.Events
A lightweight, flexible event-driven architecture library for .NET applications. This library provides a robust event handling system with proper dependency injection scope management, pre/post processing pipelines, and support for both scoped and singleton handler lifecycles.
โจ Features
- ๐ฏ Simple Event System: Clean, intuitive interfaces for events and handlers
- ๐ Flexible Handler Lifecycles: Support for both scoped and singleton event handlers
- โก Pre/Post Processing: Built-in pipeline with pre-processing and post-processing phases
- ๐งต Hosted Handlers: Background/hosted services that also receive events
- ๐๏ธ Proper DI Integration: Respects dependency injection scopes and lifecycles
- ๐ฆ Auto-Discovery: Automatic scanning and registration of event handlers from assemblies
- ๐ High Performance: Minimal overhead with concurrent handler execution
- ๐ก๏ธ Error Resilience: Individual handler failures don't stop other handlers from executing
- ๐ Comprehensive Logging: Built-in logging support for monitoring and debugging
- ๐จ Zero Dependencies: Only depends on Microsoft.Extensions abstractions
๐ Quick Start
Installation
dotnet add package Softalleys.Utilities.Events
1. Define Your Events
using Softalleys.Utilities.Events;
public class UserRegisteredEvent : IEvent
{
public string UserId { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public DateTime RegisteredAt { get; set; }
}
public class OrderCreatedEvent : IEvent
{
public string OrderId { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string CustomerId { get; set; } = string.Empty;
}
2. Create Event Handlers
// Scoped handler - has access to current request scope (DbContext, etc.)
public class UserRegisteredEmailHandler(IEmailService emailService, IDbContext dbContext) : IEventHandler<UserRegisteredEvent>
{
public async Task HandleAsync(UserRegisteredEvent eventData, CancellationToken cancellationToken = default)
{
await emailService.SendWelcomeEmailAsync(eventData.Email, cancellationToken);
// Can access scoped services like DbContext
var user = await dbContext.Users.FindAsync(eventData.UserId);
if (user != null)
{
user.EmailSent = true;
await dbContext.SaveChangesAsync(cancellationToken);
}
}
}
// Singleton handler - for stateless operations like logging
public class UserRegisteredLoggerHandler(ILogger<UserRegisteredLoggerHandler> logger) : IEventSingletonHandler<UserRegisteredEvent>
{
public async Task HandleAsync(UserRegisteredEvent eventData, CancellationToken cancellationToken = default)
{
logger.LogInformation("User {UserId} registered at {RegisteredAt}",
eventData.UserId, eventData.RegisteredAt);
await Task.CompletedTask;
}
}
// Pre-processing handler - runs before main handlers
public class UserValidationPreHandler(IUserValidationService validationService) : IEventPreHandler<UserRegisteredEvent>
{
public async Task HandleAsync(UserRegisteredEvent eventData, CancellationToken cancellationToken = default)
{
await validationService.ValidateUserAsync(eventData.UserId, cancellationToken);
}
}
// Post-processing handler - runs after main handlers
public class UserAnalyticsPostHandler(IAnalyticsService analyticsService) : IEventPostSingletonHandler<UserRegisteredEvent>
{
public async Task HandleAsync(UserRegisteredEvent eventData, CancellationToken cancellationToken = default)
{
await analyticsService.TrackUserRegistrationAsync(eventData.UserId, cancellationToken);
}
}
3. Register Services
using Softalleys.Utilities.Events;
// In your Program.cs or Startup.cs
builder.Services.AddSoftalleysEvents(); // Scans current assembly
// Or specify multiple assemblies
builder.Services.AddSoftalleysEvents(
typeof(UserRegisteredEvent).Assembly,
typeof(OrderCreatedEvent).Assembly
);
4. Publish Events
public class UserController(IEventBus eventBus) : ControllerBase
{
[HttpPost("register")]
public async Task<IActionResult> RegisterUser([FromBody] RegisterUserRequest request)
{
// Create user logic here...
var userId = await CreateUserAsync(request);
// Publish the event
var userRegisteredEvent = new UserRegisteredEvent
{
UserId = userId,
Email = request.Email,
RegisteredAt = DateTime.UtcNow
};
await eventBus.PublishAsync(userRegisteredEvent);
return Ok(new { UserId = userId });
}
}
๐ญ Handler Types and Execution Order
The library supports seven different types of event handlers, executed in a specific order:
Handler Types
Handler Type | Lifetime | Purpose | When to Use |
---|---|---|---|
IEventPreSingletonHandler<T> |
Singleton | Pre-processing | Stateless validation, logging setup |
IEventPreHandler<T> |
Scoped | Pre-processing | Database validation, scoped preparations |
IEventSingletonHandler<T> |
Singleton | Main processing | Stateless operations, caching, logging |
IEventHandler<T> |
Scoped | Main processing | Database operations, scoped business logic |
IEventHostedService<T> |
Singleton + Hosted | Background kick-off | Start background tasks based on the event |
IEventPostSingletonHandler<T> |
Singleton | Post-processing | Analytics, cleanup, stateless notifications |
IEventPostHandler<T> |
Scoped | Post-processing | Final database updates, scoped cleanup |
Execution Order
When you publish an event, handlers are executed in this order:
- Pre-processing Singleton Handlers -
IEventPreSingletonHandler<T>
- Pre-processing Scoped Handlers -
IEventPreHandler<T>
- Main Singleton Handlers -
IEventSingletonHandler<T>
- Main Scoped Handlers -
IEventHandler<T>
- Hosted Handlers -
IEventHostedService<T>
(singletons that are alsoIHostedService
) - Post-processing Singleton Handlers -
IEventPostSingletonHandler<T>
- Post-processing Scoped Handlers -
IEventPostHandler<T>
Within each phase, handlers execute concurrently for better performance.
๐ง Advanced Usage
Multiple Handlers for Same Event
// Multiple handlers can handle the same event
public class EmailNotificationHandler : IEventHandler<UserRegisteredEvent>
{
public async Task HandleAsync(UserRegisteredEvent eventData, CancellationToken cancellationToken = default)
{
// Send email
}
}
public class SmsNotificationHandler : IEventHandler<UserRegisteredEvent>
{
public async Task HandleAsync(UserRegisteredEvent eventData, CancellationToken cancellationToken = default)
{
// Send SMS
}
}
public class SlackNotificationHandler : IEventSingletonHandler<UserRegisteredEvent>
{
public async Task HandleAsync(UserRegisteredEvent eventData, CancellationToken cancellationToken = default)
{
// Send to Slack
}
}
Error Handling
try
{
await _eventBus.PublishAsync(myEvent);
}
catch (AggregateException ex)
{
// Handle multiple handler failures
foreach (var innerException in ex.InnerExceptions)
{
_logger.LogError(innerException, "Handler failed");
}
}
Hosted/Background Handlers
IEventHostedService<T>
lets you implement a background service that also receives events. The DI registration ensures a single singleton instance is used for both IEventHostedService<T>
and IHostedService
, so the Host manages lifecycle while the EventBus can invoke HandleAsync
.
using Microsoft.Extensions.Hosting;
using Softalleys.Utilities.Events;
public class OrderBackgroundProcessor : IEventHostedService<OrderProcessingEvent>
{
private readonly Channel<OrderProcessingEvent> _channel = Channel.CreateUnbounded<OrderProcessingEvent>();
private CancellationTokenSource? _cts;
private Task? _worker;
public Task StartAsync(CancellationToken cancellationToken)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_worker = Task.Run(ProcessLoopAsync);
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_channel.Writer.TryComplete();
if (_worker is not null)
{
await _worker.WaitAsync(cancellationToken);
}
_cts?.Cancel();
}
public ValueTask HandleAsync(OrderProcessingEvent eventData, CancellationToken cancellationToken = default)
=> _channel.Writer.TryWrite(eventData) ? ValueTask.CompletedTask : ValueTask.FromCanceled(cancellationToken);
private async Task ProcessLoopAsync()
{
while (await _channel.Reader.WaitToReadAsync(_cts!.Token))
{
while (_channel.Reader.TryRead(out var evt))
{
// do background processing for evt
}
}
}
}
Notes:
- Hosted handlers are singletons; implementations must be thread-safe.
- The Host calls
StartAsync/StopAsync
; EventBus callsHandleAsync(evt)
in the Hosted phase. - Registration is automatic via
AddSoftalleysEvents()
scanning.
Wiring in Program.cs / Startup.cs
// Find hosted handlers in the current and additional assemblies
builder.Services.AddSoftalleysEvents(typeof(OrderBackgroundProcessor).Assembly);
// No need to register IHostedService explicitly; the library links it to the same singleton
Publishing an event
public class OrdersController(IEventBus eventBus)
{
public async Task<IActionResult> CreateOrder(CreateOrderRequest req)
{
// ... create order domain object
await eventBus.PublishAsync(new OrderProcessingEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId
});
return Accepted();
}
}
Complex Event Scenarios
public class OrderProcessingEvent : IEvent
{
public string OrderId { get; set; } = string.Empty;
public List<string> ProductIds { get; set; } = new();
public decimal TotalAmount { get; set; }
public string CustomerId { get; set; } = string.Empty;
}
// Pre-processing: Validate inventory
public class InventoryValidationPreHandler : IEventPreHandler<OrderProcessingEvent>
{
private readonly IInventoryService _inventoryService;
public InventoryValidationPreHandler(IInventoryService inventoryService)
{
_inventoryService = inventoryService;
}
public async Task HandleAsync(OrderProcessingEvent eventData, CancellationToken cancellationToken = default)
{
foreach (var productId in eventData.ProductIds)
{
var available = await _inventoryService.CheckAvailabilityAsync(productId, cancellationToken);
if (!available)
{
throw new InvalidOperationException($"Product {productId} is out of stock");
}
}
}
}
// Main processing: Create order
public class CreateOrderHandler : IEventHandler<OrderProcessingEvent>
{
private readonly IOrderRepository _orderRepository;
private readonly IDbContext _dbContext;
public CreateOrderHandler(IOrderRepository orderRepository, IDbContext dbContext)
{
_orderRepository = orderRepository;
_dbContext = dbContext;
}
public async Task HandleAsync(OrderProcessingEvent eventData, CancellationToken cancellationToken = default)
{
var order = new Order
{
Id = eventData.OrderId,
CustomerId = eventData.CustomerId,
TotalAmount = eventData.TotalAmount,
CreatedAt = DateTime.UtcNow
};
await _orderRepository.AddAsync(order, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
}
}
// Post-processing: Send notifications and update analytics
public class OrderAnalyticsPostHandler : IEventPostSingletonHandler<OrderProcessingEvent>
{
private readonly IAnalyticsService _analyticsService;
public OrderAnalyticsPostHandler(IAnalyticsService analyticsService)
{
_analyticsService = analyticsService;
}
public async Task HandleAsync(OrderProcessingEvent eventData, CancellationToken cancellationToken = default)
{
await _analyticsService.TrackOrderAsync(eventData.OrderId, eventData.TotalAmount, cancellationToken);
}
}
๐๏ธ Architecture Benefits
Why Choose This Over MediatR or LiteBus?
Feature | Softalleys.Events | MediatR | LiteBus |
---|---|---|---|
Cost | โ Free | โ Requires License | โ Free |
DI Scope Support | โ Full Support | โ Full Support | โ Transient Only |
Handler Lifecycles | โ Scoped + Singleton | โ Configurable | โ Transient Only |
Pre/Post Processing | โ Built-in | โ Via Behaviors | โ Manual |
Performance | โ High | โ High | โ High |
Learning Curve | โ Simple | โ Complex | โ Simple |
Event-Driven Architecture Benefits
- ๐ Loose Coupling: Components don't need to know about each other directly
- ๐ Scalability: Easy to add new handlers without modifying existing code
- ๐งช Testability: Each handler can be tested independently
- ๐ง Maintainability: Clear separation of concerns
- ๐ Extensibility: Simple to add new features via new handlers
โก Performance Considerations
- Handlers within the same phase execute concurrently for better throughput
- Singleton handlers are cached and reused, reducing allocation overhead
- Minimal reflection usage with caching for type discovery
- Efficient exception handling that doesn't stop other handlers
๐ Best Practices
1. Choose the Right Handler Type
// โ
Use scoped handlers for database operations
public class SaveUserHandler : IEventHandler<UserCreatedEvent>
{
private readonly IDbContext _context;
// ...
}
// โ
Use singleton handlers for stateless operations
public class LogUserCreationHandler : IEventSingletonHandler<UserCreatedEvent>
{
private readonly ILogger _logger;
// ...
}
2. Keep Events Immutable
// โ
Good - immutable event
public class UserCreatedEvent : IEvent
{
public string UserId { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
public DateTime CreatedAt { get; init; }
}
// โ Avoid - mutable events can cause issues
public class UserCreatedEvent : IEvent
{
public string UserId { get; set; } = string.Empty;
public List<string> Roles { get; set; } = new(); // Handlers might modify this
}
3. Handle Failures Gracefully
public class EmailHandler : IEventHandler<UserRegisteredEvent>
{
public async Task HandleAsync(UserRegisteredEvent eventData, CancellationToken cancellationToken = default)
{
try
{
await _emailService.SendAsync(eventData.Email);
}
catch (EmailException ex)
{
// Log the error but don't throw - other handlers should still run
_logger.LogError(ex, "Failed to send email to {Email}", eventData.Email);
// Optionally, publish a compensation event
await _eventBus.PublishAsync(new EmailFailedEvent
{
UserId = eventData.UserId,
Reason = ex.Message
});
}
}
}
4. Use Meaningful Event Names
// โ
Clear, domain-focused names
public class UserRegisteredEvent : IEvent { }
public class OrderShippedEvent : IEvent { }
public class PaymentProcessedEvent : IEvent { }
// โ Technical or vague names
public class UserEvent : IEvent { }
public class DataChangedEvent : IEvent { }
public class SomethingHappenedEvent : IEvent { }
๐งช Testing
Unit Testing Handlers
[Test]
public async Task UserRegisteredEmailHandler_ShouldSendWelcomeEmail()
{
// Arrange
var mockEmailService = new Mock<IEmailService>();
var mockDbContext = new Mock<IDbContext>();
var handler = new UserRegisteredEmailHandler(mockEmailService.Object, mockDbContext.Object);
var eventData = new UserRegisteredEvent
{
UserId = "user123",
Email = "test@example.com",
RegisteredAt = DateTime.UtcNow
};
// Act
await handler.HandleAsync(eventData);
// Assert
mockEmailService.Verify(x => x.SendWelcomeEmailAsync("test@example.com", It.IsAny<CancellationToken>()), Times.Once);
}
Integration Testing
[Test]
public async Task EventBus_ShouldExecuteAllHandlersInCorrectOrder()
{
// Arrange
var services = new ServiceCollection();
services.AddSoftalleysEvents(typeof(UserRegisteredEvent).Assembly);
services.AddScoped<TestHandlerTracker>();
// Add other required services...
var serviceProvider = services.BuildServiceProvider();
var eventBus = serviceProvider.GetRequiredService<IEventBus>();
var eventData = new UserRegisteredEvent { UserId = "test", Email = "test@example.com" };
// Act
await eventBus.PublishAsync(eventData);
// Assert
var tracker = serviceProvider.GetRequiredService<TestHandlerTracker>();
Assert.That(tracker.ExecutionOrder, Is.EqualTo(new[] { "Pre", "Main", "Post" }));
}
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ค Contributing
We welcome contributions! Please feel free to submit a Pull Request.
๐ข About Softalleys
This library is part of the Softalleys Utilities collection, designed to provide robust, enterprise-ready components for .NET applications while maintaining simplicity and performance.
Happy Eventing! ๐
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.8)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.8)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.8)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Softalleys.Utilities.Events:
Package | Downloads |
---|---|
Softalleys.Utilities.Events.Distributed
Transport-agnostic distributed events core for Softalleys.Utilities.Events. Provides envelopes, naming, serialization, DI builder, event bus decorator, and receiver abstractions. |
|
Softalleys.Utilities.Events.Distributed.GooglePubSub
Google Pub/Sub transport implementation for Softalleys.Utilities.Events.Distributed. Provides Google Cloud Pub/Sub integration for distributed event publishing and receiving. |
GitHub repositories
This package is not used by any popular GitHub repositories.