PouryaKit
Guides

Create a command

Add a Feature that changes state, with its command, validator and handler, and leave the commit to the Kit.

What you build

A Feature that changes state: a command record, an optional validator, and exactly one handler that changes aggregates and returns a Result. This guide follows the Proving Sample's RenameProduct Feature in Catalog's Products Business Area, Products/Features/RenameProduct/.

The handler never saves

No SaveChanges, transaction or Outbox call: the Unit of Work commits what the handler changed once it returns a success, and rolls it all back when it fails.

When to use it

For every change a caller asks a Module to make. A read is a query instead: see Answer a paged query.

Steps

Declare the command

A sealed record, named after its intent with the Command suffix. Command has no value; Command<TResponse> answers one. Keep it internal: callers outside the Module reach it through its Gateway. This one also names the cache entries it makes stale, which only matters with Caching.

RenameProductCommand.cs
internal sealed record RenameProductCommand(Guid ProductId, string Name)
    : Command,
        IInvalidatesCache
{
    public IReadOnlyCollection<string> InvalidatedCacheTags =>
        [GetProductQuery.ProductTag(ProductId)];
}

Check its input

One validator beside the command, for the shape of its input. A rule that depends on state belongs in the aggregate. See Validate a command or query.

RenameProductCommandValidator.cs
internal sealed class RenameProductCommandValidator : AbstractValidator<RenameProductCommand>
{
    public RenameProductCommandValidator()
    {
        RuleFor(command => command.ProductId).NotEmpty();
        RuleFor(command => command.Name).NotEmpty().MaximumLength(200);
    }
}

Write its one handler

Derive from CommandHandler<TCommand>, or CommandHandler<TCommand, TResponse> for a command with a value. Load the aggregate through its repository, change it through its methods, and return the Result: an Error when the business says no. See Operation, Result and Error.

RenameProductCommandHandler.cs
internal sealed class RenameProductCommandHandler(IRepository<Product, ProductId> products)
    : CommandHandler<RenameProductCommand>
{
    public override async Task<Result> Handle(
        RenameProductCommand command,
        CancellationToken cancellationToken
    )
    {
        var product = await products.FindAsync(new ProductId(command.ProductId), cancellationToken);
        return product is null ? ProductErrors.NotFound : product.Rename(command.Name);
    }
}

Register the Module's Features

The Module's Composition Root already calls AddPouryaKitCqrs() and AddPouryaKitValidation(). With no assembly named, each scans the Module's own assembly, so a new Feature needs no registration of its own.

Serve it and test it

Map an endpoint that sends the command through the Module's Gateway, as Add an endpoint shows, and test it as Test a Feature shows: assert on the Result's Errors, never on exceptions.

What the Kit guarantees

GuaranteeStated in
One handler per command, and what fails the Host when there is notCommands and queries
The fixed pipeline, and nested commandsThe pipeline
One commit or nothingWhat the Kit guarantees
How the repository tracks and when it refuses to writeThe repository

Failure messages

Next steps

On this page