A lightweight, delegate-based CQRS framework for .NET that provides command/query separation with pipeline behaviors and notification support.
$ dotnet add package DispatchlyA lightweight, delegate-based CQRS framework for .NET that provides command/query separation with pipeline behaviors and notification support.
ConcurrentDictionary and ConcurrentBag for thread safetyCancellationToken support throughoutdotnet add package Dispatchly
using Dispatchly;
// Create dispatcher
var dispatcher = new Dispatcher();
// Register command handler
dispatcher.RegisterCommandHandler<CreateUserCommand>(async (command, cancellationToken) =>
{
// Handle the command
await CreateUserAsync(command, cancellationToken);
});
// Register query handler
dispatcher.RegisterQueryHandler<GetUserQuery, User>(async (query, cancellationToken) =>
{
// Handle the query
return await GetUserAsync(query.UserId, cancellationToken);
});
// Send a command
await dispatcher.Send(new CreateUserCommand { Name = "John", Email = "john@example.com" });
// Execute a query
var user = await dispatcher.Query(new GetUserQuery { UserId = 123 });
// Logging behavior
dispatcher.UseCommandBehavior<CreateUserCommand>(async (command, next, cancellationToken) =>
{
Console.WriteLine($"Executing command: {command.GetType().Name}");
try
{
await next();
Console.WriteLine("Command executed successfully");
}
catch (Exception ex)
{
Console.WriteLine($"Command failed: {ex.Message}");
throw;
}
});
// Validation behavior
dispatcher.UseQueryBehavior<GetUserQuery, User>(async (query, next, cancellationToken) =>
{
if (query.UserId <= 0)
throw new ArgumentException("User ID must be positive");
return await next();
});
// Register notification handlers
dispatcher.RegisterNotificationHandler<UserCreatedEvent>(async (notification, cancellationToken) =>
{
await SendWelcomeEmailAsync(notification.UserId, cancellationToken);
});
dispatcher.RegisterNotificationHandler<UserCreatedEvent>(async (notification, cancellationToken) =>
{
await UpdateAuditLogAsync(notification, cancellationToken);
});
// Publish notification (multiple handlers will execute)
await dispatcher.Publish(new UserCreatedEvent { UserId = 123, Name = "John" });
try
{
await dispatcher.Send(new CreateUserCommand { Name = "John" });
}
catch (HandlerNotFoundException ex)
{
Console.WriteLine($"No handler found for {ex.MessageType.Name}");
}
catch (ArgumentNullException ex)
{
Console.WriteLine($"Null argument: {ex.ParamName}");
}ICommand - Marker interface for commandsIQuery<TResult> - Marker interface for queriesINotification - Marker interface for notificationsCommandHandlerDelegate<TCommand> - Handles commandsQueryHandlerDelegate<TQuery, TResult> - Handles queriesCommandPipelineDelegate<TCommand> - Command pipeline behaviorQueryPipelineDelegate<TQuery, TResult> - Query pipeline behaviorNotificationHandlerDelegate<TNotification> - Handles notificationsRegisterCommandHandler<TCommand>() - Register command handlerRegisterQueryHandler<TQuery, TResult>() - Register query handlerRegisterNotificationHandler<TNotification>() - Register notification handlerUseCommandBehavior<TCommand>() - Add command pipeline behaviorUseQueryBehavior<TQuery, TResult>() - Add query pipeline behaviorSend<TCommand>() - Send commandQuery<TQuery, TResult>() - Execute queryPublish<TNotification>() - Publish notificationHasCommandHandler<TCommand>() - Check if command handler existsHasQueryHandler<TQuery>() - Check if query handler existsHasNotificationHandlers<TNotification>() - Check if notification handlers existCommandHandlerCount - Number of registered command handlersQueryHandlerCount - Number of registered query handlersNotificationHandlerCount - Number of registered notification handlersCommandBehaviorCount - Number of registered command behaviorsQueryBehaviorCount - Number of registered query behaviorsservices.AddSingleton<Dispatcher>();public async Task<Result<User>> CreateUserAsync(CreateUserCommand command)
{
try
{
await _dispatcher.Send(command);
return Result<User>.Success();
}
catch (HandlerNotFoundException)
{
return Result<User>.Failure("Handler not configured");
}
catch (Exception ex)
{
return Result<User>.Failure(ex.Message);
}
}public async Task<User> GetUserAsync(int userId, CancellationToken cancellationToken = default)
{
return await _dispatcher.Query(new GetUserQuery { UserId = userId }, cancellationToken);
}// Logging
dispatcher.UseCommandBehavior<CreateUserCommand>(async (command, next, cancellationToken) =>
{
_logger.LogInformation("Executing {CommandType}", command.GetType().Name);
await next();
_logger.LogInformation("Executed {CommandType}", command.GetType().Name);
});
// Validation
dispatcher.UseCommandBehavior<CreateUserCommand>(async (command, next, cancellationToken) =>
{
var validator = new CreateUserCommandValidator();
var result = await validator.ValidateAsync(command, cancellationToken);
if (!result.IsValid)
throw new ValidationException(result.Errors);
await next();
});public class MyService : IDisposable
{
private readonly Dispatcher _dispatcher;
public MyService(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public void Dispose()
{
_dispatcher?.Dispose();
}
}MIT License - see LICENSE file for details.