LiteBus: A Free and Ambitious Alternative to MediatR for .NET Applications

With MediatR recently announcing its transition to a commercial model, many .NET developers are evaluating alternatives for implementing the mediator pattern in their applications. If you're in this position, LiteBus might be worth considering—a lightweight, CQS-focused mediator library I developed five years ago that remains free and open-source. The Background Story Back in 2020, I was working as an engineer at a digital news media company, collaborating closely with our CTO on the initial architecture for new projects. We were building a content management system that needed to handle a high volume of media content, from articles to videos and live streams. Though not the technical lead, I was heavily involved in establishing the architectural approach for our growing team, which included both experienced developers and junior engineers. I was fortunate to have input on our technical stack as we implemented Domain-Driven Design with Command Query Separation patterns. While evaluating mediator libraries to help with this implementation, MediatR was the obvious choice for most teams, but I identified some potential issues for our specific use case: We wanted interfaces that directly reflected CQS concepts rather than generic requests Our MongoDB-based architecture could benefit from streaming support for large datasets With many junior developers joining the team, we needed the architecture to be as explicit as possible I researched alternatives like Enexure.MicroBus, but found it used too much reflection and wasn't actively maintained. After discussing options with our CTO, I suggested we could build something tailored to our specific requirements. This led me to develop LiteBus — optimized for our performance needs and designed with clear architectural boundaries to help onboard new team members. Core Architecture and Design Principles LiteBus was built with several key design principles in mind: Explicit over implicit: Clear interfaces representing CQS concepts Performance focus: Minimal reflection usage Modularity: Separated abstractions and implementations DDD-friendly: Support for domain events without framework dependencies One thing I focused on was reducing reflection by leveraging the .NET type system's covariance and contravariance. This means if you have a command hierarchy like BaseCommand and SpecificCommand, handlers can be registered for the base type and still process the specific types. This gives you inheritance-based handling without the performance cost of constantly using reflection. Package Structure LiteBus is modular by design, with abstractions and implementations separated into different packages: LiteBus.Commands.Abstractions LiteBus.Commands LiteBus.Commands.Extensions.MicrosoftDependencyInjection LiteBus.Queries.Abstractions LiteBus.Queries LiteBus.Queries.Extensions.MicrosoftDependencyInjection LiteBus.Events.Abstractions LiteBus.Events LiteBus.Events.Extensions.MicrosoftDependencyInjection With this separation you reference only the abstractions in your domain and application layers, keeping implementation details in your infrastructure layer which aligns well with clean architecture principles if you prioritize that. The Message Handling Pipeline LiteBus provides a comprehensive message handling pipeline with support for pre-processing, main handling, post-processing, and error handling. Here's an overview of the key interfaces available for each module: Command Module // Main interfaces ICommand // Base command interface (void result) ICommand // Command returning a result // Main handlers ICommandHandler // For void commands ICommandHandler // For commands returning results // Pre-handlers ICommandPreHandler // For all commands ICommandPreHandler // For specific command types ICommandValidator // Specialized validator // Post-handlers ICommandPostHandler // For all commands ICommandPostHandler // For specific commands ICommandPostHandler // For commands with results // Error handlers ICommandErrorHandler // For all commands ICommandErrorHandler // For specific command types Query Module // Main interfaces IQuery // Regular query interface IStreamQuery // Streaming query interface // Main handlers IQueryHandler // For regular queries IStreamQueryHandler // For streaming queries // Pre-handlers IQueryPreHandler // For all queries IQueryPreHandler // For specific query types // Post-handlers IQueryPostHandler // For all queries IQuer

Apr 19, 2025 - 20:37
 0
LiteBus: A Free and Ambitious Alternative to MediatR for .NET Applications

With MediatR recently announcing its transition to a commercial model, many .NET developers are evaluating alternatives for implementing the mediator pattern in their applications. If you're in this position, LiteBus might be worth considering—a lightweight, CQS-focused mediator library I developed five years ago that remains free and open-source.

The Background Story

