diff --git a/CQRS.slnx b/CQRS.slnx
new file mode 100644
index 0000000..b4c46ba
--- /dev/null
+++ b/CQRS.slnx
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/.gitkeep b/lib/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/Logitar.CQRS/CommandBus.cs b/lib/Logitar.CQRS/CommandBus.cs
new file mode 100644
index 0000000..98166a7
--- /dev/null
+++ b/lib/Logitar.CQRS/CommandBus.cs
@@ -0,0 +1,195 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Logitar.CQRS;
+
+///
+/// Represents an in-memory bus in which sent commands are executed synchronously.
+///
+public class CommandBus : ICommandBus
+{
+ ///
+ /// The name of the command handler method.
+ ///
+ protected const string HandlerName = nameof(ICommandHandler<,>.HandleAsync);
+
+ ///
+ /// Gets the logger.
+ ///
+ protected virtual ILogger? Logger { get; }
+ ///
+ /// Gets the pseudo-random number generator.
+ ///
+ protected virtual Random Random { get; } = new();
+ ///
+ /// Gets the service provider.
+ ///
+ protected virtual IServiceProvider ServiceProvider { get; }
+ ///
+ /// Gets the retry settings.
+ ///
+ protected virtual RetrySettings Settings { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The service provider.
+ public CommandBus(IServiceProvider serviceProvider)
+ {
+ Logger = serviceProvider.GetService>();
+ ServiceProvider = serviceProvider;
+ Settings = serviceProvider.GetService() ?? new();
+ }
+
+ ///
+ /// Executes the specified command.
+ ///
+ /// The type of the command result.
+ /// The command to execute.
+ /// The cancellation token.
+ /// The command result.
+ /// The handler did not define the handle method, or it did not return a task.
+ public virtual async Task ExecuteAsync(ICommand command, CancellationToken cancellationToken)
+ {
+ Settings.Validate();
+
+ object handler = await GetHandlerAsync(command, cancellationToken);
+
+ Type handlerType = handler.GetType();
+ Type commandType = command.GetType();
+ Type[] parameterTypes = [commandType, typeof(CancellationToken)];
+ MethodInfo handle = handlerType.GetMethod(HandlerName, parameterTypes)
+ ?? throw new InvalidOperationException($"The handler {handlerType} must define a '{HandlerName}' method.");
+
+ object[] parameters = [command, cancellationToken];
+ Exception? innerException;
+ int attempt = 0;
+ while (true)
+ {
+ attempt++;
+ try
+ {
+ object? result = handle.Invoke(handler, parameters);
+ if (result is not Task task)
+ {
+ throw new InvalidOperationException($"The handler {handlerType} {HandlerName} method must return a {nameof(Task)}.");
+ }
+ return await task;
+ }
+ catch (Exception exception)
+ {
+ if (!ShouldRetry(command, exception))
+ {
+ throw;
+ }
+ innerException = exception;
+
+ int millisecondsDelay = CalculateMillisecondsDelay(command, exception, attempt);
+ if (millisecondsDelay < 0)
+ {
+ throw new InvalidOperationException($"The retry delay '{millisecondsDelay}' should be greater than or equal to 0ms.");
+ }
+
+ if (Settings.Algorithm == RetryAlgorithm.None
+ || (Settings.MaximumRetries > 0 && attempt > Settings.MaximumRetries)
+ || (Settings.MaximumDelay > 0 && millisecondsDelay > Settings.MaximumDelay))
+ {
+ break;
+ }
+
+ if (Logger is not null && Logger.IsEnabled(LogLevel.Warning))
+ {
+ Logger.LogWarning(exception, "Command '{Command}' execution failed at attempt {Attempt}, will retry in {Delay}ms.", commandType, attempt, millisecondsDelay);
+ }
+
+ if (millisecondsDelay > 0)
+ {
+ await Task.Delay(millisecondsDelay, cancellationToken);
+ }
+ }
+ }
+
+ throw new InvalidOperationException($"Command '{commandType}' execution failed after {attempt} attempts. See inner exception for more detail.", innerException);
+ }
+
+ ///
+ /// Finds the handler for the specified command.
+ ///
+ /// The type of the command result.
+ /// The command.
+ /// The cancellation token.
+ /// The command handler.
+ /// There is no handler or many handlers for the specified command.
+ protected virtual async Task