Kovecses.Result 2.11.0

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

Kovecses.Result

NuGet Version NuGet Version (AspNetCore) NuGet Version (FluentAssertions) License: MIT

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

  1. Introduction
  2. Installation
  3. Core Library (Kovecses.Result)
  4. ASP.NET Core Integration
  5. 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, and Bind.

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 returns 400 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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