Back in 2020, I was working as an engineer at a digital news media company, collaborating closely with our CTO on the initial architecture for new projects. We were building a content management system that needed to handle a high volume of media content, from articles to videos and live streams. Though not the technical lead, I was heavily involved in establishing the architectural approach for our growing team, which included both experienced developers and junior engineers.

I was fortunate to have input on our technical stack as we implemented Domain-Driven Design with Command Query Separation patterns. While evaluating mediator libraries to help with this implementation, MediatR was the obvious choice for most teams, but I identified some potential issues for our specific use case:

  1. We wanted interfaces that directly reflected CQS concepts rather than generic requests
  2. Our MongoDB-based architecture could benefit from streaming support for large datasets
  3. With many junior developers joining the team, we needed the architecture to be as explicit as possible

I researched alternatives like Enexure.MicroBus, but found it used too much reflection and wasn't actively maintained. After discussing options with our CTO, I suggested we could build something tailored to our specific requirements. This led me to develop LiteBus — optimized for our performance needs and designed with clear architectural boundaries to help onboard new team members.

Core Architecture and Design Principles

LiteBus was built with several key design principles in mind:

  1. Explicit over implicit: Clear interfaces representing CQS concepts
  2. Performance focus: Minimal reflection usage
  3. Modularity: Separated abstractions and implementations
  4. DDD-friendly: Support for domain events without framework dependencies

One thing I focused on was reducing reflection by leveraging the .NET type system's covariance and contravariance. This means if you have a command hierarchy like BaseCommand and SpecificCommand, handlers can be registered for the base type and still process the specific types. This gives you inheritance-based handling without the performance cost of constantly using reflection.

Package Structure

LiteBus is modular by design, with abstractions and implementations separated into different packages:

LiteBus.Commands.Abstractions
LiteBus.Commands
LiteBus.Commands.Extensions.MicrosoftDependencyInjection

LiteBus.Queries.Abstractions
LiteBus.Queries
LiteBus.Queries.Extensions.MicrosoftDependencyInjection

LiteBus.Events.Abstractions
LiteBus.Events
LiteBus.Events.Extensions.MicrosoftDependencyInjection

With this separation you reference only the abstractions in your domain and application layers, keeping implementation details in your infrastructure layer which aligns well with clean architecture principles if you prioritize that.

The Message Handling Pipeline

LiteBus provides a comprehensive message handling pipeline with support for pre-processing, main handling, post-processing, and error handling. Here's an overview of the key interfaces available for each module:

Command Module

// Main interfaces
ICommand                                          // Base command interface (void result)
ICommand<TCommandResult>                          // Command returning a result

// Main handlers
ICommandHandler<TCommand>                         // For void commands
ICommandHandler<TCommand, TCommandResult>         // For commands returning results

// Pre-handlers
ICommandPreHandler                                // For all commands
ICommandPreHandler<TCommand>                      // For specific command types
ICommandValidator<TCommand>                       // Specialized validator

// Post-handlers
ICommandPostHandler                               // For all commands
ICommandPostHandler<TCommand>                     // For specific commands
ICommandPostHandler<TCommand, TCommandResult>     // For commands with results

// Error handlers
ICommandErrorHandler                              // For all commands
ICommandErrorHandler<TCommand>                    // For specific command types

Query Module

// Main interfaces
IQuery<TQueryResult>                              // Regular query interface
IStreamQuery<TQueryResult>                        // Streaming query interface

// Main handlers
IQueryHandler<TQuery, TQueryResult>               // For regular queries
IStreamQueryHandler<TQuery, TQueryResult>         // For streaming queries

// Pre-handlers
IQueryPreHandler                                  // For all queries
IQueryPreHandler<TQuery>                          // For specific query types

// Post-handlers
IQueryPostHandler                                 // For all queries
IQueryPostHandler<TQuery>                         // For specific queries
IQueryPostHandler<TQuery, TQueryResult>           // For specific queries with results

// Error handlers
IQueryErrorHandler                                // For all queries
IQueryErrorHandler<TQuery>                        // For specific query types

Event Module

