Josephan.CQRS.ResultExtensions 3.0.1

dotnet add package Josephan.CQRS.ResultExtensions --version 3.0.1
                    
NuGet\Install-Package Josephan.CQRS.ResultExtensions -Version 3.0.1
                    
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.ResultExtensions" Version="3.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Josephan.CQRS.ResultExtensions" Version="3.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Josephan.CQRS.ResultExtensions" />
                    
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.ResultExtensions --version 3.0.1
                    
#r "nuget: Josephan.CQRS.ResultExtensions, 3.0.1"
                    
#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.ResultExtensions@3.0.1
                    
#: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.ResultExtensions&version=3.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Josephan.CQRS.ResultExtensions&version=3.0.1
                    
Install as a Cake Tool

Josephan.CQRS.ResultExtensions

A lightweight, high-performance CQRS library for .NET with a built-in Result pattern, pipeline behaviors, and functional utilities.

Installation

dotnet add package Josephan.CQRS.ResultExtensions

Looking for a More Powerful Result Type?

The built-in Result and Result<T> types are intentionally lightweight, and designed to cover the vast majority of CQRS applications.

For many projects, they are all you'll ever need.

However, as business workflows become more complex, you may find yourself writing additional validation logic, manually composing operations, or repeatedly propagating errors through your application.

If your project heavily embraces functional programming or Railway-Oriented Programming (ROP), consider Josephan.CQRS.FunctionalResults — a more powerful Result implementation designed to make complex workflows significantly easier to express and maintain.

Features

  • Multiple errors per result
  • Rich validation support
  • Railway-Oriented Programming (ROP) operators
  • Fluent result composition
  • Synchronous and asynchronous Bind and Map operations
  • More expressive error propagation
  • Reduced boilerplate in complex business workflows
var result = await Result
    .From(command.Email)
    .Bind(ValidateEmail)
    .BindAsync(_repository.FindByEmailAsync)
    .BindAsync(ActivateUserAsync);

Important

Josephan.CQRS.FunctionalResults is an alternative Result implementation, not an extension of the built-in Result package.

Before migrating, remove the current package and install FunctionalResults instead:

dotnet remove package Josephan.CQRS.ResultExtensions
dotnet add package Josephan.CQRS.FunctionalResults

📦 NuGet: https://www.nuget.org/packages/Josephan.CQRS.FunctionalResults/


Getting Started

Register Services

services.AddCQRS(typeof(Program).Assembly, cfg =>
{
    cfg.AddBehavior(typeof(ValidationBehavior<,>));
    cfg.AddBehavior(typeof(LoggingBehavior<,>));
});

Result Pattern

All commands and queries return a Result or Result<T> indicating success or failure.

Result

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

Result<T>

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

Error

Error error = Error.Failure("Order.Invalid", "Order is invalid.");
Error error = Error.NotFound("User.NotFound", "User was not found.");
Error error = Error.Conflict("User.Exists", "User already exists.");
Error error = Error.Unauthorized("Auth.Unauthorized", "Authentication is required.");
Error error = Error.Forbidden("Auth.Forbidden", "You do not have permission.");

ValidationError

var errors = new[]
{
    Error.Failure("Name.Empty", "Name is required."),
    Error.Failure("Email.Invalid", "Email is not valid.")
};

var validationError = new ValidationError(errors);

Unit Type

Represents a void-like success result for command handlers that don't need to return a value. Unit.Value implicitly converts to Result.Success(), so both are equivalent:

Usage

// these are identical at runtime
return Result.Success();
return Unit.Value;      // preferred — signals intentional void return

Commands

Define a Command

public record CreateUserCommand(string Name, string Email) : ICommand;

Handle a Command

public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand>
{
    public Task<Result> Handle(CreateUserCommand command, CancellationToken ct)
    {
        return Task.FromResult(Result.Success());
    }
}

Command with Response

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

public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, Guid>
{
    public Task<Result<Guid>> Handle(CreateUserCommand command, CancellationToken ct)
    {
        return Task.FromResult(Result.Success(Guid.NewGuid()));
    }
}

Send a Command

public class UserService(ICommandSender sender)
{
    public async Task CreateUser(string name, string email)
    {
        Result result = await sender.Send(new CreateUserCommand(name, email));

        if (result.IsFailure)
            Console.WriteLine(result.Error.Description);
    }
}

Queries

Define a Query

public record GetUserQuery(Guid Id) : IQuery<UserResponse>;

Handle a Query

public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserResponse>
{
    public Task<Result<UserResponse>> Handle(GetUserQuery query, CancellationToken ct)
    {
        var user = new UserResponse(query.Id, "Yousef");
        return Task.FromResult(Result.Success(user));
    }
}

Send a Query

