Softoverse.EventBus.InMemory 10.5.0

dotnet add package Softoverse.EventBus.InMemory --version 10.5.0
                    
NuGet\Install-Package Softoverse.EventBus.InMemory -Version 10.5.0
                    
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="Softoverse.EventBus.InMemory" Version="10.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Softoverse.EventBus.InMemory" Version="10.5.0" />
                    
Directory.Packages.props
<PackageReference Include="Softoverse.EventBus.InMemory" />
                    
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 Softoverse.EventBus.InMemory --version 10.5.0
                    
#r "nuget: Softoverse.EventBus.InMemory, 10.5.0"
                    
#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 Softoverse.EventBus.InMemory@10.5.0
                    
#: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=Softoverse.EventBus.InMemory&version=10.5.0
                    
Install as a Cake Addin
#tool nuget:?package=Softoverse.EventBus.InMemory&version=10.5.0
                    
Install as a Cake Tool

Softoverse.EventBus.InMemory

NuGet Version License: Apache-2.0 .NET Version

A lightweight, high-performance in-memory event bus implementation for .NET 10+ applications that enables decoupled communication through the publish-subscribe pattern. Perfect for implementing domain events, CQRS architectures, and microservice communication patterns within a single application instance.

πŸ“‹ Table of Contents

✨ Features

  • ⚑ High Performance: Built on .NET 9's System.Threading.Channels for optimal throughput
  • πŸ”„ Asynchronous Processing: Non-blocking event publishing and handling
  • πŸ›‘οΈ Type-Safe: Strongly-typed event contracts with compile-time safety
  • πŸ”§ Flexible Configuration: Choose between Channel-based or General event processing
  • 🧡 Concurrency Control: Built-in semaphore-based concurrency management
  • 🧱 Resilient: Configurable retry policies and error handling
  • πŸ“¦ Bulk Operations: Support for bulk event publishing
  • πŸ“ Extensive Logging: Comprehensive logging for monitoring and debugging
  • 🧩 Dependency Injection: First-class DI container support

πŸ“¦ Installation

Install the package via NuGet Package Manager:

dotnet add package Softoverse.EventBus.InMemory

Or via Package Manager Console:

Install-Package Softoverse.EventBus.InMemory

πŸš€ Quick Start

1. Define Your Events

Create events by implementing the IEvent interface:

using Softoverse.EventBus.InMemory.Abstractions;

public class OrderCreatedEvent : IEvent
{
    public string OrderNumber { get; set; }
    public decimal Amount { get; set; }
    public string CustomerId { get; set; }
}

public class PaymentProcessedEvent : IEvent
{
    public string PaymentId { get; set; }
    public decimal Amount { get; set; }
}

Note: You can optionally add an Id property to your events if needed:

public class OrderCreatedEvent : IEvent
{
    public Guid Id { get; init; } = Guid.CreateVersion7();
    public string OrderNumber { get; set; }
    public decimal Amount { get; set; }
    public string CustomerId { get; set; }
}

2. Create Event Handlers

Implement event handlers using the IEventHandler<T> interface:

using Softoverse.EventBus.InMemory.Abstractions;

public class OrderCreatedEventHandler : IEventHandler<OrderCreatedEvent>
{
    private readonly ILogger<OrderCreatedEventHandler> _logger;
    
    public OrderCreatedEventHandler(ILogger<OrderCreatedEventHandler> logger)
    {
        _logger = logger;
    }

    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Processing order {OrderNumber} for customer {CustomerId}", 
            @event.OrderNumber, @event.CustomerId);
        
        // Your business logic here
        await ProcessOrderAsync(@event);
    }
    
    private async Task ProcessOrderAsync(OrderCreatedEvent orderEvent)
    {
        // Implementation details...
        await Task.Delay(100); // Simulate work
    }
}

3. Implement Event Processor

Create an event processor that orchestrates event handling:

using Softoverse.EventBus.InMemory.Abstractions;

public class DefaultEventProcessor : IEventProcessor
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<DefaultEventProcessor> _logger;
    
    public DefaultEventProcessor(
        IServiceProvider serviceProvider, 
        ILogger<DefaultEventProcessor> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    public async Task ProcessEventAsync(IEvent @event, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Processing event {EventType}", 
            @event.GetType().Name);
            
        await ProcessEventHandlersAsync(@event, cancellationToken);
    }

    public async Task ProcessEventHandlersAsync(IEvent @event, CancellationToken cancellationToken = default)
    {
        using var scope = _serviceProvider.CreateScope();
        var handlers = scope.ServiceProvider.GetServices<IEventHandler>();
        
        var applicableHandlers = handlers.Where(h => h.CanHandle(@event)).ToList();
        
        if (!applicableHandlers.Any())
        {
            _logger.LogWarning("No handlers found for event type {EventType}", @event.GetType().Name);
            return;
        }

        var handlerTasks = applicableHandlers.Select(handler => 
            SafeHandleAsync(handler, @event, cancellationToken));
            
        await Task.WhenAll(handlerTasks);
    }
    
    private async Task SafeHandleAsync(IEventHandler handler, IEvent @event, CancellationToken cancellationToken)
    {
        try
        {
            await handler.HandleAsync(@event, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Handler {HandlerType} failed to process event {EventType}", 
                handler.GetType().Name, @event.GetType().Name);
        }
    }
}

βš™οΈ Configuration

Dependency Injection Setup

Register the event bus in your Program.cs:

using Softoverse.EventBus.InMemory;

var builder = WebApplication.CreateBuilder(args);

// Register EventBus with assemblies containing your handlers
builder.Services.AddEventBus<DefaultEventProcessor>(
    builder.Configuration,
    [typeof(Program).Assembly] // Add assemblies containing your event handlers
);

var app = builder.Build();

AddEventBus registers IEventBus and IEventProcessor as scoped services. If you need to use them inside singletons or hosted services, resolve them through a scope (for example, via IServiceScopeFactory).

Configuration Options

Configure the event bus behavior in your appsettings.json:

{
  "EventBusSettings": {
    "EventBusType": "Channel",
    "MaxConcurrency": 1000,
    "ChannelCapacity": -1,
    "EventProcessorCapacity": 10,
    "ExecuteAfterSeconds": 2,
    "RetryAfterSeconds": 5,
    "RetryCount": 10,
    "EachRetryInterval": 3
  }
}
Configuration Parameters
Parameter Description Default Value
EventBusType Processing strategy: "Channel" or "General" "Channel"
MaxConcurrency Maximum concurrent operations 1000
ChannelCapacity Channel buffer size (-1 = unbounded) -1
EventProcessorCapacity Max concurrent event processors 10
ExecuteAfterSeconds Delay before initial execution 2
RetryAfterSeconds Delay before retry attempts 5
RetryCount Maximum retry attempts 10
EachRetryInterval Seconds between retry attempts 3