// Main interface
IEvent                                            // Base event interface (optional for POCO events)

// Main handlers
IEventHandler<TEvent>                             // For handling specific events

// Pre-handlers
IEventPreHandler                                  // For all events
IEventPreHandler<TEvent>                          // For specific event types

// Post-handlers
IEventPostHandler                                 // For all events
IEventPostHandler<TEvent>                         // For specific event types

// Error handlers
IEventErrorHandler                                // For all events
IEventErrorHandler<TEvent>                        // For specific event types

The pipeline components work together to process messages:

1. Pre-Handlers

Pre-handlers execute before the main handler, useful for cross-cutting concerns such as validation, logging, or security checks:

public class ValidationPreHandler : ICommandPreHandler<CreateArticleCommand>
{
    public Task PreHandleAsync(CreateArticleCommand command, CancellationToken cancellationToken)
    {
        // Validation logic
        if (string.IsNullOrEmpty(command.Title))
            throw new ValidationException("Title is required");

        return Task.CompletedTask;
    }
}

These pre-handlers can work with inheritance too. A pre-handler for a base command can automatically handle derived commands, so you don't need to duplicate validation logic across similar command types.

2. Main Handlers

Main handlers contain the core business logic for processing commands and queries:

public class CreateArticleCommandHandler : ICommandHandler<CreateArticleCommand, ArticleDto>
{
    private readonly IArticleRepository _repository;

    public async Task<ArticleDto> HandleAsync(CreateArticleCommand command, CancellationToken cancellationToken)
    {
        var article = new Article
        {
            Id = Guid.NewGuid(),
            Title = command.Title,
            Content = command.Content
        };

        await _repository.SaveAsync(article, cancellationToken);

        return new ArticleDto
        {
            Id = article.Id,
            Title = article.Title
        };
    }
}

In query handlers, the same inheritance principles apply. A handler can return a more specific type than what the interface requires, giving you flexibility without extra code.

3. Post-Handlers

Post-handlers run after the main handler, useful for operations like caching, notifications, or logging:

public class NotificationPostHandler : ICommandPostHandler<CreateArticleCommand, ArticleDto>
{
    private readonly IEventPublisher _eventPublisher;

    public async Task PostHandleAsync(
        CreateArticleCommand command, 
        ArticleDto result,
        CancellationToken cancellationToken)
    {
        await _eventPublisher.PublishAsync(new ArticleCreatedEvent
        {
            ArticleId = result.Id,
            Title = result.Title
        }, cancellationToken);
    }
}

Post-handlers follow the same pattern - a post-handler for a base command works with all derived commands.

4. Error Handlers

Error handlers provide centralized exception handling:

public class LoggingErrorHandler : ICommandErrorHandler<CreateArticleCommand>
{
    private readonly ILogger<LoggingErrorHandler> _logger;

    public Task HandleErrorAsync(
        CreateArticleCommand command, 
        object? result,
        Exception exception, 
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "Error creating article: {Title}", command.Title);
        return Task.CompletedTask;
    }
}

DDD-Friendly Event Handling

A particularly useful feature of LiteBus is its support for domain events without requiring domain objects to implement framework interfaces. This aligns with DDD principles, where the domain model should be independent of infrastructure concerns.

You can publish plain objects as events:

// Domain event with no dependencies on LiteBus
public class ArticlePublished
{
    public Guid ArticleId { get; }
    public string Title { get; }
    public DateTime PublishedAt { get; }

    public ArticlePublished(Guid articleId, string title)
    {
        ArticleId = articleId;
        Title = title;
        PublishedAt = DateTime.UtcNow;
    }
}

// In your application service
public async Task PublishArticle(Guid articleId)
{
    var article = await _repository.GetByIdAsync(articleId);
    article.MarkAsPublished();

    await _repository.SaveAsync(article);

    // Publish domain event without requiring it to implement IEvent
    await _eventPublisher.PublishAsync(new ArticlePublished(article.Id, article.Title));
}

Handlers for these events are still type-safe:

public class NotifySubscribersHandler : IEventHandler<ArticlePublished>
{
    public async Task HandleAsync(ArticlePublished @event, CancellationToken cancellationToken)
    {
        // Send notification to subscribers
    }
}

Specialized Features

Beyond the core functionality, LiteBus offers several specialized features that emerged from real-world requirements.

Streaming Support

LiteBus provides first-class support for streaming data through IStreamQuery and IAsyncEnumerable:

public class StreamRecentArticlesQuery : IStreamQuery<ArticleDto>
{
    public int BatchSize { get; init; } = 100;
}

public class StreamRecentArticlesQueryHandler 
    : IStreamQueryHandler<StreamRecentArticlesQuery, ArticleDto>
{
    private readonly IArticleRepository _repository;

    public async IAsyncEnumerable<ArticleDto> StreamAsync(
        StreamRecentArticlesQuery query,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await foreach (var article in _repository.GetRecentArticlesAsync(
            query.BatchSize, cancellationToken))
        {
            yield return new ArticleDto
            {
                Id = article.Id,
                Title = article.Title
            };
        }
    }
}

This streaming capability was crucial for our media platform, allowing us to efficiently process large sets of articles, videos, and other content without consuming excessive memory.

Contextual Command Handling

LiteBus supports tag-based filtering for handlers to enable different processing based on the execution context:

[HandlerTag("Frontend")]
public class FrontendValidator : ICommandPreHandler<PublishArticleCommand>
{
    // Validation logic specific to frontend requests
}

[HandlerTag("InternalService")]
public class InternalValidator : ICommandPreHandler<PublishArticleCommand>
{
    // Validation logic specific to internal service requests
}

This feature emerged from our need to handle commands differently based on their source. Our system had to process commands coming both from user-facing frontend applications and internal backend services. The validation requirements, security considerations, and other aspects varied significantly depending on the source.

With tag-based handlers, we could implement appropriate validation and processing for each context:

// From frontend
await _commandMediator.SendAsync(command, "Frontend");

// From internal service
await _commandMediator.SendAsync(command, "InternalService");

Generic Commands

Generic commands provide flexibility for cross-cutting concerns like logging:

public class LogActivityCommand<TPayload> : ICommand
{
    public required string UserId { get; init; }
    public required string Action { get; init; }
    public required TPayload Payload { get; init; }
}

public class LogActivityCommandHandler<TPayload> 
    : ICommandHandler<LogActivityCommand<TPayload>>
{
    public Task HandleAsync(LogActivityCommand<TPayload> command, 
        CancellationToken cancellationToken)
    {
        // Generic logging logic that works with any payload type
        return Task.CompletedTask;
    }
}

We used this pattern extensively for our activity logging system, allowing us to maintain a consistent logging structure while accommodating diverse payload types for different user actions.

Integration with ASP.NET Core

Integrating LiteBus into an ASP.NET Core application is straightforward:

services.AddLiteBus(liteBus =>
{
    liteBus.AddCommandModule(module =>
    {
        module.RegisterFromAssembly(typeof(Program).Assembly);
    });

    liteBus.AddQueryModule(module =>
    {
        module.RegisterFromAssembly(typeof(Program).Assembly);
    });

    liteBus.AddEventModule(module =>
    {
        module.RegisterFromAssembly(typeof(Program).Assembly);
    });
});

Feature Comparison with MediatR

For a factual comparison, here's how LiteBus measures up against MediatR:

Feature MediatR LiteBus
Base Abstraction IRequest ICommand, IQuery, IEvent
Streaming Support Added in later versions First-class with IStreamQuery
Pipeline Behaviors Pre-handlers, post-handlers, error handlers
Contextual Processing Limited Tag-based handler filtering
Reflection Usage Standard Minimized with type system features
POCO Event Support Limited Full support without requiring interfaces
Package Structure Consolidated Separated abstractions and implementations
Current Licensing Going Commercial Free and open-source

LiteBus has many more features not covered in this overview. For the complete picture, check out the GitHub repository and Wiki documentation, which include detailed examples, best practices, and guides for advanced scenarios.