Resulta 3.2.0

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

Resulta 🎯

A lightweight C# library for the Result pattern – error handling without exceptions.

NuGet NuGet Downloads CI License: MIT .NET


Why Resulta?

Instead of:

// ❌ Exceptions as control flow – hard to read, easy to forget
try {
    var user = GetUser(id);  // throws NotFoundException
    return Ok(user);
} catch (NotFoundException ex) {
    return NotFound(ex.Message);
} catch (Exception ex) {
    return StatusCode(500, ex.Message);
}

Use this:

// ✅ Explicit, type-safe, no try/catch
return GetUser(id).Match<IActionResult>(
    onSuccess: user => Ok(user),
    onFailure: err  => err.Code switch
    {
        "NOT_FOUND" => NotFound(err.Message),
        _           => StatusCode(500, err.Message)
    }
);

Installation

Resulta packages target both net8.0 and net10.0.

Core package

dotnet add package Resulta

Optional integrations

dotnet add package Resulta.AspNetCore
dotnet add package Resulta.FluentValidation

Quick Start

Ok & Fail

using Resulta;

Result<int> Divide(int a, int b)
{
    if (b == 0)
        return Result.Fail<int>("Division by zero!");

    return Result.Ok(a / b);
}

var result = Divide(10, 2);

if (result.IsSuccess)
    Console.WriteLine(result.Value);    // 5
else
    Console.WriteLine(result.Error);    // error message

OkIf & FailIf

var result = Result.OkIf(user.IsActive, user, Error.Unauthorized("Account is inactive"));

var conflict = Result.FailIf(exists, resource, Error.Conflict("Already exists"));

Map & Bind – Chaining

var dto = LoadUser(1)
    .Bind(ConvertToDto)
    .Ensure(d => d.Email.Contains('@'), "Not a valid email")
    .Map(d => d with { Name = d.Name.Trim() });

Match – handle both cases

string response = result.Match(
    onSuccess: value => $"Result: {value}",
    onFailure: err   => $"Error: {err.Message}"
);

Success and null (reference types)

For reference types, Result<T>.Ok(value) still represents success when value is null. If null is invalid in your domain, validate explicitly and return Result<T>.Fail(...) instead. Value types are unaffected (Result<int>.Ok always carries a value).

Try – catch exceptions

var result = ResultExtensions.Try(
    () => int.Parse(input),
    ex  => new Error("Invalid number").WithCode("PARSE_ERROR")
);

CombineAsync – parallel async operations

var result = await ResultExtensions.CombineAsync(
    LoadUserAsync(id),
    LoadOrderAsync(id),
    LoadAddressAsync(id)
);

Error Class

var err = new Error("Not found")
    .WithCode("NOT_FOUND")
    .WithMetadata("id", 42);

// Predefined factories
var err = Error.NotFound("Product");
var err = Error.Validation("email", "Invalid email address");
var err = Error.Unauthorized();
var err = Error.Forbidden();
var err = Error.Unexpected(exception);
var err = Error.Conflict("Name already taken");
var err = Error.Unprocessable("Cannot process the submitted state");
var err = Error.TooManyRequests();

// Error chain
var err = Error.NotFound("User")
    .WithCause(new Error("Database connection failed"));

ValidationResult – collect multiple errors

var result = Validator<RegisterDto>.For(dto)
    .Must(d => d.Name.Length >= 2,    Error.Validation("name",  "At least 2 characters"))
    .Must(d => d.Email.Contains('@'), Error.Validation("email", "Must be a valid email"))
    .Must(d => d.Age >= 18,           Error.Validation("age",   "Must be at least 18"))
    .Validate();

result.Match(
    onSuccess: dto    => Console.WriteLine($"Registered: {dto.Name}"),
    onFailure: errors => errors.ToList().ForEach(e => Console.WriteLine($"  x {e.Message}"))
);

Railway Pipelines

Synchronous

var token = Pipeline<string>
    .Start(input)
    .Validate(s => s.Length > 0, "Must not be empty")
    .Then(s => FindUser(s))
    .Tap(user => logger.LogInformation("Login: {Name}", user.Name))
    .Then(user => CreateToken(user))
    .Finally(
        onSuccess: t   => $"Token: {t}",
        onFailure: err => $"Error: {err.Message}"
    );

Asynchronous

var result = await AsyncPipeline<Order>
    .Start(() => LoadOrderAsync(id))
    .Validate(order => order.Items.Count > 0, "Order must contain at least one item")
    .ThenAsync(order => ReserveStockAsync(order))
    .Tap(order => logger.LogInformation("Stock reserved: {Id}", order.Id))
    .ThenAsync(order => ProcessPaymentAsync(order))
    .TapAsync(async order => await SendConfirmationAsync(order))
    .Finally(
        onSuccess: _ => "Order placed successfully!",
        onFailure: e => $"Error: {e.Message}"
    );