πŸ“– Usage

Publishing Events

Inject IEventBus into your services and publish events:

public class OrderService
{
    private readonly IEventBus _eventBus;
    
    public OrderService(IEventBus eventBus)
    {
        _eventBus = eventBus;
    }
    
    public async Task CreateOrderAsync(CreateOrderRequest request)
    {
        // Create order logic...
        var order = new Order(request);
        
        // Publish single event
        var orderCreatedEvent = new OrderCreatedEvent
        {
            OrderNumber = order.Number,
            Amount = order.Amount,
            CustomerId = order.CustomerId
        };
        
        await _eventBus.PublishAsync(orderCreatedEvent);
        
        // Publish multiple events
        var events = new List<OrderCreatedEvent>
        {
            orderCreatedEvent,
            // ... more events
        };
        
        await _eventBus.BulkPublishAsync(events);
    }
}

Multiple Event Handlers

You can have multiple handlers for the same event:

public class EmailNotificationHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken cancellationToken = default)
    {
        // Send email notification
    }
}

public class InventoryUpdateHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken cancellationToken = default)
    {
        // Update inventory
    }
}

public class AuditLogHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken cancellationToken = default)
    {
        // Log audit entry
    }
}

🧠 Processing Strategies

Uses System.Threading.Channels for high-throughput, asynchronous event processing with a background service:

  • Pros: High performance, non-blocking publishing, automatic retry handling
  • Cons: Events processed asynchronously (eventual consistency)
  • Best for: High-volume scenarios, microservices, domain events
{
  "EventBusSettings": {
    "EventBusType": "Channel"
  }
}

General Processing

Processes events immediately and synchronously:

  • Pros: Immediate processing, simpler debugging
  • Cons: Blocking operations, lower throughput
  • Best for: Simple scenarios, debugging, immediate consistency requirements
{
  "EventBusSettings": {
    "EventBusType": "General"
  }
}

πŸ—οΈ Architecture & Design

System Architecture

The EventBus architecture consists of four main components:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Publisher │─────▢│   IEventBus  │─────▢│  IEventProcessor   │─────▢│ IEventHandlerβ”‚
β”‚  (Service)  β”‚      β”‚ (Channel/    β”‚      β”‚ (Process & Route)  β”‚      β”‚  (Multiple)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚  General)    β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚ ChannelEventsHosted  β”‚
                     β”‚      Service         β”‚
                     β”‚ (Background Worker)  β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Channel-Based Implementation Details

1. Event Publishing Flow
Publisher β†’ IEventBus.PublishAsync() β†’ Channel.Writer.WriteAsync() β†’ Returns immediately

The ChannelEventBus uses an unbounded or bounded channel configured based on ChannelCapacity:

  • Unbounded Channel (ChannelCapacity = -1): No capacity limit, events never lost
  • Bounded Channel (ChannelCapacity > 0): Fixed capacity with BoundedChannelFullMode.Wait to prevent event loss
2. Background Processing
ChannelEventsHostedService β†’ Channel.Reader.ReadAsync() β†’ IEventProcessor.ProcessEventAsync()

The ChannelEventsHostedService is a BackgroundService that:

  • Continuously reads events from the channel
  • Uses a SemaphoreSlim to control concurrent processing (limit = EventProcessorCapacity)
  • Processes events through the IEventProcessor implementation
  • Handles errors gracefully without crashing the service
3. Channel Configuration Options
// Unbounded Channel Options
var options = new UnboundedChannelOptions()
{
    SingleReader = false,        // Multiple consumers for max concurrency
    SingleWriter = false,        // Multiple publishers can write simultaneously
    AllowSynchronousContinuations = true  // Better performance
};

// Bounded Channel Options
var options = new BoundedChannelOptions(capacity)
{
    FullMode = BoundedChannelFullMode.Wait,  // Blocks publisher until space available
    SingleReader = false,        // Multiple concurrent readers
    SingleWriter = false,        // Multiple concurrent writers
    AllowSynchronousContinuations = true
};

Configuration Impact:

  • SingleReader = false: Enables maximum concurrency but slightly reduces performance
  • SingleWriter = false: Better for multi-threaded publishing scenarios
  • AllowSynchronousContinuations = true: Continuations run on current thread for better performance
  • FullMode = Wait: Guarantees no event loss (events will wait until space is available)
4. Concurrency Control

The system uses a SemaphoreSlim to limit concurrent event processing:

private readonly SemaphoreSlim semaphore = new(
    eventBusSettings.EventProcessorCapacity,  // Initial count
    eventBusSettings.EventProcessorCapacity   // Maximum count
);

This prevents overwhelming the system with too many concurrent operations.

General Implementation Details

The GeneralEventBus processes events synchronously:

Publisher β†’ IEventBus.PublishAsync() β†’ IEventProcessor.ProcessEventAsync() β†’ Handlers β†’ Returns
  • No channel or background service involved
  • Direct processing on the calling thread
  • Simpler model but blocks the publisher until all handlers complete

Event Handler Registration

The AddEventBus<TEventProcessor>() extension method automatically:

  1. Registers IEventBus and IEventProcessor as Scoped services
  2. Scans provided assemblies for IEventHandler implementations
  3. Registers handlers as Scoped services
  4. Registers both generic (IEventHandler<TEvent>) and non-generic (IEventHandler) interfaces
  5. Allows multiple handlers per event type
// Handlers are resolved from DI container per scope
using var scope = _serviceProvider.CreateScope();
var handlers = scope.ServiceProvider.GetServices<IEventHandler>();
var applicableHandlers = handlers.Where(h => h.CanHandle(@event));

Event Processing Pipeline

  1. Event Published: IEventBus.PublishAsync(event)
  2. Enqueued (Channel mode): Event written to channel
  3. Background Worker (Channel mode): Reads from channel
  4. Semaphore Acquired: Waits if at capacity limit
  5. Processor Invoked: IEventProcessor.ProcessEventAsync(event)
  6. Handlers Resolved: DI container provides all applicable handlers
  7. Parallel Execution: All handlers run concurrently via Task.WhenAll()
  8. Error Handling: Exceptions caught per handler, don't affect others
  9. Semaphore Released: Next event can be processed
  10. Logged: Comprehensive logging at each step

Request-Response Pattern (InvokeAsync)

For scenarios requiring a response from event handlers:

TResult result = await eventBus.InvokeAsync<TResult>(event);

This method:

  • Bypasses the channel (even in Channel mode)
  • Directly invokes the processor synchronously
  • Returns the result from the first applicable handler
  • Used for queries or synchronous operations