public class UserService(IQuerySender sender)
{
    public async Task<UserResponse?> GetUser(Guid id)
    {
        Result<UserResponse> result = await sender.Send(new GetUserQuery(id));

        return result.IsSuccess ? result.Value : null;
    }
}

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, 
    publishingStrategy: NotificationPublishingStrategy.Parallel);


Pipeline Behaviors

Define a Behavior

public 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 ct)
    {
        Console.WriteLine($"Handling {typeof(TRequest).Name}");
        var response = await next(request, ct);
        Console.WriteLine($"Handled {typeof(TRequest).Name}");
        return response;
    }
}

Register Behaviors

services.AddCQRS(typeof(Program).Assembly, cfg =>
{
    cfg.AddBehavior(typeof(ValidationBehavior<,>));
    cfg.AddBehavior(typeof(LoggingBehavior<,>));
});

Closed-Generic Behavior

services.AddCQRS(typeof(Program).Assembly, cfg =>
{
    cfg.AddBehavior<GetUserQuery, Result<UserResponse>, CachingBehavior>();
});

Command-Only Behavior

public class AuditBehavior<TRequest, TResponse>
    : ICommandPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
    where TResponse : Result
{
    public async Task<TResponse> Handle(
        TRequest command,
        RequestHandlerDelegate<TRequest, TResponse> next,
        CancellationToken ct)
    {
        return await next(command, ct);
    }
}
cfg.AddCommandBehavior(typeof(AuditBehavior<,>));

Query-Only Behavior

public class CachingBehavior<TRequest, TResponse>
    : IQueryPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
    where TResponse : Result
{
    public async Task<TResponse> Handle(
        TRequest query,
        RequestHandlerDelegate<TRequest, TResponse> next,
        CancellationToken ct)
    {
        return await next(query, ct);
    }
}
cfg.AddQueryBehavior(typeof(CachingBehavior<,>));

A behavior cannot implement both ICommandPipelineBehavior<,> and IQueryPipelineBehavior<,>.

Tips & Tricks

Code Style Comparison

❌ Verbose Style (manual error propagation)
var fnameResult = FirstName.Create(command.FirstName);
if (fnameResult.IsFailure)
    return fnameResult.Error;

var lnameResult = LastName.Create(command.LastName);
if (lnameResult.IsFailure)
    return lnameResult.Error;

var emailResult = Email.Create(command.Email);
if (emailResult.IsFailure)
    return emailResult.Error;
✅ Functional Style (composed results)
Result result = Result.FirstFailureOrSuccess(
    FirstName.Create(command.FirstName),
    LastName.Create(command.LastName),
    Email.Create(command.Email)
);

Returning a Failure Result from a Generic TResponse

When building pipeline behaviors, you may need to return a failure result without knowing whether TResponse is Result or Result<T> at compile time. Use this helper:

private static TResult CreateFailureResult<TResult>(Error error)
    where TResult : Result
{
    if (typeof(TResult) == typeof(Result))
        return (TResult)Result.Failure(error);

    var result = typeof(TResult)
        .GetGenericTypeDefinition()
        .MakeGenericType(typeof(TResult).GenericTypeArguments[0])
        .GetMethod(nameof(Result<>.Failure))!
        .Invoke(null, [error]);

    return (TResult)result!;
}

Example — Validation Behavior

public class ValidationBehavior<TRequest, TResponse>
    : ICommandPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
    where TResponse : Result
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
        => _validators = validators;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TRequest, TResponse> next,
        CancellationToken ct)
    {
        if (!_validators.Any())
            return await next(request, ct);

        var errors = _validators
            .Select(v => v.Validate(request))
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .Select(f =>
            {
                var camelCase = char.ToLower(f.PropertyName[0]) + f.PropertyName[1..];
                return Error.Failure(camelCase, f.ErrorMessage);
            })
            .ToArray();

        if (errors.Length == 0)
            return await next(request, ct);

        return CreateFailureResult<TResponse>(new ValidationError(errors));
    }

    private static TResult CreateFailureResult<TResult>(Error error)
        where TResult : Result
    {
        if (typeof(TResult) == typeof(Result))
            return (TResult)Result.Failure(error);

        var result = typeof(TResult)
            .GetGenericTypeDefinition()
            .MakeGenericType(typeof(TResult).GenericTypeArguments[0])
            .GetMethod(nameof(Result<>.Failure))!
            .Invoke(null, [error]);

        return (TResult)result!;
    }
}

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.1 135 7/14/2026
3.0.0 116 6/23/2026
2.0.4 134 5/28/2026
2.0.3 116 5/22/2026
2.0.2 111 5/21/2026
2.0.1 113 5/12/2026
2.0.0 118 5/12/2026 2.0.0 is deprecated because it has critical bugs.
1.0.0 117 5/12/2026 1.0.0 is deprecated because it has critical bugs.