ASP.NET Core Integration

dotnet add package Resulta.AspNetCore
builder.Services.AddResulta();
app.UseResulta();

// MVC
[HttpGet("{id}")]
public IActionResult Get(int id)
    => _service.GetUser(id).ToActionResult(this);

// Minimal API
app.MapGet("/api/users/{id}", (int id, UserService svc)
    => svc.GetUser(id).ToMinimalApiResult());

// Minimal API with TypedResults + OpenAPI annotations
app.MapGet("/api/users/{id}", (int id, UserService svc)
    => svc.GetUser(id).ToTypedResult())
   .ProducesResultaErrors();
Error.Code HTTP Status
NOT_FOUND 404 Not Found
VALIDATION_ERROR 400 Bad Request
UNAUTHORIZED 401 Unauthorized
FORBIDDEN 403 Forbidden
CONFLICT 409 Conflict
UNPROCESSABLE 422 Unprocessable Entity
TOO_MANY_REQUESTS 429 Too Many Requests
(anything else) 500 Internal Server Error

All failure responses use the RFC 7807 application/problem+json format. Validation errors return HttpValidationProblemDetails with an errors dictionary keyed by field name. Other codes return ProblemDetails. The original Error.Code is preserved on every response as the code extension property:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  "title": "Not Found",
  "status": 404,
  "detail": "'User' was not found.",
  "instance": "/api/users/42",
  "code": "NOT_FOUND"
}

For type-safe Minimal API endpoints with full OpenAPI metadata, use ToTypedResult() together with ProducesResultaErrors() — the endpoint then advertises every possible response shape (200/204, 404, 400 validation, 409, and ProblemHttpResult for other error responses).

Extending the error map

builder.Services.AddResulta(options =>
{
    options.MapError("RATE_LIMITED", StatusCodes.Status429TooManyRequests,
        "Rate Limited", "https://example.com/problems/rate-limited");

    options.ConfigureProblemDetails = (problem, error, http) =>
    {
        problem.Extensions["traceId"] = http?.TraceIdentifier;
    };
});

JSON converters for Result / Error

The core package also ships System.Text.Json converters if you want to serialize Result<T> over the wire (for inter-service messaging, queues, or persistence) instead of mapping through HTTP:

var options = new JsonSerializerOptions().AddResultaConverters();
var json    = JsonSerializer.Serialize(Result<int>.Fail(Error.Validation("email", "Invalid")), options);
// → {"isSuccess":false,"error":{"message":"Validation failed for 'email': Invalid","code":"VALIDATION_ERROR","field":"email"}}

The converters never leak exception stack traces and truncate causedBy chains at three levels.


FluentValidation Bridge

dotnet add package Resulta.FluentValidation
public Result<User> Register(RegisterDto dto) =>
    _validator.ValidateToResult(dto).Bind(CreateUser);

public async Task<Result<User>> RegisterAsync(RegisterDto dto) =>
    await _validator.ValidateToResultAsync(dto).Bind(CreateUserAsync);

Project Structure

Resulta/
├── Resulta/
│   ├── src/
│   │   ├── Result.cs
│   │   ├── ResultT.cs
│   │   ├── Error.cs
│   │   └── ResultExtensions.cs
│   └── extensions/
│       ├── ValidationResult.cs
│       └── Pipeline.cs
├── Resulta.AspNetCore/
│   └── AspNetCoreIntegration.cs
├── Resulta.FluentValidation/
│   └── FluentValidationBridge.cs
├── samples/
│   └── Resulta.Samples/     # optional console demos (not packed)
├── Resulta.Tests/
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── release.yml
├── CHANGELOG.md
├── VERSIONING.md
└── README.md

Releases and Versioning

Resulta follows Semantic Versioning:

  • MAJOR for breaking changes
  • MINOR for backwards-compatible features
  • PATCH for fixes and small improvements

GitHub Releases are created for every vX.Y.Z tag. NuGet packages are published only for MINOR or MAJOR tags where the patch component is 0 (for example v3.1.0 or v4.0.0). Patch tags such as v3.0.1 are GitHub-only.

For release history, see CHANGELOG.md. For version bump rules and release guidance, see VERSIONING.md.


Contributing

Contributions, issues and feature requests are welcome! Feel free to open an issue or submit a pull request on GitHub.


License

MIT – see LICENSE for details.

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 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.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Resulta:

Package Downloads
Resulta.AspNetCore

ASP.NET Core integration for Resulta, including HTTP result mapping and application setup helpers.

Resulta.FluentValidation

FluentValidation integration for Resulta, including helpers for converting validation output into Result and ValidationResult.

GitHub repositories

This package is not used by any popular GitHub repositories.