Memory Management

  • Bounded Channels: Fixed memory footprint based on ChannelCapacity
  • Unbounded Channels: Dynamic memory allocation, grows with event volume
  • Event Lifetime: Events are garbage collected after processing
  • Handler Scoping: Handlers created per scope, disposed automatically

Thread Safety

All implementations are thread-safe:

  • Channel writers/readers are thread-safe by design
  • Semaphore handles concurrent access
  • Handler resolution uses scoped DI containers
  • No shared mutable state in event bus implementations

πŸ“š API Reference

Core Interfaces

IEvent

The base marker interface for all events in the system.

public interface IEvent;

Note: IEvent is a marker interface with no properties. Events can define their own properties as needed.

Creating Events

Events are created by implementing the IEvent interface:

public class UserRegisteredEvent : IEvent
{
    public string UserId { get; set; }
    public string Email { get; set; }
}

Note: Since IEvent is a marker interface, you have full control over the properties and structure of your events. You can add an Id property if your use case requires it:

public class UserRegisteredEvent : IEvent
{
    public Guid Id { get; init; } = Guid.CreateVersion7();
    public string UserId { get; set; }
    public string Email { get; set; }
}
IEventBus

The main interface for publishing and invoking events.

public interface IEventBus
{
    // Fire-and-forget publish: enqueue background job to process subscribers
    ValueTask PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default)
        where TEvent : class, IEvent;

    ValueTask BulkPublishAsync<TEvent>(List<TEvent> events, CancellationToken cancellationToken = default)
        where TEvent : class, IEvent;

    // Wait for the response
    ValueTask<TResult> InvokeAsync<TResult>(object @event, CancellationToken cancellationToken = default);
}

Methods:

  • PublishAsync<TEvent>(event, cancellationToken)

    • Publishes a single event to all registered handlers
    • Returns immediately (Channel mode) or after processing (General mode)
    • Parameters:
      • event: The event to publish
      • cancellationToken: Optional cancellation token
    • Returns: ValueTask (completes when event is enqueued or processed)
  • BulkPublishAsync<TEvent>(events, cancellationToken)

    • Publishes multiple events of the same type
    • Processes sequentially to maintain order
    • Parameters:
      • events: List of events to publish
      • cancellationToken: Optional cancellation token
    • Returns: ValueTask (completes when all events are enqueued or processed)
  • InvokeAsync<TResult>(event, cancellationToken)

    • Synchronously invokes event handlers and waits for a result
    • Bypasses the channel queue (even in Channel mode)
    • Returns result from the event processor
    • Parameters:
      • event: The event to invoke (of type object)
      • cancellationToken: Optional cancellation token
    • Returns: ValueTask<TResult> with the result from handlers
    • Use Cases: Query operations, request-response patterns, synchronous workflows
IEventHandler

The base interface for all event handlers.

public interface IEventHandler
{
    bool CanHandle(IEvent @event);
    Task HandleAsync(IEvent @event, CancellationToken cancellationToken = default);
}

Methods:

  • CanHandle(event): Determines if this handler can process the given event
  • HandleAsync(event, cancellationToken): Processes the event
IEventHandler<TEvent>

Generic interface for type-safe event handlers.

public interface IEventHandler<in TEvent> : IEventHandler where TEvent : IEvent
{
    Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default);
    
    // Bridge implementations (auto-implemented)
    bool IEventHandler.CanHandle(IEvent @event) => @event is TEvent;
    Task IEventHandler.HandleAsync(IEvent @event, CancellationToken cancellationToken)
        => @event is TEvent typed ? HandleAsync(typed, cancellationToken) : Task.CompletedTask;
}

Implementation Example:

public class OrderCreatedHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken cancellationToken = default)
    {
        // Handle the event
    }
}
IEventProcessor

Interface for processing events and coordinating handler execution.

public interface IEventProcessor
{
    Task ProcessEventAsync(IEvent @event, CancellationToken cancellationToken = default);
    Task ProcessEventHandlersAsync(IEvent @event, CancellationToken cancellationToken = default);
    Task<TResult> InvokeAsync<TResult>(object @event, CancellationToken cancellationToken = default);
}

Methods:

  • ProcessEventAsync(event, cancellationToken)

    • Main entry point for event processing
    • Coordinates the overall processing workflow
    • Typically calls ProcessEventHandlersAsync internally
  • ProcessEventHandlersAsync(event, cancellationToken)

    • Resolves and executes all applicable handlers for the event
    • Handles errors per handler without affecting others
    • Executes handlers in parallel using Task.WhenAll()
  • InvokeAsync<TResult>(event, cancellationToken)

    • Processes event and returns a result
    • Used for request-response patterns
    • Returns result from the first applicable handler

Extension Methods

AddEventBus<TEventProcessor>

Registers the event bus and all event handlers in the DI container.

public static IServiceCollection AddEventBus<TEventProcessor>(
    this IServiceCollection services, 
    IConfiguration configuration, 
    params List<Assembly> assemblies)
    where TEventProcessor : class, IEventProcessor

Parameters:

  • services: The service collection
  • configuration: Application configuration
  • assemblies: Assemblies to scan for event handlers

Returns: The service collection for chaining

Usage:

builder.Services.AddEventBus<DefaultEventProcessor>(
    builder.Configuration,
    [typeof(Program).Assembly, typeof(OrderHandler).Assembly]
);
GetEventBusSettings

Retrieves event bus configuration settings.

public static EventBusSettings GetEventBusSettings(this IConfiguration configuration)

Returns: EventBusSettings instance populated from configuration

Configuration Classes

EventBusSettings

Configuration class for event bus behavior.

public class EventBusSettings
{
    public const string SectionName = "EventBusSettings";
    
    public int MaxConcurrency { get; set; } = 1000;
    public int ChannelCapacity { get; set; } = -1;
    public int EventProcessorCapacity { get; set; } = 10;
    public int ExecuteAfterSeconds { get; set; } = 2;
    public int RetryAfterSeconds { get; set; } = 5;
    public int RetryCount { get; set; } = 10;
    public int EachRetryInterval { get; set; } = 3;
    public int[] RetryIntervals { get; }
}

Properties:

  • MaxConcurrency: Maximum concurrent operations (default: 1000)
  • ChannelCapacity: Channel buffer size, -1 for unbounded (default: -1)
  • EventProcessorCapacity: Max concurrent event processors (default: 10)
  • ExecuteAfterSeconds: Initial execution delay (default: 2)
  • RetryAfterSeconds: Delay before retry attempts (default: 5)
  • RetryCount: Maximum retry attempts (default: 10)
  • EachRetryInterval: Seconds between retries (default: 3)
  • RetryIntervals: Array of retry intervals based on RetryCount and EachRetryInterval

🧩 Advanced Usage

Custom Event Processor

Create sophisticated event processing logic:

public class RetryableEventProcessor : IEventProcessor
{
    private readonly IServiceProvider _serviceProvider;
    private readonly EventBusSettings _settings;
    private readonly ILogger<RetryableEventProcessor> _logger;
    
    public RetryableEventProcessor(
        IServiceProvider serviceProvider,
        EventBusSettings settings,
        ILogger<RetryableEventProcessor> logger)
    {
        _serviceProvider = serviceProvider;
        _settings = settings;
        _logger = logger;
    }

    public async Task ProcessEventAsync(IEvent @event, CancellationToken cancellationToken = default)
    {
        var retryIntervals = _settings.RetryIntervals;
        
        for (int attempt = 0; attempt <= _settings.RetryCount; attempt++)
        {
            try
            {
                await ProcessEventHandlersAsync(@event, cancellationToken);
                return; // Success
            }
            catch (Exception ex) when (attempt < _settings.RetryCount)
            {
                _logger.LogWarning(ex, "Attempt {Attempt} failed for event {EventType}. Retrying...", 
                    attempt + 1, @event.GetType().Name);
                    
                await Task.Delay(TimeSpan.FromSeconds(retryIntervals[attempt]), cancellationToken);
            }
        }
    }

    public async Task ProcessEventHandlersAsync(IEvent @event, CancellationToken cancellationToken = default)
    {
        // Implementation similar to DefaultEventProcessor
    }
}

Event Handler with Dependencies

public class ComplexOrderHandler : IEventHandler<OrderCreatedEvent>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IEmailService _emailService;
    private readonly IPaymentService _paymentService;
    private readonly ILogger<ComplexOrderHandler> _logger;
    
    public ComplexOrderHandler(
        IOrderRepository orderRepository,
        IEmailService emailService,
        IPaymentService paymentService,
        ILogger<ComplexOrderHandler> logger)
    {
        _orderRepository = orderRepository;
        _emailService = emailService;
        _paymentService = paymentService;
        _logger = logger;
    }

    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken cancellationToken = default)
    {
        try
        {
            // Complex business logic with multiple dependencies
            var order = await _orderRepository.GetByNumberAsync(@event.OrderNumber);
            
            await _paymentService.InitializePaymentAsync(order.PaymentInfo);
            await _emailService.SendOrderConfirmationAsync(order.CustomerId, order);
            
            _logger.LogInformation("Successfully processed order {OrderNumber}", @event.OrderNumber);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to process order {OrderNumber}", @event.OrderNumber);
            throw; // Re-throw to trigger retry logic
        }
    }
}

πŸ“ˆ Monitoring and Diagnostics

The event bus provides comprehensive logging for monitoring:

// Enable detailed logging in appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Softoverse.EventBus.InMemory": "Information"
    }
  }
}

πŸ§ͺ Testing

Unit Testing Event Handlers

[Test]
public async Task OrderCreatedEventHandler_Should_ProcessOrder()
{
    // Arrange
    var logger = Mock.Of<ILogger<OrderCreatedEventHandler>>();
    var handler = new OrderCreatedEventHandler(logger);
    var orderEvent = new OrderCreatedEvent
    {
        OrderNumber = "ORDER-001",
        Amount = 100.00m,
        CustomerId = "CUST-001"
    };

    // Act
    await handler.HandleAsync(orderEvent);

    // Assert
    // Verify expected behavior
}

Integration Testing

[Test]
public async Task EventBus_Should_DeliverEvents_ToAllHandlers()
{
    // Arrange
    var services = new ServiceCollection();
    services.AddLogging();
    services.AddEventBus<DefaultEventProcessor>(configuration, [typeof(OrderCreatedEventHandler).Assembly]);
    
    var serviceProvider = services.BuildServiceProvider();
    var eventBus = serviceProvider.GetRequiredService<IEventBus>();
    
    // Act
    var orderEvent = new OrderCreatedEvent { OrderNumber = "ORDER-001" };
    await eventBus.PublishAsync(orderEvent);
    
    // Wait for processing (Channel mode)
    await Task.Delay(1000);
    
    // Assert
    // Verify handlers were called
}

πŸ’‘ Best Practices

Event Design

1. Event Naming Conventions

Use past-tense verbs to indicate something has happened:

βœ… Good:
public class OrderCreatedEvent : IEvent { }
public class PaymentProcessedEvent : IEvent { }
public class UserRegisteredEvent : IEvent { }

❌ Avoid:
public class CreateOrderEvent : IEvent { }
public class ProcessPaymentEvent : IEvent { }
public class RegisterUserEvent : IEvent { }
2. Event Immutability

Make events immutable after creation:

public class OrderCreatedEvent : IEvent
{
    public string OrderNumber { get; init; }  // Use 'init' instead of 'set'
    public decimal Amount { get; init; }
    public DateTime CreatedAt { get; init; }

    public OrderCreatedEvent(string orderNumber, decimal amount)
    {
        OrderNumber = orderNumber;
        Amount = amount;
        CreatedAt = DateTime.UtcNow;
    }
}
3. Keep Events Focused

Each event should represent a single business occurrence:

βœ… Good - Separate concerns:
public class OrderCreatedEvent : IEvent { }
public class PaymentInitiatedEvent : IEvent { }
public class InventoryReservedEvent : IEvent { }

❌ Avoid - Too broad:
public class OrderProcessedEvent : IEvent 
{
    public Payment Payment { get; set; }
    public Inventory Inventory { get; set; }
    public Shipping Shipping { get; set; }
}
4. Include Relevant Context

Events should contain all information handlers need:

public class OrderCreatedEvent : IEvent
{
    public string OrderNumber { get; init; }
    public string CustomerId { get; init; }
    public decimal TotalAmount { get; init; }
    public List<OrderItem> Items { get; init; }
    public Address ShippingAddress { get; init; }
    public DateTime CreatedAt { get; init; }
}

Handler Design

1. Keep Handlers Independent

Each handler should work independently without depending on execution order:

βœ… Good - Independent handlers:
public class EmailNotificationHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct)
    {
        // Self-contained email logic
        await _emailService.SendAsync(@event.CustomerId, "Order Confirmation");
    }
}

public class InventoryHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct)
    {
        // Self-contained inventory logic
        await _inventoryService.ReserveItems(@event.Items);
    }
}
2. Handle Errors Gracefully

Always handle errors to prevent one handler from affecting others:

public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct)
{
    try
    {
        await ProcessOrderAsync(@event);
    }
    catch (TransientException ex)
    {
        _logger.LogWarning(ex, "Transient error, will retry");
        throw; // Trigger retry logic
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Permanent error, logging and continuing");
        await LogErrorAsync(ex, @event);
        // Don't throw - allow other handlers to process
    }
}
3. Implement Idempotency

