Table of Contents

Result Pattern

The Nexum.Results package provides an explicit error-handling primitive: Result<T, TError>. Results are opt-in — you can mix them freely with exception-based handlers in the same application.

The type

public readonly record struct Result<T, TError>
{
    public bool IsSuccess { get; }
    public T Value { get; }      // valid only if IsSuccess
    public TError Error { get; } // valid only if !IsSuccess

    public static Result<T, TError> Success(T value);
    public static Result<T, TError> Failure(TError error);

    public TOut Match<TOut>(Func<T, TOut> onSuccess, Func<TError, TOut> onFailure);
}

A convenience alias defaults the error type to NexumError:

public readonly record struct Result<T>
{
    // same shape, TError = NexumError
}

NexumError is a lightweight error type with a Code, Message, and optional Metadata dictionary. Use it when you don't need a project-specific error hierarchy.

Using results in handlers

public record CreateOrderCommand(string CustomerId, List<string> Items) : ICommand<Result<Guid>>;

[CommandHandler]
public sealed class CreateOrderHandler : ICommandHandler<CreateOrderCommand, Result<Guid>>
{
    private readonly IOrderRepository _repo;
    public CreateOrderHandler(IOrderRepository repo) => _repo = repo;

    public async ValueTask<Result<Guid>> HandleAsync(CreateOrderCommand command, CancellationToken ct)
    {
        if (command.Items.Count == 0)
            return Result<Guid>.Failure(new NexumError("order.empty", "At least one item is required"));

        var order = Order.Create(command.CustomerId, command.Items);
        await _repo.SaveAsync(order, ct);
        return Result<Guid>.Success(order.Id);
    }
}

Matching results

var result = await dispatcher.DispatchAsync(new CreateOrderCommand(...), ct);

return result.Match(
    onSuccess: id => Results.Created($"/orders/{id}", new { Id = id }),
    onFailure: err => Results.Problem(err.Message, statusCode: 400));

FluentValidation integration

The Nexum.Results.FluentValidation package adds a pipeline behavior that runs FluentValidation validators and converts failures into Result<T>.Failure(...) automatically — without throwing.

services.AddNexum();
services.AddNexumResultsFluentValidation();
services.AddValidatorsFromAssemblyContaining<CreateOrderCommandValidator>();
public sealed class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(c => c.CustomerId).NotEmpty();
        RuleFor(c => c.Items).NotEmpty();
    }
}

When the command returns a Result<T>, the behavior short-circuits to Result<T>.Failure on validation failure. For commands that do not return a Result<T>, the behavior falls back to throwing a NexumValidationException.

Composition over inheritance

Nexum's result type is a readonly record struct — zero-allocation, equality by value, no inheritance hierarchy. This is intentional: results compose (Bind, Map, Match) rather than extending a base class.