Josephan.CQRS.FunctionalResults 1.0.7

dotnet add package Josephan.CQRS.FunctionalResults --version 1.0.7
                    
NuGet\Install-Package Josephan.CQRS.FunctionalResults -Version 1.0.7
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Josephan.CQRS.FunctionalResults" Version="1.0.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Josephan.CQRS.FunctionalResults" Version="1.0.7" />
                    
Directory.Packages.props
<PackageReference Include="Josephan.CQRS.FunctionalResults" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Josephan.CQRS.FunctionalResults --version 1.0.7
                    
#r "nuget: Josephan.CQRS.FunctionalResults, 1.0.7"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Josephan.CQRS.FunctionalResults@1.0.7
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Josephan.CQRS.FunctionalResults&version=1.0.7
                    
Install as a Cake Addin
#tool nuget:?package=Josephan.CQRS.FunctionalResults&version=1.0.7
                    
Install as a Cake Tool

Josephan.CQRS.FunctionalResults

Functional CQRS built around Result and Result<T>.

Instead of throwing exceptions for expected business failures, commands and queries return strongly typed results that flow through the pipeline as values. This makes failures explicit, composable, testable, and easy to map to HTTP responses.


Installation

dotnet add package Josephan.CQRS.FunctionalResults

Why Functional Results?

Traditional applications often use exceptions for both unexpected failures and expected business outcomes.

if (user is null)
    throw new UserNotFoundException();

With functional results, failures become part of the method contract:

if (user is null)
    return Error.NotFound(
        "User.NotFound",
        "User was not found");

Benefits:

  • Explicit success and failure paths
  • No exception-driven control flow
  • Easier testing
  • Better API contracts
  • Composable pipelines
  • Natural HTTP response mapping

Quick Start

Define a command:

public sealed record CreateUserCommand(
    string Name,
    string Email)
    : ICommand<Guid>;

Create a handler:

public sealed class CreateUserCommandHandler
    : ICommandHandler<CreateUserCommand, Guid>
{
    private readonly IUserRepository _repository;

    public CreateUserCommandHandler(
        IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<Result<Guid>> Handle(
        CreateUserCommand command,
        CancellationToken cancellationToken)
    {
        if (await _repository.ExistsAsync(command.Email))
        {
            return Error.Conflict(
                "User.AlreadyExists",
                "Email is already registered");
        }

        var user = new User(
            command.Name,
            command.Email);

        await _repository.AddAsync(user);

        return Result.Success(user.Id);
    }
}

Register services:

builder.Services.AddCQRS(options =>
{
    options.AddHandlersFromAssemblies(
        typeof(Program).Assembly);
});

Send a command:

var result = await sender.Send(
    new CreateUserCommand(
        "Ali",
        "ali@mail.com"));

if (result.IsFailure)
{
    return Results.BadRequest(result.Errors);
}

return Results.Ok(result.Value);

Core Concepts

Every operation returns one of two result types:

Type Description
Result Success or failure without a value
Result<T> Success or failure with a value

Failures are represented using the Error type.


Error

Error represents a failure.

Each error contains:

  • Code
  • Description
  • Type
public readonly record struct Error(
    string Code,
    string Description,
    ErrorType Type);

Built-In Errors

Error.None
Error.NullValue
Error.Unexpected
Error Description
None Represents success internally
NullValue A required value was null
Unexpected Unknown or unclassified failure

Creating Errors

Not Found

var error = Error.NotFound(
    "User.NotFound",
    "User was not found");

Validation

var error = Error.Validation(
    "User.InvalidEmail",
    "Email is invalid");

Conflict

var error = Error.Conflict(
    "User.AlreadyExists",
    "User already exists");

Failure

var error = Error.Failure(
    "User.SaveFailed",
    "Failed to save user");

Unauthorized

var error = Error.Unauthorized(
    "Auth.Unauthenticated",
    "User is not authenticated");

Forbidden

var error = Error.Forbidden(
    "Auth.Forbidden",
    "User lacks permissions");

Error Types

Type Meaning
Failure General application failure
Validation Validation or business rule failure
Conflict State conflict
NotFound Resource not found
Unauthorized Authentication required
Forbidden Permission denied
Unexpected Unknown failure

Result

Represents the outcome of an operation that returns no value.


Success

Result result = Result.Success();

Failure

Result result = Result.Failure(
    Error.NotFound(
        "User.NotFound",
        "User was not found"));

Multiple Errors

All errors must share the same ErrorType.

Result result = Result.Failure(
    Error.Validation(
        "Name.Required",
        "Name is required"),
    Error.Validation(
        "Email.Required",
        "Email is required"));

Implicit Conversions

Error → Result

Result result =
    Error.NotFound(
        "User.NotFound",
        "User was not found");

Error[] → Result

Result result = new[]
{
    Error.Validation(
        "Name.Required",
        "Name is required"),

    Error.Validation(
        "Email.Invalid",
        "Email is invalid")
};

Inspecting Results

if (result.IsSuccess)
{
}

if (result.IsFailure)
{
}

foreach (var error in result.Errors)
{
    Console.WriteLine(
        $"{error.Code}: {error.Description}");
}

Result<T>

Represents the outcome of an operation that returns a value.


Success

Result<Guid> result =
    Result.Success(Guid.NewGuid());

Failure

Result<Guid> result =
    Result.Failure<Guid>(
        Error.NotFound(
            "User.NotFound",
            "User not found"));

From Value (Recommended entry point for railway pipelines)

Result<User> result =
    Result.From(user);

Returns:

Error.NullValue

if the value is null.


Implicit Conversion

TValue → Result<T>

Result<string> result =
    "Hello";

Error → Result<T>

Result<Guid> result =
    Error.NotFound(
        "User.NotFound",
        "User was not found");

Accessing Values

if (result.IsSuccess)
{
    var value = result.Value;
}

Accessing Value on a failed result throws:

InvalidOperationException

Commands

Commands represent write operations.


Command Without Response

public sealed record DeleteUserCommand(
    Guid UserId)
    : ICommand;
public sealed class DeleteUserCommandHandler
    : ICommandHandler<DeleteUserCommand>
{
    private readonly IUserRepository _repository;

    public DeleteUserCommandHandler(
        IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<Result> Handle(
        DeleteUserCommand command,
        CancellationToken cancellationToken)
    {
        var user =
            await _repository.FindAsync(
                command.UserId);

        if (user is null)
        {
            return Error.NotFound(
                "User.NotFound",
                "User was not found");
        }

        await _repository.DeleteAsync(user);

        return Result.Success();
    }
}

Command With Response

public sealed record CreateUserCommand(
    string Name,
    string Email)
    : ICommand<Guid>;
public sealed class CreateUserCommandHandler
    : ICommandHandler<CreateUserCommand, Guid>
{
    private readonly IUserRepository _repository;

    public CreateUserCommandHandler(
        IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<Result<Guid>> Handle(
        CreateUserCommand command,
        CancellationToken cancellationToken)
    {
        if (await _repository.ExistsAsync(command.Email))
        {
            return Error.Conflict(
                "User.AlreadyExists",
                "Email is already registered");
        }

        var user = new User(
            command.Name,
            command.Email);

        await _repository.AddAsync(user);

        return Result.Success(user.Id);
    }
}

Queries

Queries represent read operations.

public sealed record GetUserQuery(
    Guid Id)
    : IQuery<UserDto>;
public sealed class GetUserQueryHandler
    : IQueryHandler<GetUserQuery, UserDto>
{
    private readonly IUserRepository _repository;

    public GetUserQueryHandler(
        IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<Result<UserDto>> Handle(
        GetUserQuery query,
        CancellationToken cancellationToken)
    {
        var user =
            await _repository.FindAsync(query.Id);

        if (user is null)
        {
            return Error.NotFound(
                "User.NotFound",
                "User was not found");
        }

        return Result.Success(
            new UserDto(
                user.Id,
                user.Name));
    }
}

Pipeline Behaviors

Behaviors work exactly like regular CQRS middleware.

Open-Generic Behavior

public sealed class LoggingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
    where TResponse : Result
{
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TRequest, TResponse> next,
        CancellationToken cancellationToken)
    {
        Console.WriteLine($"Handling {typeof(TReq).Name}");
        var response = await next(request, ct);
        Console.WriteLine($"Handled {typeof(TReq).Name}");
        return response;
    }
}

Closed-Generic Behavior

Runs only for a specific request/response pair — ideal for caching, auditing, or type-specific logic.

public class CachingBehavior : IPipelineBehavior<GetUserQuery, Result<UserDto>>
{
    public async Task<Result<UserDto>> Handle(
        GetUserQuery request,
        RequestHandlerDelegate<GetUserQuery, UserDto> next,
        CancellationToken ct)
    {
         // Implement Logic Here
    }
}

Registration:

services.AddCQRS(options =>
{
    options.AddHandlersFromAssemblies(typeof(Program).Assembly);

    // Open-Generic
    options.AddBehavior(typeof(LoggingBehavior<,>));

    // Closed-Generic
    options.AddBehavior<GetUserQuery, Result<UserDto>, CachingBehavior>();
    // Or simply
    options.AddBehavior<CachingBehavior>();

     // Command-only → Implelent ICommandPipelineBehavior 
    options.AddCommandBehavior(typeof(AuditBehavior<,>));

    // Query-only → Implelent IQueryPipelineBehavior 
    options.AddQueryBehavior(typeof(QueryCachingBehavior<,>));
});

Calling AddCQRS from Multiple Layers/Modules

It's safe to call AddCQRS more than once — for example, once from an Application layer module to register behaviors, and once from an Infrastructure layer module to register handlers:

// Application layer
services.AddCQRS(options =>
{
    options.AddBehavior(typeof(LoggingBehavior<,>));
    options.AddCommandBehavior(typeof(AuditBehavior<,>));
});

// Infrastructure layer
services.AddCQRS(options =>
{
    options.AddHandlersFromAssemblies(
        typeof(ApplicationAssemblyMarker).Assembly,
        typeof(InfrastructureAssemblyMarker).Assembly);
});

Handlers, behaviors, and pipeline decoration are all idempotent — no duplicate registrations, no double-wrapped pipelines, no matter how many modules call AddCQRS or in what order.

Notifications

Notifications allow broadcasting events to multiple handlers.


Define Notification

public sealed record UserCreatedNotification(
    Guid UserId,
    string Email)
    : INotification;

Handlers

public sealed class SendWelcomeEmailHandler
    : INotificationHandler<UserCreatedNotification>
{
    public Task Handle(
        UserCreatedNotification notification,
        CancellationToken cancellationToken)
    {
        Console.WriteLine(
            $"Sending email to {notification.Email}");

        return Task.CompletedTask;
    }
}
public sealed class AuditUserCreationHandler
    : INotificationHandler<UserCreatedNotification>
{
    public Task Handle(
        UserCreatedNotification notification,
        CancellationToken cancellationToken)
    {
        Console.WriteLine(
            $"Created user {notification.UserId}");

        return Task.CompletedTask;
    }
}

Publish Notification

await publisher.PublishAsync(
    new UserCreatedNotification(
        userId,
        email));

Publishing Strategies

Strategy Description
Sequential Default. Executes handlers one by one
Parallel Executes handlers concurrently
await publisher.PublishAsync(
    notification,
    NotificationPublishingStrategy.Parallel);

Railway-Oriented Pipeline

Composable operators for building workflows without deeply nested conditionals and repetitive success/failure checks.


Bind

Transforms a successful result into another result-producing operation. If the current result is a failure, the chain stops and the failure is propagated.

var result = await Result
    .From(command.Email)
    .Bind(ValidateEmail)
    .BindAsync(_repository.FindByEmailAsync)
    .BindAsync(ActivateUserAsync);

Map

Transforms the success value while preserving the result state. Failures pass through unchanged.

Result<UserDto> dto =
    await userResult.MapAsync(
        user => new UserDto(
            user.Id,
            user.Name));

Tap

Executes side effects on success without modifying the result. Useful for logging, caching, publishing events, or metrics.

await result
    .TapAsync(user =>
        _cache.SetAsync(
            user.Id,
            user))
    .TapAsync(user =>
        _publisher.PublishAsync(
            new UserActivatedEvent(
                user.Id)));

TapError

Executes side effects on failure without modifying the result. Useful for logging or monitoring failed operations.

result.TapError(error =>
{
    logger.LogWarning(
        "{Code}: {Description}",
        error.Code,
        error.Description);
});

Match

Handles both success and failure branches and produces a final value.

return result.Match(
    onSuccess: user =>
        Results.Ok(user),

    onFailure: failure =>
        Results.BadRequest(
            failure.Errors));

Ensure

Validates a value and converts validation failures into a result. Useful for enforcing business rules inside a pipeline.

var result = ResultExtensions.Ensure(
    command.Email,
    email => email.Contains('@'),
    Error.Validation(
        "Email.Invalid",
        "Email is invalid"));

Try

Executes code that may throw exceptions and converts exceptions into failures.

result.Try(
    value =>
        _externalService.Notify(value),

    ex =>
        Error.Failure(
            "Notify.Failed",
            ex.Message));

Finally

Executes a callback regardless of success or failure. The original result is returned unchanged.

await resultTask.FinallyAsync(result =>
{
    logger.LogInformation(
        "Completed. Success={Success}",
        result.IsSuccess);
});

Combine

Merges multiple results into a single result. If any operation fails, all errors are aggregated.

var result = ResultExtensions.Combine(
    ValidateName(name),
    ValidateEmail(email),
    ValidateAge(age));

Combine typed results into a tuple:

var result = ResultExtensions.Combine(
    userResult,
    orderResult);

Result:

Result<(User User, Order Order)>

Unit

Unit is a void-like success value.

public sealed class PingHandler
    : ICommandHandler<PingCommand>
{
    public async Task<Result> Handle(
        PingCommand command,
        CancellationToken cancellationToken)
    {
        await Task.Delay(
            100,
            cancellationToken);

        return Unit.Value;
    }
}

Fully Functional style (No Imperative style)

A complete handler written as a pure railway pipeline — no if, no early returns, no exception throws.

public sealed class CreateUserCommandHandler
    : ICommandHandler<CreateUserCommand, Guid>
{
    private readonly IUserRepository _repository;
    private readonly IPublisher _publisher;
    private readonly ILogger<CreateUserCommandHandler> _logger;

    public CreateUserCommandHandler(
        IUserRepository repository,
        IPublisher publisher,
        ILogger<CreateUserCommandHandler> logger)
    {
        _repository = repository;
        _publisher  = publisher;
        _logger     = logger;
    }

    public Task<Result<Guid>> Handle(
        CreateUserCommand command,
        CancellationToken cancellationToken)
    {
        return Result
            .From(command)                                   // Entry point — wraps value, guards null

            .Ensure(                                         // Validate email format
                cmd => cmd.Email.Contains('@'),
                Error.Validation(
                    "User.InvalidEmail",
                    "Email is invalid"))

            .Ensure(                                         // Validate name not empty
                cmd => !string.IsNullOrWhiteSpace(cmd.Name),
                Error.Validation(
                    "User.InvalidName",
                    "Name is required"))

            .BindAsync(cmd =>                                // Check for duplicate — stops chain on conflict
                _repository.ExistsAsync(cmd.Email)
                    ? Task.FromResult<Result<CreateUserCommand>>(
                        Error.Conflict(
                            "User.AlreadyExists",
                            "Email is already registered"))
                    : Task.FromResult(Result.Success(cmd)))

            .MapAsync(cmd =>                                 // Pure transformation — create the entity
                new User(cmd.Name, cmd.Email))

            .BindAsync(user =>                               // Persist — propagates storage errors
                _repository
                    .AddAsync(user)
                    .ContinueWith(_ => Result.Success(user)))

            .TapAsync(user =>                                // Side effect on success — publish event
                _publisher.PublishAsync(
                    new UserCreatedNotification(
                        user.Id,
                        user.Email),
                    cancellationToken))

            .TapError(errors =>                              // Side effect on failure — log errors
                _logger.LogWarning(
                    "CreateUser failed: {Errors}",
                    string.Join(", ", errors.Select(e => e.Code))))

            .MapAsync(user => user.Id)                       // Project — extract the Guid

            .FinallyAsync(result =>                          // Always runs — telemetry / cleanup
                _logger.LogInformation(
                    "CreateUser completed. Success={Success}",
                    result.IsSuccess));
    }
}

Wire it up in the endpoint:

app.MapPost("/users", async (CreateUserCommand command, ISender sender) =>
{
    return await Result
        .From(command)
        .BindAsync(_ => sender.Send(command))
        .MatchAsync(
            onSuccess: id =>
                Results.Created($"/users/{id}", new { Id = id }),

            onFailure: failure =>
                failure.Errors.First().Type switch
                {
                    ErrorType.Validation => Results.BadRequest(failure.Errors),
                    ErrorType.Conflict => Results.Conflict(),
                    ErrorType.NotFound => Results.NotFound(),
                    ErrorType.Unauthorized => Results.Unauthorized(),
                    ErrorType.Forbidden => Results.StatusCode(403),
                    _ => Results.StatusCode(500)
                });
});

Each operator's role at a glance:

Step Operator Purpose
1 Result.From Entry point — wraps raw value, auto-fails on null
2–3 Ensure Business rule validation before any I/O
4 BindAsync Duplicate check — short-circuits on conflict
5 MapAsync Pure entity construction — no failure possible
6 BindAsync Persist — propagates repository errors
7 TapAsync Publish domain event — success side effect only
8 TapError Log failure — error side effect only
9 MapAsync Project entity → return value (Guid)
10 FinallyAsync Telemetry / cleanup — always runs regardless

The failure from any step short-circuits all remaining Bind/Map/Tap steps and flows straight to TapError → FinallyAsync → Match.


HTTP Response Mapping

A common ASP.NET Core pattern:

return result.Match(
    onSuccess: value =>
        Results.Ok(value),

    onFailure: failure =>
        failure.Errors.First().Type switch
        {
            ErrorType.Validation =>
                Results.BadRequest(failure.Errors),

            ErrorType.NotFound =>
                Results.NotFound(),

            ErrorType.Conflict =>
                Results.Conflict(),

            ErrorType.Unauthorized =>
                Results.Unauthorized(),

            ErrorType.Forbidden =>
                Results.StatusCode(403),

            _ =>
                Results.StatusCode(500)
        });

License

MIT © Yousef Yahia

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Josephan.CQRS.FunctionalResults:

Package Downloads
Josephan.CQRS.FunctionalResults.NewtonsoftJson

Newtonsoft.Json converters for Josephan.CQRS Functional Results. Provides serialization and deserialization support for Result, Result<T>, and Error with a single extension method for JsonSerializerSettings.

Josephan.CQRS.FunctionalResults.SystemTextJson

System.Text.Json converters for Josephan.CQRS Functional Results. Provides serialization and deserialization support for Result, Result<T>, and Error with a single extension method for JsonSerializerOptions.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.7 180 7/16/2026
1.0.6 113 7/14/2026
1.0.5 136 7/9/2026
1.0.4 110 7/1/2026
1.0.3 118 6/30/2026
1.0.2 126 6/26/2026
1.0.1 147 6/24/2026 1.0.1 is deprecated because it has critical bugs.
1.0.0 119 6/23/2026