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
<PackageReference Include="Josephan.CQRS.ResultExtensions" Version="3.0.1" />
<PackageVersion Include="Josephan.CQRS.ResultExtensions" Version="3.0.1" />
<PackageReference Include="Josephan.CQRS.ResultExtensions" />
paket add Josephan.CQRS.ResultExtensions --version 3.0.1
#r "nuget: Josephan.CQRS.ResultExtensions, 3.0.1"
#:package Josephan.CQRS.ResultExtensions@3.0.1
#addin nuget:?package=Josephan.CQRS.ResultExtensions&version=3.0.1
#tool nuget:?package=Josephan.CQRS.ResultExtensions&version=3.0.1
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
BindandMapoperations - 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<,>andIQueryPipelineBehavior<,>.
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 | Versions 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. |
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.