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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Softalleys.Utilities.Events" Version="1.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Softalleys.Utilities.Events" Version="1.1.1" />
                    
Directory.Packages.props
<PackageReference Include="Softalleys.Utilities.Events" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Softalleys.Utilities.Events --version 1.1.1
                    
#r "nuget: Softalleys.Utilities.Events, 1.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Softalleys.Utilities.Events@1.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Softalleys.Utilities.Events&version=1.1.1
                    
Install as a Cake Addin
#tool nuget:?package=Softalleys.Utilities.Events&version=1.1.1
                    
Install as a Cake Tool

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:

  1. Pre-processing Singleton Handlers - IEventPreSingletonHandler<T>
  2. Pre-processing Scoped Handlers - IEventPreHandler<T>
  3. Main Singleton Handlers - IEventSingletonHandler<T>
  4. Main Scoped Handlers - IEventHandler<T>
  5. Hosted Handlers - IEventHostedService<T> (singletons that are also IHostedService)
  6. Post-processing Singleton Handlers - IEventPostSingletonHandler<T>
  7. 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 calls HandleAsync(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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
1.1.1 203 8/13/2025
1.1.0 76 8/10/2025
1.0.0 152 8/8/2025