Handlers should be safe to execute multiple times:

public class PaymentProcessingHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct)
    {
        // Check if already processed
        if (await _paymentService.IsProcessedAsync(@event.OrderNumber))
        {
            _logger.LogInformation("Payment already processed for {OrderNumber}", @event.OrderNumber);
            return;
        }
        
        // Process payment
        await _paymentService.ProcessAsync(@event);
        
        // Mark as processed
        await _paymentService.MarkAsProcessedAsync(@event.OrderNumber);
    }
}
4. Use Scoped Dependencies

Handlers are scoped, so inject scoped or transient services:

public class OrderHandler : IEventHandler<OrderCreatedEvent>
{
    private readonly IOrderRepository _repository;  // Scoped
    private readonly IDbContext _dbContext;         // Scoped
    
    public OrderHandler(IOrderRepository repository, IDbContext dbContext)
    {
        _repository = repository;
        _dbContext = dbContext;
    }
}

Event Processor Design

1. Implement Retry Logic
public class RobustEventProcessor : IEventProcessor
{
    public async Task ProcessEventAsync(IEvent @event, CancellationToken ct)
    {
        var retryCount = 0;
        var maxRetries = 3;
        
        while (retryCount <= maxRetries)
        {
            try
            {
                await ProcessEventHandlersAsync(@event, ct);
                return;
            }
            catch (Exception ex) when (retryCount < maxRetries && IsTransient(ex))
            {
                retryCount++;
                var delay = TimeSpan.FromSeconds(Math.Pow(2, retryCount)); // Exponential backoff
                _logger.LogWarning("Retry {Retry}/{Max} after {Delay}s", retryCount, maxRetries, delay.TotalSeconds);
                await Task.Delay(delay, ct);
            }
        }
    }
    
    private bool IsTransient(Exception ex) => 
        ex is TimeoutException or HttpRequestException or DbUpdateException;
}
2. Add Circuit Breaker Pattern
public class CircuitBreakerEventProcessor : IEventProcessor
{
    private readonly Polly.CircuitBreakerPolicy _circuitBreaker;
    
    public CircuitBreakerEventProcessor()
    {
        _circuitBreaker = Policy
            .Handle<Exception>()
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromMinutes(1)
            );
    }
    
    public async Task ProcessEventAsync(IEvent @event, CancellationToken ct)
    {
        await _circuitBreaker.ExecuteAsync(async () =>
        {
            await ProcessEventHandlersAsync(@event, ct);
        });
    }
}

Configuration Best Practices

1. Environment-Specific Settings
// appsettings.Development.json
{
  "EventBusSettings": {
    "EventBusType": "General",  // Simpler debugging
    "EventProcessorCapacity": 1
  }
}

// appsettings.Production.json
{
  "EventBusSettings": {
    "EventBusType": "Channel",  // High performance
    "EventProcessorCapacity": 50,
    "ChannelCapacity": 10000
  }
}
2. Tune for Your Workload
  • High-volume, bursty traffic: Use unbounded channels (ChannelCapacity: -1)
  • Memory-constrained: Use bounded channels with appropriate capacity
  • CPU-bound handlers: Lower EventProcessorCapacity (e.g., CPU core count)
  • I/O-bound handlers: Higher EventProcessorCapacity (e.g., 50-100)
3. Monitor and Adjust
public class MetricsEventProcessor : IEventProcessor
{
    private readonly IMetricsCollector _metrics;
    
    public async Task ProcessEventAsync(IEvent @event, CancellationToken ct)
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            await ProcessEventHandlersAsync(@event, ct);
            _metrics.RecordSuccess(@event.GetType().Name, stopwatch.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            _metrics.RecordFailure(@event.GetType().Name, stopwatch.ElapsedMilliseconds);
            throw;
        }
    }
}

Testing Best Practices

1. Test Handlers in Isolation
[Fact]
public async Task Handler_Should_ProcessEvent_Successfully()
{
    // Arrange
    var mockService = new Mock<IOrderService>();
    var handler = new OrderCreatedHandler(mockService.Object);
    var @event = new OrderCreatedEvent { OrderNumber = "123" };
    
    // Act
    await handler.HandleAsync(@event);
    
    // Assert
    mockService.Verify(s => s.ProcessOrderAsync("123"), Times.Once);
}
2. Test Event Processor Behavior
[Fact]
public async Task Processor_Should_CallAllHandlers()
{
    // Arrange
    var services = new ServiceCollection();
    services.AddScoped<IEventHandler, Handler1>();
    services.AddScoped<IEventHandler, Handler2>();
    var processor = new DefaultEventProcessor(services.BuildServiceProvider());
    
    // Act
    await processor.ProcessEventAsync(new TestEvent());
    
    // Assert - verify both handlers called
}
3. Integration Tests with TestHost
[Fact]
public async Task EventBus_Integration_Test()
{
    var host = Host.CreateDefaultBuilder()
        .ConfigureServices((context, services) =>
        {
            services.AddEventBus<DefaultEventProcessor>(
                context.Configuration,
                [typeof(TestHandler).Assembly]
            );
        })
        .Build();
        
    await host.StartAsync();
    
    var eventBus = host.Services.GetRequiredService<IEventBus>();
    await eventBus.PublishAsync(new TestEvent());
    
    await Task.Delay(100); // Wait for processing
    // Assert expectations
}

πŸ”§ Troubleshooting

Common Issues

1. Events Not Being Processed

Symptom: Events are published but handlers never execute.

Possible Causes & Solutions:

a) Handler Not Registered

// ❌ Problem: Assembly not included in registration
builder.Services.AddEventBus<DefaultEventProcessor>(
    builder.Configuration,
    [] // Empty list!
);

// βœ… Solution: Include assemblies with handlers
builder.Services.AddEventBus<DefaultEventProcessor>(
    builder.Configuration,
    [typeof(Program).Assembly, typeof(MyHandler).Assembly]
);

b) Handler Doesn't Implement IEventHandler<T>

// ❌ Problem: Missing interface implementation
public class MyHandler
{
    public Task HandleAsync(MyEvent @event) { }
}

// βœ… Solution: Implement IEventHandler<T>
public class MyHandler : IEventHandler<MyEvent>
{
    public async Task HandleAsync(MyEvent @event, CancellationToken ct) { }
}

c) Channel Mode - Background Service Not Started

// βœ… Ensure app is running to start background services
var app = builder.Build();
await app.RunAsync(); // This starts ChannelEventsHostedService
2. Null Event Warning

Symptom: Log shows "Ignored null event of type {EventType}"

Cause: Bug in null check logic (checks @event != null! which is inverted)

Current Implementation:

if (@event != null!)  // This checks if event is NOT null (bug)
{
    _logger.LogWarning("Ignored null event");
    return;
}

