Overview
Savvyio.Commands supplies the command-side building blocks for CQRS in Savvy I/O. It extends Savvyio.Core with a concrete Command base record, a fire-and-forget CommandDispatcher, and a CommandHandler base class that lets consumers register delegates for specific command types.
Use this package when commands stay in-process and you want explicit command models plus explicit handler registration. Transport-specific scaling concerns are intentionally left to sibling packages such as Savvyio.Commands.Messaging.
Key APIs
Command is the abstract base record for concrete commands. Its constructor assigns a new correlation identifier and merges any incoming metadata, while the inherited Request base from Savvyio.Core has already populated member-type metadata for the concrete command type.
CommandDispatcher.Commit and CommandDispatcher.CommitAsync dispatch an ICommand through the fire-and-forget pipeline exposed by ICommandHandler. Both methods validate that the request is non-null, and the underlying dispatcher throws OrphanedHandlerException when no resolved handler can invoke the command.
CommandHandler is the abstract handler base for command processing. Its constructor builds the Delegates activator through HandlerFactory.CreateFireForget<ICommand>, and consumers implement RegisterDelegates(IFireForgetRegistry<ICommand> handlers) to map concrete command types to synchronous or asynchronous delegates.
SavvyioOptionsExtensions.AddCommandHandler<TImplementation> records an ICommandHandler implementation in SavvyioOptions. The generic constraint requires a concrete class that implements ICommandHandler, which keeps handler registration explicit.
SavvyioOptionsExtensions.AddCommandDispatcher records the default ICommandDispatcher to CommandDispatcher mapping in SavvyioOptions. It is the package's built-in composition hook for adding command dispatch to a wider Savvy I/O setup.
Basic usage
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Codebelt.Extensions.Xunit;
using Savvyio.Commands;
using Savvyio.Dispatchers;
using Savvyio.Handlers;
using Xunit;
namespace MyProject.Tests;
public class SavvyioCommandsTests : Test
{
public SavvyioCommandsTests(ITestOutputHelper output) : base(output) { }
[Fact]
public async Task Commit_MissingHandler_ThrowsOrphanedHandlerException()
{
var dispatcher = new CommandDispatcher(new EmptyServiceLocator());
var command = new ArchiveOrderCommand();
var sync = Assert.Throws<OrphanedHandlerException>(() => dispatcher.Commit(command));
var async = await Assert.ThrowsAsync<OrphanedHandlerException>(() => dispatcher.CommitAsync(command));
TestOutput.WriteLine(sync.Message);
TestOutput.WriteLine(async.Message);
Assert.Contains(nameof(ArchiveOrderCommand), sync.Message, StringComparison.Ordinal);
}
}
public sealed record ArchiveOrderCommand : Command;
public sealed class EmptyServiceLocator : IServiceLocator
{
public IEnumerable<object> GetServices(Type serviceType) => Array.Empty<object>();
}
Use this pattern when you want the dispatcher to surface missing command registrations immediately during composition or test setup. It matters because the package fails fast instead of silently ignoring commands that have no matching handler.
Installation
dotnet add package Savvyio.Commands
Usage guidance
Choose Savvyio.Commands when you need explicit command types, delegate-based command handlers, and fire-and-forget dispatch inside the current process, especially when a higher-level composition package will turn SavvyioOptions registrations into container wiring. If you only need the shared request and metadata abstractions, Savvyio.Core is the smaller starting point, and if commands must be transported to distributed subsystems, Savvyio.Commands.Messaging is the better boundary.
Family packages
- 🏭Savvyio.App
- 📦Savvyio.Commands.Messaging
- 📦Savvyio.Core
- 📦Savvyio.Domain
- 📦Savvyio.Domain.EventSourcing
- 📦Savvyio.EventDriven
- 📦Savvyio.EventDriven.Messaging
- 📦Savvyio.Extensions.Dapper
- 📦Savvyio.Extensions.DapperExtensions
- 📦Savvyio.Extensions.DependencyInjection
- 📦Savvyio.Extensions.DependencyInjection.Dapper
- 📦Savvyio.Extensions.DependencyInjection.DapperExtensions
- 📦Savvyio.Extensions.DependencyInjection.Domain
- 📦Savvyio.Extensions.DependencyInjection.EFCore
- 📦Savvyio.Extensions.DependencyInjection.EFCore.Domain
- 📦Savvyio.Extensions.DependencyInjection.EFCore.Domain.EventSourcing
- 📦Savvyio.Extensions.DependencyInjection.NATS
- 📝Savvyio.Extensions.DependencyInjection.Newtonsoft.Json
- 📦Savvyio.Extensions.DependencyInjection.QueueStorage
- 📦Savvyio.Extensions.DependencyInjection.RabbitMQ
- 📦Savvyio.Extensions.DependencyInjection.SimpleQueueService
- 📝Savvyio.Extensions.DependencyInjection.Text.Json
- 📦Savvyio.Extensions.Dispatchers
- 📦Savvyio.Extensions.EFCore
- 📦Savvyio.Extensions.EFCore.Domain
- 📦Savvyio.Extensions.EFCore.Domain.EventSourcing
- 📦Savvyio.Extensions.NATS
- 📝Savvyio.Extensions.Newtonsoft.Json
- 📦Savvyio.Extensions.QueueStorage
- 📦Savvyio.Extensions.RabbitMQ
- 📦Savvyio.Extensions.SimpleQueueService
- 📝Savvyio.Extensions.Text.Json
- 📦Savvyio.Messaging
- 📦Savvyio.Queries