Softalleys.Utilities.Events
1.0.0
See the version list below for details.
dotnet add package Softalleys.Utilities.Events --version 1.0.0
NuGet\Install-Package Softalleys.Utilities.Events -Version 1.0.0
<PackageReference Include="Softalleys.Utilities.Events" Version="1.0.0" />
<PackageVersion Include="Softalleys.Utilities.Events" Version="1.0.0" />
<PackageReference Include="Softalleys.Utilities.Events" />
paket add Softalleys.Utilities.Events --version 1.0.0
#r "nuget: Softalleys.Utilities.Events, 1.0.0"
#:package Softalleys.Utilities.Events@1.0.0
#addin nuget:?package=Softalleys.Utilities.Events&version=1.0.0
#tool nuget:?package=Softalleys.Utilities.Events&version=1.0.0
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
- ๐๏ธ 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 : IEventHandler<UserRegisteredEvent>
{
private readonly IEmailService _emailService;
private readonly IDbContext _dbContext;
public UserRegisteredEmailHandler(IEmailService emailService, IDbContext dbContext)
{
_emailService = emailService;
_dbContext = dbContext;
}
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 : IEventSingletonHandler<UserRegisteredEvent>
{
private readonly ILogger<UserRegisteredLoggerHandler> _logger;
public UserRegisteredLoggerHandler(ILogger<UserRegisteredLoggerHandler> logger)
{
_logger = logger;
}
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 : IEventPreHandler<UserRegisteredEvent>
{
private readonly IUserValidationService _validationService;
public UserValidationPreHandler(IUserValidationService validationService)
{
_validationService = validationService;
}
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 : IEventPostSingletonHandler<UserRegisteredEvent>
{
private readonly IAnalyticsService _analyticsService;
public UserAnalyticsPostHandler(IAnalyticsService analyticsService)
{
_analyticsService = analyticsService;
}
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 : ControllerBase
{
private readonly IEventBus _eventBus;
public UserController(IEventBus eventBus)
{
_eventBus = eventBus;
}
[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 six 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 |
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>
- 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");
}
}
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
-
net9.0
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.