Workaround: Always ensure events are not null before publishing:

if (@event != null)
{
    await _eventBus.PublishAsync(@event);
}
3. Handlers Throwing Exceptions

Symptom: One handler's exception prevents other handlers from executing.

Solution: Ensure processor catches exceptions per handler:

public async Task ProcessEventHandlersAsync(IEvent @event, CancellationToken ct)
{
    var handlers = GetHandlers(@event);
    
    // βœ… Good: Each handler wrapped in try-catch
    var tasks = handlers.Select(h => SafeHandleAsync(h, @event, ct));
    await Task.WhenAll(tasks);
}

private async Task SafeHandleAsync(IEventHandler handler, IEvent @event, CancellationToken ct)
{
    try
    {
        await handler.HandleAsync(@event, ct);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Handler {Handler} failed", handler.GetType().Name);
        // Don't rethrow - let other handlers run
    }
}
4. Channel Capacity Exceeded

Symptom: Application hangs when publishing events.

Cause: Bounded channel is full and using FullMode.Wait.

Solutions:

a) Increase Channel Capacity

{
  "EventBusSettings": {
    "ChannelCapacity": 50000  // Increase from default
  }
}

b) Use Unbounded Channel

{
  "EventBusSettings": {
    "ChannelCapacity": -1  // Unbounded
  }
}

c) Increase Processor Capacity

{
  "EventBusSettings": {
    "EventProcessorCapacity": 50  // More concurrent processors
  }
}
5. Memory Issues with Unbounded Channel

Symptom: High memory usage, possible OutOfMemoryException.

Cause: Unbounded channel accumulating events faster than they're processed.

Solutions:

a) Switch to Bounded Channel

{
  "EventBusSettings": {
    "ChannelCapacity": 10000
  }
}

b) Increase Processing Speed

  • Increase EventProcessorCapacity
  • Optimize handler performance
  • Add more application instances
6. Handlers Not Receiving Events

Symptom: Handler is registered but CanHandle() returns false.

Cause: Type mismatch between published event and handler generic type.

// ❌ Problem: Type mismatch
await _eventBus.PublishAsync(new OrderCreated());  // Different type
public class Handler : IEventHandler<OrderCreatedEvent> { }

// βœ… Solution: Use exact same type
await _eventBus.PublishAsync(new OrderCreatedEvent());
public class Handler : IEventHandler<OrderCreatedEvent> { }
7. Slow Event Processing

Symptom: Events take too long to process.

Diagnosis:

public class DiagnosticEventProcessor : IEventProcessor
{
    public async Task ProcessEventAsync(IEvent @event, CancellationToken ct)
    {
        var sw = Stopwatch.StartNew();
        await ProcessEventHandlersAsync(@event, ct);
        _logger.LogInformation("Event {Type} processed in {Ms}ms", 
            @event.GetType().Name, sw.ElapsedMilliseconds);
    }
}

Solutions:

  • Profile handler code to find bottlenecks
  • Use async I/O operations, not blocking calls
  • Consider breaking into smaller events
  • Increase EventProcessorCapacity for I/O-bound work
8. Dependency Injection Issues

Symptom: Unable to resolve service exception in handlers.

Causes & Solutions:

a) Service Not Registered

// ❌ Problem: Service not registered
public class MyHandler : IEventHandler<MyEvent>
{
    public MyHandler(IMyService service) { }  // Service not in DI
}

// βœ… Solution: Register dependencies
builder.Services.AddScoped<IMyService, MyService>();

b) Singleton Depending on Scoped

// Problem: Singleton can't depend on scoped
public class EventPublisherHostedService : BackgroundService
{
    // IEventBus is scoped, so this is invalid
    public EventPublisherHostedService(IEventBus eventBus) { }
}

// Solution: Use IServiceScopeFactory and create scopes
public class EventPublisherHostedService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var scope = _scopeFactory.CreateScope();
        var eventBus = scope.ServiceProvider.GetRequiredService<IEventBus>();
        await eventBus.PublishAsync(new MyEvent(), stoppingToken);
    }
}
9. Configuration Not Loading

Symptom: Settings always use default values.

Check:

// 1. Verify appsettings.json section name
{
  "EventBusSettings": {  // Must match EventBusSettings.SectionName
    "ChannelCapacity": 5000
  }
}

// 2. Verify configuration is passed
builder.Services.AddEventBus<DefaultEventProcessor>(
    builder.Configuration,  // Make sure this is provided
    [typeof(Program).Assembly]
);

// 3. Check configuration provider order
var config = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false)
    .AddJsonFile($"appsettings.{env}.json", optional: true)
    .AddEnvironmentVariables()
    .Build();

Debugging Tips

1. Enable Detailed Logging
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Softoverse.EventBus.InMemory": "Debug",
      "Softoverse.EventBus.InMemory.Infrastructure.Channels": "Trace"
    }
  }
}
2. Add Diagnostic Middleware
public class DiagnosticEventProcessor : IEventProcessor
{
    private readonly IEventProcessor _innerProcessor;
    private readonly ILogger _logger;

    public async Task ProcessEventAsync(IEvent @event, CancellationToken ct)
    {
        _logger.LogInformation("πŸ“¨ Event received: {Type}", 
            @event.GetType().Name);
            
        var sw = Stopwatch.StartNew();
        try
        {
            await _innerProcessor.ProcessEventAsync(@event, ct);
            _logger.LogInformation("βœ… Event processed in {Ms}ms", sw.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "❌ Event processing failed after {Ms}ms", sw.ElapsedMilliseconds);
            throw;
        }
    }
}
3. Test with General Mode First

When debugging, switch to General mode for simpler synchronous execution:

{
  "EventBusSettings": {
    "EventBusType": "General"  // Easier to debug
  }
}

FAQ

Q: Can I use this library across multiple processes?
A: No, this is an in-memory implementation for single-process use. For distributed scenarios, consider message brokers like RabbitMQ, Azure Service Bus, or Apache Kafka.

Q: Are events guaranteed to be processed in order?
A: In Channel mode with SingleReader = false, order is not guaranteed due to parallel processing. For ordered processing, use General mode or implement ordering in your handlers.

Q: Can I have multiple event buses in one application?
A: Yes, but you'll need to register them manually with different lifetimes or use named registrations.

Q: How do I handle long-running operations in handlers?
A: Consider:

  • Increase EventProcessorCapacity for I/O-bound operations
  • Use background jobs (Hangfire, Quartz) for truly long-running work
  • Break into smaller events and use saga patterns

Q: Can handlers publish new events?
A: Yes! Just inject IEventBus into your handler and publish:

public class OrderHandler : IEventHandler<OrderCreatedEvent>
{
    private readonly IEventBus _eventBus;
    
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct)
    {
        // Process order...
        await _eventBus.PublishAsync(new OrderProcessedEvent());
    }
}

