This guide walks through the shortest path from nothing to dispatching your first command. It assumes you have the .NET 10 SDK installed.
dotnet add package SharpDispatchSharpDispatch depends only on Microsoft.Extensions.DependencyInjection.Abstractions,
so it stays lightweight. If you want to build a container manually (as shown in
this guide) also reference the full DI package:
dotnet add package Microsoft.Extensions.DependencyInjectionASP.NET Core projects already include the full DI package through the shared framework, so no extra package is required there.
A command is any immutable object that implements the empty marker interface
ICommand. A handler implements ICommandHandler<TCommand> for exactly one
command type.
using SharpDispatch;
// ── Command ─────────────────────────────────────────────────────────────────
public sealed record CreateOrderCommand(string OrderId, decimal Amount) : ICommand;
// ── Handler ─────────────────────────────────────────────────────────────────
public sealed class CreateOrderCommandHandler(IOrderRepository repository)
: ICommandHandler<CreateOrderCommand>
{
public Task<CommandDispatchResult> HandleAsync(
CreateOrderCommand command,
CancellationToken cancellationToken)
{
if (command.Amount <= 0)
{
return Task.FromResult(CommandDispatchResult.Fail("Amount must be positive."));
}
repository.Save(new Order(command.OrderId, command.Amount));
return Task.FromResult(CommandDispatchResult.Ok($"Order {command.OrderId} created."));
}
}Notes:
- Commands can be classes or structs. Structs avoid boxing on the optimized dispatch path (see Advanced Patterns).
- Handlers are async-first:
HandleAsyncreturnsTask<CommandDispatchResult>and receives aCancellationToken. - Return
CommandDispatchResult.Ok(...)on success andCommandDispatchResult.Fail(...)on a handled failure.
using Microsoft.Extensions.DependencyInjection;
using SharpDispatch;
var services = new ServiceCollection();
// (a) Register the handler. Default lifetime: Singleton.
services.AddCommandHandler<CreateOrderCommand, CreateOrderCommandHandler>();
// (b) Register a dispatcher. Choose ONE:
services.AddCommandDispatcher(); // simple, DI-per-call
// services.AddOptimizedCommandDispatcher(cfg => // high throughput
// cfg.AddHandler<CreateOrderCommand, CreateOrderCommandHandler>());
var provider = services.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<ICommandDispatcher>();
// (c) Dispatch!
var result = await dispatcher.DispatchAsync(
new CreateOrderCommand("ORD-001", 49.99m));
Console.WriteLine(result.IsSuccess ? result.Message : $"Failed: {result.Message}");var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IOrderRepository, InMemoryOrderRepository>();
builder.Services.AddCommandHandler<CreateOrderCommand, CreateOrderCommandHandler>();
builder.Services.AddCommandDispatcher();
var app = builder.Build();
app.MapPost("/orders", async (
CreateOrderCommand command,
ICommandDispatcher dispatcher,
CancellationToken ct) =>
{
var result = await dispatcher.DispatchAsync(command, ct);
return result.IsSuccess
? Results.Ok(result)
: Results.BadRequest(result.Message);
});
app.Run();A complete, runnable version lives in examples/MinimalApi.
- Run the ready-made examples in this repository:
- Choose the right dispatcher: Dispatchers.
- Learn lifetime and scoping rules: Advanced Patterns.