FlowMediator is a lightweight, opinionated mediator library for .NET 8 and .NET 9.
It focuses on explicit application flow, not generic messaging.
FlowMediator was built around a simple idea:
Not everything is a request.
Commands and queries represent intent.
Events represent facts that already happened.
FlowMediator enforces this distinction explicitly.
| Concept | Description |
|---|---|
| SendAsync | Commands & Queries (single handler, pipeline-enabled) |
| PublishAsync | Events (multiple handlers, side-effect oriented) |
| Pipeline | Applies only to SendAsync |
| Events | Never treated as requests |
dotnet add package FlowMediatorpublic record GetUserByIdQuery(int Id) : IRequest<UserDto>;
public class GetUserByIdQueryHandler
: IRequestHandler<GetUserByIdQuery, UserDto>
{
private readonly IUserRepository _repository;
public GetUserByIdQueryHandler(IUserRepository repository)
{
_repository = repository;
}
public async Task<UserDto> Handle(
GetUserByIdQuery request,
CancellationToken cancellationToken)
{
var user = await _repository.GetByIdAsync(request.Id);
return user ?? throw new Exception("User not found");
}
}services.AddFlowMediator(typeof(GetUserByIdQuery).Assembly);
services.AddScoped<IUserRepository, UserRepository>();
services.AddTransient(
typeof(IPipelineBehavior<,>),
typeof(LoggingBehavior<,>)
);var mediator = provider.GetRequiredService<IMediator>();
var user = await mediator.SendAsync(new GetUserByIdQuery(1));
Console.WriteLine(user.Name);Events are published, not sent.
- Event handlers are executed sequentially (in-process) by default for predictability.
- Handler order is not guaranteed unless you explicitly control registration/ordering.
- If any handler throws, dispatch stops and the exception is rethrown (remaining handlers won’t run).
public class UserCreatedEvent : IEvent
{
public Guid EventId { get; } = Guid.NewGuid();
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}
await mediator.PublishAsync(new UserCreatedEvent());PublishAsyncexecutes handlers in-process and sequentially by default.- Handler order is not guaranteed unless explicitly controlled.
- No pipeline behaviors are applied to events.
- Exceptions: If an event handler throws, PublishAsync stops and the exception is re-thrown (remaining handlers won’t run).
- Events are not durable messages; for reliable cross-process delivery use Outbox + worker / broker.
- Keep handlers fast; long-running work should go to background processing.
public class UserCreatedEventHandler
: IEventHandler<UserCreatedEvent>
{
public Task Handle(
UserCreatedEvent @event,
CancellationToken cancellationToken)
{
Console.WriteLine("User created");
return Task.CompletedTask;
}
}Pipelines apply only to SendAsync.
They are ideal for:
- Logging
- Validation
- Transactions
Events are executed outside the pipeline.
- Clean application flow
- Explicit CQRS
- Domain-driven design
- Predictable execution
- FlowContext (CorrelationId, UserId, Metadata)
- Step-based execution model
- Observability and retry
- Command / Query specialization
FlowMediator focuses on application flow and execution semantics.
It does not provide:
- Authorization or authentication
- Security policies
- Exception handling strategies
- Infrastructure-level concerns
These responsibilities intentionally remain in the application layer.
FlowMediator is designed to be explicit and predictable,
not a full application framework.
Guarantees
SendAsyncis request/response: single handler, pipeline-enabled.PublishAsyncis event notification: multiple handlers, no response.
Non-Goals
- Durable messaging / exactly-once delivery guarantees
- Distributed transactions
- Automatic retries without idempotency guarantees
Contributions, bug reports, and feature requests are welcome.
Licensed under the MIT License.