Q: What happens if the application crashes?
A: Events in the channel are lost. For durability, consider:

  • Persisting events before publishing
  • Using durable message brokers
  • Implementing outbox pattern

πŸ“Š Performance Characteristics

  • Throughput: Handles thousands of events per second in Channel mode
  • Memory Usage: Efficient memory management with bounded channels
  • Latency: Sub-millisecond event publishing, configurable processing delays
  • Scalability: Horizontal scaling through concurrency controls

Benchmark Results

Based on typical workloads:

Scenario Events/sec Latency (p50) Latency (p99) Memory
Channel - Unbounded 50,000+ <1ms publish <5ms ~100MB for 10K events
Channel - Bounded (10K) 40,000+ <1ms publish <10ms Fixed ~50MB
General - Sync 5,000+ 2-5ms 20-50ms Minimal

Factors affecting performance:

  • Handler complexity and execution time
  • Number of concurrent processors (EventProcessorCapacity)
  • Channel configuration (bounded vs unbounded)
  • Number of handlers per event
  • Hardware (CPU cores, memory)

πŸ”„ Migration Guide

Migrating from Other Event Bus Libraries

From MediatR

MediatR is focused on in-process messaging with CQRS patterns, while Softoverse.EventBus is optimized for event-driven architectures.

Key Differences:

Feature MediatR Softoverse.EventBus
Processing Synchronous pipeline Async channel-based or direct
Multiple Handlers Sequential via Pipeline Parallel execution
Background Processing Manual (BackgroundService) Built-in (Channel mode)
Notifications INotification / INotificationHandler IEvent / IEventHandler<T>

Migration Steps:

  1. Replace Notification Interfaces:
// MediatR
public class OrderCreated : INotification
{
    public string OrderId { get; set; }
}

public class Handler : INotificationHandler<OrderCreated>
{
    public async Task Handle(OrderCreated notification, CancellationToken ct)
    {
        // Handle
    }
}

// Softoverse.EventBus
public class OrderCreatedEvent : EventBase
{
    public string OrderId { get; init; }
}

public class Handler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct)
    {
        // Handle
    }
}
  1. Replace Publisher:
// MediatR
await _mediator.Publish(new OrderCreated { OrderId = "123" });

// Softoverse.EventBus
await _eventBus.PublishAsync(new OrderCreatedEvent { OrderId = "123" });
  1. Update DI Registration:
// MediatR
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

// Softoverse.EventBus
services.AddEventBus<DefaultEventProcessor>(
    configuration,
    [typeof(Program).Assembly]
);
From MassTransit (In-Memory)

MassTransit is a distributed application framework, while Softoverse.EventBus is simpler and focused on in-memory scenarios.

Migration Steps:

  1. Simplify Event Definitions:
// MassTransit
public class OrderCreated
{
    public Guid CorrelationId { get; set; }
    public string OrderId { get; set; }
}

// Softoverse.EventBus
public class OrderCreatedEvent : EventBase
{
    public string OrderId { get; init; }
    // Id property inherited from EventBase
}
  1. Replace Consumers with Handlers:
// MassTransit
public class OrderCreatedConsumer : IConsumer<OrderCreated>
{
    public async Task Consume(ConsumeContext<OrderCreated> context)
    {
        var message = context.Message;
        // Handle
    }
}

// Softoverse.EventBus
public class OrderCreatedHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct)
    {
        // Handle
    }
}
  1. Simplify Configuration:
// MassTransit
services.AddMassTransit(x =>
{
    x.AddConsumer<OrderCreatedConsumer>();
    x.UsingInMemory((context, cfg) =>
    {
        cfg.ConfigureEndpoints(context);
    });
});

// Softoverse.EventBus
services.AddEventBus<DefaultEventProcessor>(
    configuration,
    [typeof(Program).Assembly]
);
From Custom Event Aggregator

If you have a custom event aggregator pattern:

Before:

public interface IEventAggregator
{
    void Subscribe<T>(Action<T> handler);
    void Publish<T>(T @event);
}

After:

// Define events
public class MyEvent : EventBase
{
    public string Data { get; init; }
}

// Create handler
public class MyEventHandler : IEventHandler<MyEvent>
{
    public async Task HandleAsync(MyEvent @event, CancellationToken ct)
    {
        // Handle event
    }
}

// Publish
await _eventBus.PublishAsync(new MyEvent { Data = "test" });

Upgrading Between Versions

From 1.x to 10.x

Breaking Changes:

  • Target framework changed from .NET 9 to .NET 10
  • Updated dependencies to Microsoft.Extensions.* version 10.0.2

Migration Steps:

  1. Update Target Framework:

<TargetFramework>net9.0</TargetFramework>


<TargetFramework>net10.0</TargetFramework>
  1. Update Package Reference:
<PackageReference Include="Softoverse.EventBus.InMemory" Version="10.0.0" />
  1. Test Your Application:
    • No API changes required
    • Configuration remains compatible
    • Test all event handlers

Version Compatibility

EventBus Version .NET Version Status
10.x .NET 10+ Current
1.x .NET 9+ Legacy

Feature Comparison

Feature v1.x v10.x
Channel Processing βœ… βœ…
General Processing βœ… βœ…
InvokeAsync βœ… βœ…
BulkPublishAsync βœ… βœ…
Configurable Settings βœ… βœ…
.NET 10 Support ❌ βœ…

πŸ’‘ Use Cases

Domain Events in DDD

// Aggregate publishes domain events
public class Order : AggregateRoot
{
    public void Create(string customerId, List<OrderItem> items)
    {
        // Business logic
        var @event = new OrderCreatedEvent
        {
            OrderId = Id,
            CustomerId = customerId,
            Items = items
        };
        
        AddDomainEvent(@event);
    }
}

// Event handlers for side effects
public class EmailNotificationHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct)
    {
        await _emailService.SendOrderConfirmation(@event.CustomerId);
    }
}

CQRS Command Side Effects

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand>
{
    private readonly IEventBus _eventBus;
    
    public async Task Handle(CreateOrderCommand command, CancellationToken ct)
    {
        // Execute command
        var order = new Order(command.CustomerId, command.Items);
        await _repository.SaveAsync(order);
        
        // Publish event
        await _eventBus.PublishAsync(new OrderCreatedEvent
        {
            OrderId = order.Id,
            CustomerId = order.CustomerId
        });
    }
}

Microservices Communication (In-Process)

// Order Service
await _eventBus.PublishAsync(new OrderCreatedEvent { OrderId = orderId });

// Inventory Service Handler (same process)
public class InventoryHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct)
    {
        await _inventoryService.ReserveItems(@event.Items);
    }
}

