Kovecses.Result
2.11.0
dotnet add package Kovecses.Result --version 2.11.0
NuGet\Install-Package Kovecses.Result -Version 2.11.0
<PackageReference Include="Kovecses.Result" Version="2.11.0" />
<PackageVersion Include="Kovecses.Result" Version="2.11.0" />
<PackageReference Include="Kovecses.Result" />
paket add Kovecses.Result --version 2.11.0
#r "nuget: Kovecses.Result, 2.11.0"
#:package Kovecses.Result@2.11.0
#addin nuget:?package=Kovecses.Result&version=2.11.0
#tool nuget:?package=Kovecses.Result&version=2.11.0
Kovecses.Result
A lightweight, functional, and robust Result pattern implementation for .NET 8, 9, and 10 with seamless ASP.NET Core integration.
Support the Project
If you find this library useful, please give it a star on GitHub! It helps more developers discover the project. ⭐
Table of Contents
- Introduction
- Installation
- Core Library (Kovecses.Result)
- ASP.NET Core Integration
- Testing Support (Fluent Assertions)
Installation
Install the packages via NuGet:
# Core Library
dotnet add package Kovecses.Result
# ASP.NET Core Integration
dotnet add package Kovecses.Result.AspNetCore
# Fluent Assertions for Testing
dotnet add package Kovecses.Result.FluentAssertions
1. Introduction
The Result pattern encapsulates the outcome of an operation. Instead of relying on exceptions for flow control, methods return a Result object that explicitly indicates success or failure.
Why use this library?
- High Performance: Optimized generic factories for MediatR-style pipelines and no expensive exception overhead.
- Type Safety: Flat, unified error structure avoiding key collisions in validation.
- Standard Compliant: Native support for RFC 7807 (Problem Details) and
ValidationProblemDetails. - Railway-Oriented: Build clean, declarative processing pipelines using
Match,Map, andBind.
2. Core Library (Kovecses.Result)
The core library contains the fundamental types and functional extensions. It supports multiple errors per result and implicit conversions.
Basic Usage
The library supports powerful implicit conversions to reduce boilerplate, including support for C# 12 collection expressions.
// Success (with or without data)
public Result Create() => Result.Success();
public Result<Employee> Get() => new Employee(1, "John Doe", "Engineer"); // Implicit conversion
// Failure (Single Error)
public Result<Employee> Get(int id) => Error.NotFound($"Employee {id} not found."); // Implicit conversion
// Failure (Custom code and message - defaults to ErrorType.Failure / HTTP 400)
public Result Process() => Result.Failure("Order.InvalidState", "The order cannot be modified in its current state.");
// Failure (With explicit type - e.g. Conflict -> HTTP 409)
public Result Create() => Result.Failure("User.Exists", "User already registered.", ErrorType.Conflict);
// Failure with data and explicit type (e.g. NotFound -> HTTP 404)
public Result<User> GetUser(int id)
=> Result.Failure<User>("User.NotFound", $"User {id} not found.", ErrorType.NotFound);
Multiple Errors
The library natively supports returning multiple errors at once.
// Failure (Multiple Errors using Collection Expressions - C# 12)
public Result Validate(User user) => [
Error.Validation("Email", "Email is required."),
Error.Validation("Age", "Must be 18 or older.")
]; // Implicitly converts Error[] to Result
// Failure (List of Errors)
public Result<Employee> RegisterEmployee(RegisterRequest request) {
List<Error> errors = [];
if (string.IsNullOrEmpty(request.Name)) errors.Add(Error.Validation("Name", "Required"));
if (request.Salary < 0) errors.Add(Error.Validation("Salary", "Positive only"));
if (errors.Count > 0)
return errors; // Implicitly converts List<Error> to Result<Employee>
return new Employee(request.Name, request.Salary);
}
Accessing Error Details
When an operation fails, you can easily inspect the results:
var result = Get(123);
if (result.IsFailure)
{
Error? first = result.FirstError; // The primary Error object
Error[]? all = result.Errors; // All Error objects
string? msg = result.FirstErrorMessage; // Message of the primary error
string summary = result.JoinErrorMessages(); // All messages joined: "Msg 1; Msg 2"
}
Functional Extensions (Railway-Oriented Programming)
Reduce nested if statements and build declarative pipelines.
// Match: Execute different paths based on state
// 1. Match and discard - ignore error details
return result.Match(
data => Results.Created($"/employees/{data.Id}", data),
_ => Results.StatusCode(StatusCodes.Status400BadRequest));
// 2. MatchFirst - use only the first error
return result.MatchFirst(
data => Results.Created($"/employees/{data.Id}", data),
error => result.ToMinimalApiResult());
// 3. Match with Error[] - use all errors
return result.Match(
data => Results.Created($"/employees/{data.Id}", data),
errors => Results.BadRequest(new
{
message = "One or more errors occurred",
errors = errors.Select(e => new { e.Code, e.Message })
}));
// Map: Transform success data
Result<UserDto> dto = result.Map(u => new UserDto(u.Id, u.Name));
// Bind: Chain result-returning operations (FlatMap)
return await GetUserAsync(id)
.BindAsync(user => UpdateAsync(user));
// Tap: Execute side effects (e.g., logging) without modifying the result
result.Tap(data => Console.WriteLine($"Processed: {data}"));
Async Chaining (Task Extensions)
Chain operations directly on Task<Result> without manual await at each step. This makes asynchronous "railway-oriented" pipelines much cleaner.
return await _repository.GetByIdAsync(id) // Task<Result<User>>
.BindAsync(user => _service.Validate(user)) // Task<Result<User>>
.BindAsync(user => _repository.Update(user)) // Task<Result>
.MatchAsync(() => Results.NoContent(), errors => Result.Failure(errors).ToMinimalApiResult());
Error Aggregation (Combine)
Merges multiple results into one. If any fail, it aggregates all errors from all results into a single flat collection. This avoids any information loss and is perfect for complex validation scenarios.
var result = Result.Combine(
ValidateEmail(email),
ValidatePassword(password),
CheckPermissions(user)
);
if (result.IsFailure) {
// result.Errors contains all collected errors from all failing steps
}
Custom Errors & Metadata
Define domain-specific errors and attach extra context to any result.
// 1. Define constants for your business error codes
public static class UserErrorCodes {
public const string Disabled = "User.Disabled";
}
// 2. Create a factory for domain-specific Error objects
public static class UserErrors {
public static Error Disabled(int id) => Error.Disabled("User is disabled.", UserErrorCodes.Disabled);
}
// Attach Success Metadata
var metadata = new Dictionary<string, object> { { "TraceId", "abc-123" } };
return Result.Success(data, metadata);
// Validation errors with field-level metadata (recommended pattern)
// Metadata keys are property names, values are validation message arrays
var validationMetadata = new Dictionary<string, object>
{
["Email"] = new[] { "Email is required.", "Email format is invalid." },
["Password"] = new[] { "Password must be at least 8 characters." }
};
var validationError = Error.Validation(
ErrorCodes.Validation,
"Validation failed.",
validationMetadata
);
var result = Result.Failure(validationError);
Validation Error Response Format:
{
"data": null,
"errors": [
{
"code": "General.Validation",
"message": "Validation failed.",
"type": 2,
"metadata": {
"Email": [
"Email is required.",
"Email format is invalid."
],
"Password": [
"Password must be at least 8 characters."
]
}
}
]
}
This pattern is recommended for handling multiple validation errors in application layers (e.g., MediatR ValidationBehavior). Clients can easily parse and display field-level errors directly from metadata without additional processing. This pattern is natively supported by the AspNetCore mapping logic, automatically populating the errors dictionary in ValidationProblemDetails.
Safety Helpers
Fail fast or provide fallbacks when certain of the outcome.
var data = result.ValueOrThrow(); // Throws InvalidOperationException on failure with all error details
var data = result.ValueOrThrow(errors => new MyException(errors[0].Message));
var data = result.ValueOrDefault("Fallback");
JSON Serialization Support
The library includes built-in JSON converters for System.Text.Json that handle both serialization and deserialization correctly.
// Serialization
var result = Result.Success(new UserDto(1, "John"));
var json = JsonSerializer.Serialize(result);
// Deserialization (works with direct API responses)
var deserialized = JsonSerializer.Deserialize<Result<UserDto>>(json);
// Works with naming policies
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var restored = JsonSerializer.Deserialize<Result<UserDto>>(json, options);
Advanced Usage (Performance)
The library is optimized for high-performance scenarios like MediatR Pipelines. The CreateFailure<TResponse> method uses cached delegates to instantiate generic result types nearly as fast as direct constructor calls.
// Example in a MediatR Pipeline Behavior
public async Task<TResponse> Handle(TRequest req, RequestHandlerDelegate<TResponse> next, CancellationToken ct) {
var errors = await ValidateAsync(req);
if (errors.Any()) {
return Result.CreateFailure<TResponse>(errors); // Ultra-fast generic instantiation
}
return await next();
}
3. ASP.NET Core Integration (Kovecses.Result.AspNetCore)
Standardized HTTP responses with zero effort.
Automatic Mapping
The library detects the nature of failures and responds accordingly:
- Pure Validation: If all errors are of type
Validation, it returns400 ValidationProblemDetails(standard ASP.NET format). - Mixed/Other Failures: Uses "First Error Wins" for status code and includes all errors in the
extensions["errors"]field.
| ErrorType | Status Code | Default Title | Description |
|---|---|---|---|
| Validation | 400 | Validation Error | Grouped into ValidationProblemDetails |
| Failure | 400 | Bad Request | General business rule violations |
| NotFound | 404 | Not Found | Resource does not exist |
| Conflict | 409 | Conflict | Resource state conflict (e.g., duplicate) |
| Unauthorized | 401 | Unauthorized | Authentication required |
| Forbidden | 403 | Forbidden | Insufficient permissions |
| Timeout | 408 | Request Timeout | The operation timed out |
| Canceled | 400 | Bad Request | Operation was canceled |
| Unexpected | 500 | Internal Server Error | Unhandled or internal errors |
Mapping Strategies
Standard REST (Public APIs)
Returns the data on success (200 OK or 204 No Content) and ProblemDetails on failure.
// Minimal API Example (Asynchronous)
app.MapGet("/users/{id}", (int id, IMediator m) =>
m.SendAsync(new GetUser(id)).ToMinimalApiResultAsync());
// Controller Example (Asynchronous)
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
=> await _service.GetByIdAsync(id).ToActionResultAsync();
// Synchronous variants are also available
public IActionResult Create(User cmd) => _service.Create(cmd).ToActionResult();
Smart Mapping (Custom Success Path)
The MatchToActionResult and MatchToMinimalApiResult methods (and their async counterparts) allow you to handle the success case (e.g., returning 201 Created) while letting the library handle the failure mapping automatically.
// Controller with custom success and default failure mapping
[HttpPost]
public async Task<IActionResult> Create(CreateCommand cmd)
=> await _mediator.SendAsync(cmd)
.MatchToActionResultAsync(data => CreatedAtAction(nameof(Get), new { id = data.Id }, data));
// Minimal API with custom success and custom failure mapping
app.MapPost("/users", async (CreateUserCommand cmd, IMediator m) =>
await m.SendAsync(cmd).MatchToMinimalApiResultAsync(
data => Results.Created($"/users/{data.Id}", data),
errors => Results.BadRequest("Custom error message")));
Wrapped Results (Internal/Typed Clients)
Returns the full Result object in the body (e.g., for Blazor or Typed Clients).
// Server-side (Asynchronous)
return await resultTask.ToMinimalApiResultAsync(includeResultInResponse: true);
// Server-side (Synchronous)
return result.ToMinimalApiResult(includeResultInResponse: true);
// Client-side deserialization
var result = await response.Content.ReadFromJsonAsync<Result<UserDto>>();
if (result.IsSuccess)
Console.WriteLine(result.Data.Name);
Direct Error Mapping
You can also map a single Error object directly without wrapping it in a Result first.
Error error = Error.NotFound("User not found");
return error.ToMinimalApiResult(); // Returns 404 ProblemDetails
return error.ToActionResult(); // Returns 404 ObjectResult(ProblemDetails)
4. Testing Support (Kovecses.Result.FluentAssertions)
Fluent extension methods for readable and maintainable tests.
using Kovecses.Result.FluentAssertions;
[Fact]
public void Test_Operation()
{
var result = _service.DoWork();
// Assert Success
result.Should().BeSuccess()
.HaveData(expectedUser) // 1. Equality check
.HaveData(u => u!.Name == "John") // 2. Boolean predicate
.HaveData(u => // 3. Action-based inspection (auto null-check)
{
u.Name.Should().Be("John");
u.Age.Should().BeGreaterThan(18);
});
// 4. Direct access via WhichData (auto success & null-check)
result.Should().WhichData.Name.Should().Be("John");
// Assert Specific Error
result.Should().BeFailure()
.HaveError(ErrorCodes.NotFound)
.HaveMessage("User not found.");
// Assert Collection of Errors
result.Should().HaveErrors()
.HaveCount(2)
.AllBeOfType(ErrorType.Validation).And
.Contain(e => e.Code == "Email");
// Assert Field-Specific Validation
result.Should().HaveValidationErrorFor("Email").Contain("required");
// Assert detailed Error properties and chain back to Result
result.Should()
.HaveError("User.Disabled")
.HaveMessage("User is disabled.").And
.BeFailure();
// Assert that accessing data on a failure result throws the correct exception
result.Should().ThrowOnValueAccess<InvalidOperationException>();
}
Aggregated Validation Errors with Metadata
When validating multiple fields, create a single aggregated validation error with metadata containing field-level messages:
public async Task<Result> ValidateAsync(CreateUserCommand request)
{
var validationMessages = new Dictionary<string, object>();
if (string.IsNullOrEmpty(request.FullName))
validationMessages["FullName"] = new[] { "Name is required." };
if (request.Position is not { Length: >= 3 })
validationMessages["Position"] = new[] { "Position must be at least 3 characters." };
if (validationMessages.Count > 0)
return Result.Failure(Error.Validation(ErrorCodes.Validation, "Validation failed.", validationMessages));
return Result.Success();
}
Assert with chainable fluent syntax:
[Fact]
public async Task CreateUser_WithValidationFailures_ShouldAggregateErrors()
{
var result = await _validator.ValidateAsync(new CreateUserCommand
{
FullName = "",
Position = "PM" // Too short
});
result.Should().BeFailure()
.HaveError(ErrorCodes.Validation)
.HaveValidationProperty("FullName")
.Contain("required")
.And
.HaveValidationProperty("Position")
.Contain("at least 3 characters");
}
HaveValidationProperty() returns a chainable ValidationPropertyAssertions with:
.Contain(string message)- Assert message contains text.ContainAll(params string[] messages)- Assert all messages are present.HaveCount(int expected)- Assert exact message count.BeExactly(params string[] messages)- Assert exact match.And- Chain back to error assertions for multiple property checks
Custom Assertions
If you need to write custom assertions for validation errors, you can use the ValidationAssertions.ExtractMessages helper to retrieve and normalize error messages from an Error object regardless of how they are stored (JsonElement, List, etc.).
var error = result.Errors!.First(e => e.Code == "Email");
var messages = ValidationAssertions<ResultAssertions>.ExtractMessages(error, "Email");
Assert.Contains(messages, m => m.Contains("required"));
| 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 is compatible. 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 is compatible. 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. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Kovecses.Result:
| Package | Downloads |
|---|---|
|
Kovecses.Result.FluentAssertions
Fluent assertions for Kovecses.Result. Provides a highly readable, chainable API for testing success, failures, data content, and validation errors using xUnit. |
|
|
Kovecses.Result.AspNetCore
ASP.NET Core integration for Kovecses.Result. Automatically maps results to RFC 7807 ProblemDetails, including validation errors. Provides fluent extensions for Minimal APIs and Controllers. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.11.0 | 203 | 5/7/2026 |
| 2.10.0 | 157 | 5/5/2026 |
| 2.9.0 | 164 | 4/7/2026 |
| 2.8.0 | 163 | 4/6/2026 |
| 2.7.0 | 157 | 4/6/2026 |
| 2.6.0 | 161 | 4/5/2026 |
| 2.5.0 | 154 | 4/4/2026 |
| 2.4.1 | 161 | 4/4/2026 |
| 2.4.0 | 151 | 4/4/2026 |
| 2.3.1 | 155 | 4/4/2026 |
| 2.3.0 | 148 | 4/4/2026 |
| 2.2.0 | 155 | 4/4/2026 |
| 2.1.0 | 152 | 4/4/2026 |
| 2.0.0 | 164 | 4/4/2026 |
| 1.2.1 | 157 | 3/30/2026 |
| 1.1.0 | 155 | 3/29/2026 |
| 1.0.1 | 152 | 3/29/2026 |
| 1.0.0 | 149 | 3/29/2026 |