LiteBus
Migration

LiteBus vs. MediatR

LiteBus and MediatR are both in-process mediators for .NET, but they make different trade-offs. This page is for a developer deciding between them, or one who knows MediatR and wants to map concepts onto LiteBus quickly. It states where the two agree, where they differ, and how to translate the common patterns.

Both libraries route a message to its handler so callers depend on an interface rather than a concrete service. The difference is in how much structure the library imposes and how much control it gives over the handler pipeline.

The Core Split

MediatR has two message shapes: a request (IRequest<T>), handled by one handler, and a notification (INotification), handled by many. Whether a request reads or writes is a convention you keep in your head.

LiteBus splits the write-side request into a command and the read-side request into a query, and gives each its own mediator and handler interfaces. The type a message implements states its intent: ICommand<T> changes state, IQuery<T> reads it, IEvent announces a fact. A reviewer sees the category from the declaration without opening the handler.

MediatRLiteBusNotes
IRequest (no result)ICommandOne handler, returns Task
IRequest<T> (write)ICommand<T>One handler, returns Task<T>
IRequest<T> (read)IQuery<T>One handler, side-effect free by convention
IStreamRequest<T>IStreamQuery<T>One handler, returns IAsyncEnumerable<T>
INotificationIEvent or any POCOZero to many handlers; LiteBus does not require an interface
IRequestHandler<TReq, TRes>ICommandHandler<TCmd, TRes> / IQueryHandler<TQry, TRes>Method is HandleAsync
INotificationHandler<T>IEventHandler<T>Method is HandleAsync

Pipeline Model

MediatR wraps a request in IPipelineBehavior<TRequest, TResponse> delegates. A behavior calls next() to continue, and code before and after that call runs before and after the handler. Cross-cutting steps are written as behaviors and apply to every request unless you constrain the generic.

LiteBus does not use a wrapping delegate. Each message type has named stages: pre-handlers, the main handler, post-handlers, and error-handlers. You attach a step to the stage where it belongs rather than reconstructing the stage from where you call next(). A pre-handler always runs before the main handler; a post-handler always runs after a successful one; an error-handler runs only on exception.

ConcernMediatRLiteBus
Run before handlerCode before next() in a behaviorICommandPreHandler<T> / IQueryPreHandler<T>
Run after handlerCode after next() in a behaviorICommandPostHandler<T, TResult> / IQueryPostHandler<T, TResult>
Handle failuretry/catch around next() in a behaviorICommandErrorHandler<T>
Short-circuitSkip calling next()AmbientExecutionContext.Current.Abort(result) from any stage
Cross-cutting for all messagesOpen generic behaviorOpen generic pre/post/error handler, or a handler on a base interface (polymorphic dispatch)

The pipeline is documented in full at The Handler Pipeline.

Translating a Behavior to LiteBus

A MediatR logging behavior:

public sealed class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        Console.WriteLine($"Handling {typeof(TRequest).Name}");
        var response = await next();
        Console.WriteLine($"Handled {typeof(TRequest).Name}");
        return response;
    }
}

The equivalent in LiteBus is two open generic handlers, each owning one side of the call:

public sealed class CommandLoggingPre<TCommand> : ICommandPreHandler<TCommand> where TCommand : ICommand
{
    public Task PreHandleAsync(TCommand command, CancellationToken ct = default)
    {
        Console.WriteLine($"Handling {typeof(TCommand).Name}");
        return Task.CompletedTask;
    }
}

public sealed class CommandLoggingPost<TCommand> : ICommandPostHandler<TCommand> where TCommand : ICommand
{
    public Task PostHandleAsync(TCommand command, object? result, CancellationToken ct = default)
    {
        Console.WriteLine($"Handled {typeof(TCommand).Name}");
        return Task.CompletedTask;
    }
}

The cost is two types instead of one. The benefit is that "before" and "after" are not coupled through a shared local variable, and the post-handler does not run when the main handler throws, which is usually what you want for logging success.

Capability Differences

CapabilityLiteBusMediatR
DI containerContainer-agnostic core; adapters for Microsoft DI and AutofacRegisters into Microsoft DI; broad container support via that
Handler resolutionReflection runs once at registration into a cached MessageRegistry; mediation reads metadataResolves through IServiceProvider at request time
Event concurrencyPer-call: priority groups and handlers within a group run Sequential or Parallel independentlyA single notification publisher strategy
Handler selectionBuilt-in [HandlerTag] and runtime HandlerPredicate filteringWrite a behavior to filter
Polymorphic dispatchA handler for a base type or interface runs for derived messages (pre/post/error and events)A handler for Base does not run for Derived
Durable messagingBuilt-in command inbox and event outbox with PostgreSQL storesNot built in; use an external library
LicenseMIT, freeCheck the current MediatR license terms for your use

Choosing

Choose LiteBus when the message category (command, query, event) should be visible in the type system, when you want named pipeline stages instead of wrapping behaviors, when event concurrency or handler filtering matters, or when you want a durable inbox or outbox without adding another dependency.

Choose MediatR when you prefer one request abstraction over the command/query split, when your pipeline needs are met by behaviors, or when you depend on its ecosystem of community packages.

The two are not mutually exclusive at the concept level: the CQS discipline LiteBus enforces is a discipline you can also keep by hand in MediatR. LiteBus moves it from convention into the compiler.

Next

On this page