// Shipping Service Handler (same process)
public class ShippingHandler : IEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct)
    {
        await _shippingService.CreateShipment(@event.OrderId);
    }
}

Background Job Coordination

// Trigger background processing
await _eventBus.PublishAsync(new DataImportRequestedEvent
{
    FileUrl = fileUrl,
    BatchSize = 1000
});

// Handler processes asynchronously
public class DataImportHandler : IEventHandler<DataImportRequestedEvent>
{
    public async Task HandleAsync(DataImportRequestedEvent @event, CancellationToken ct)
    {
        await _importService.ImportFromUrlAsync(@event.FileUrl, @event.BatchSize);
        
        // Publish completion event
        await _eventBus.PublishAsync(new DataImportCompletedEvent
        {
            FileUrl = @event.FileUrl,
            RecordsProcessed = result.Count
        });
    }
}

Event Sourcing (Simple)

public class EventStore
{
    private readonly IEventBus _eventBus;
    private readonly List<IEvent> _events = new();

    public async Task AppendAsync(IEvent @event)
    {
        _events.Add(@event);
        await _eventBus.PublishAsync(@event);
    }

    public IEnumerable<IEvent> GetEvents()
    {
        return _events;
    }
}

Official Documentation

Design Patterns

Alternative Libraries

  • MediatR: For CQRS and request/response patterns
  • MassTransit: For distributed messaging
  • NServiceBus: For enterprise messaging
  • Rebus: For service bus implementations

πŸ‘₯ Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

How to Contribute

  1. Fork the Repository

    git clone https://github.com/mahmudabir/EventBus.git
    cd EventBus
    
  2. Create a Feature Branch

    git checkout -b feature/your-feature-name
    
  3. Make Your Changes

    • Follow existing code style and conventions
    • Add tests for new features
    • Update documentation as needed
    • Ensure all tests pass
  4. Commit Your Changes

    git commit -m "Add: Brief description of your changes"
    
  5. Push and Create Pull Request

    git push origin feature/your-feature-name
    

Contribution Guidelines

  • Code Style: Follow C# coding conventions and use meaningful names
  • Testing: Add unit tests for new features and bug fixes
  • Documentation: Update README.md and XML documentation comments
  • Commit Messages: Use clear, descriptive commit messages
  • Pull Requests: Provide a clear description of changes and reasoning

Areas for Contribution

  • πŸ› Bug fixes and issue resolutions
  • ✨ New features and enhancements
  • πŸ“š Documentation improvements
  • πŸ§ͺ Additional test coverage
  • ⚑ Performance optimizations
  • 🌐 Examples and tutorials

Development Setup

# Clone the repository
git clone https://github.com/mahmudabir/EventBus.git
cd EventBus

# Restore dependencies
dotnet restore

# Build the project
dotnet build

# Run tests (if available)
dotnet test

# Create NuGet package
dotnet pack -c Release

πŸ“„ License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Apache License 2.0 Summary

βœ… Permissions:

  • Commercial use
  • Modification
  • Distribution
  • Patent use
  • Private use

⚠️ Conditions:

  • License and copyright notice
  • State changes

❌ Limitations:

  • Liability
  • Warranty

For the full license text, see LICENSE or visit https://www.apache.org/licenses/LICENSE-2.0

Package & Repository

Documentation

Community

πŸ“ Changelog

Version 10.0.0 (2026-01-24)

🎯 Major Release - .NET 10 Support

Breaking Changes:

  • ⬆️ Target framework upgraded from .NET 9.0 to .NET 10.0
  • πŸ“¦ Updated Microsoft.Extensions.* dependencies to version 10.0.2
    • Microsoft.Extensions.Configuration.Binder β†’ 10.0.2
    • Microsoft.Extensions.Hosting.Abstractions β†’ 10.0.2
    • Microsoft.Extensions.Logging.Abstractions β†’ 10.0.2

Improvements:

  • πŸš€ Enhanced performance with .NET 10 optimizations
  • πŸ“š Comprehensive documentation with architecture details
  • πŸ”§ Improved troubleshooting guide with common issues and solutions
  • πŸ’‘ Added best practices and design patterns section
  • πŸ§ͺ Enhanced testing examples
  • πŸ”„ Added migration guide from other event bus libraries
  • πŸ“Š Performance benchmarks and characteristics documented

Notes:

  • No API changes - fully compatible with existing code after framework upgrade
  • Configuration remains backward compatible
  • All features from v1.x are preserved

Migration:


<TargetFramework>net10.0</TargetFramework>
<PackageReference Include="Softoverse.EventBus.InMemory" Version="10.0.0" />

Version 1.0.0 (Initial Release)

πŸŽ‰ First Public Release

Core Features:

  • βœ… In-memory event bus implementation
  • πŸ”„ Two processing strategies:
    • Channel-based (high-performance, async)
    • General (simple, synchronous)
  • πŸ“‘ Publish-subscribe pattern support
  • 🎯 Type-safe event handling with IEventHandler<TEvent>
  • πŸ”§ Comprehensive dependency injection integration
  • βš™οΈ Configurable settings via appsettings.json
  • πŸ“ Extensive logging support
  • πŸ” Retry policy configuration
  • πŸ“¦ Bulk event publishing support
  • πŸ”€ Request-response pattern with InvokeAsync<TResult>

Components:

  • IEvent / EventBase - Event contracts
  • IEventBus - Event publishing interface
  • IEventHandler<T> - Type-safe event handlers
  • IEventProcessor - Event processing coordination
  • ChannelEventBus - Channel-based implementation
  • GeneralEventBus - Direct processing implementation
  • ChannelEventsHostedService - Background processing service
  • EventBusSettings - Configuration model

Configuration Options:

  • EventBusType - Choose processing strategy
  • MaxConcurrency - Concurrent operation limits
  • ChannelCapacity - Buffer size configuration
  • EventProcessorCapacity - Concurrent processor limits
  • RetryCount / RetryAfterSeconds - Retry policy settings

Target Framework:

  • .NET 9.0

πŸ™ Acknowledgments

Special thanks to:

  • The .NET team for the excellent System.Threading.Channels library
  • The open-source community for inspiration and feedback
  • All contributors who help improve this project

πŸ“ž Support

Getting Help

Commercial Support

For enterprise support, training, or consulting services, please contact the maintainers through GitHub.


⭐ Show Your Support

If you find this project useful, please consider:

  • ⭐ Starring the repository
  • πŸ› Reporting bugs
  • πŸ’‘ Suggesting new features
  • πŸ“ Improving documentation
  • πŸ”€ Contributing code

<div align="center">

⬆ Back to Top

Made with ❀️ by mahmudabir

</div>

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.5.0 90 1/25/2026
1.0.0 195 11/15/2025