Skip to content

Repository files navigation

Darp.Results

Darp.Results Downloads License

A Result type implementation for C# that provides a safe way to handle operations that can succeed or fail, inspired by Rust's Result<T, E> type.

Packages

Quick Start

using Darp.Results;

// Create Results
Result<int, string> success = new Result.Ok<int, string>(42);
Result<int, string> failure = new Result.Err<int, string>("Something went wrong");

// Implicit conversions
Result<int, string> implicitSuccess = 42;
Result<int, string> implicitFailure = "error";

// Extract values safely
if (result.TryGetValue(out int value)) { /* use value */ }
if (result.TryGetError(out string error)) { /* handle error */ }

// Switch on result
return result switch 
{
    Result.Ok<int, string>(var value) => value,
    Result.Err<int, string>(var error) => error,
}

Why another results library?

  • Get values through switching
  • API heavily inspired by Rust's Result<T, E>
  • Ability to attach metadata to results
  • Immutability
  • Any object should be a possible error type (e.g., enums)
  • Analyzers/CodeFixers
  • Designed to benefit from the discriminated unions proposal

Existing projects did not fit all the requirements. However, these projects were an inspiration:

Core API Reference

Creation

Constructors / Implicit Conversions

// Value to Result
Result<int, string> result = 42; // Creates Ok(42)
Result<int, string> result = new Result.Ok<int, string>(42);
Result<int, string> result = new Result.Ok<int, string>(42, metadata: new Dictionary<string, object>());

// Error to Result  
Result<int, string> result = "error"; // Creates Err("error")
Result<int, string> result = new Result.Err<int, string>("error");
Result<int, string> result = new Result.Err<int, string>("error", metadata: new Dictionary<string, object>());

Factory Methods

// Try-catch wrapper
Result<TValue, Exception> Result.Try<TValue>(Func<TValue> func)

// TryParse wrapper
Result<TValue, StandardError> Result.From<T, TValue>(T input, TryParseFunc<T, TValue> tryParse)

State Checking

bool IsOk { get; }  // True if result is success
bool IsErr { get; } // True if result is error

Value Extraction

Safe Extraction

// Extract value
bool TryGetValue(out TValue value)
bool TryGetValue(out TValue value, out Result.Err<TNewValue, TError> error)
bool TryGetValue<TNewValue>(out TValue value, out Result.Err<TNewValue, TError> error)

// Extract error
bool TryGetError(out TError error)
bool TryGetError(out TError error, out Result.Ok<TValue, TNewError> success)
bool TryGetError<TNewError>(out TError error, out Result.Ok<TValue, TNewError> success)

// Extract with a default value
TValue Unwrap(TValue defaultValue)           // Returns value or default
TValue Unwrap(Func<TValue> valueProvider)    // Returns value or computed default
TValue? UnwrapOrDefault()                    // Returns value or default(TValue)

Unsafe Extraction (throws on wrong state)

TValue Unwrap()                    // Throws if error
TValue Expect(string message)      // Throws with custom message if error
TError UnwrapError()               // Throws if success
TError ExpectError(string message) // Throws with custom message if success

Transformations

Map Operations

// Transform success value
Result<TNewValue, TError> Map<TNewValue>(Func<TValue, TNewValue> func)

// Transform error
Result<TValue, TNewError> MapError<TNewError>(Func<TError, TNewError> func)

Logical Combinators

// And operations
Result<TNewValue, TError> And<TNewValue>(Result<TNewValue, TError> result)
Result<TValue, TError> And(Func<TValue, Result<TValue, TError>> resultProvider)
Result<TNewValue, TError> And<TNewValue>(Func<TValue, Result<TNewValue, TError>> resultProvider)

// Or operations  
Result<TValue, TError> Or(Result<TValue, TError> result)
Result<TValue, TError> Or(Func<TError, Result<TValue, TError>> resultProvider)
Result<TValue, TNewError> Or<TNewError>(Func<TError, Result<TValue, TNewError>> resultProvider)

Task-based Extensions

Chain Task<Result<…>> without manual await/unwrap boilerplate:

// Map
Task<Result<TNewValue, TError>> Map<TValue, TError, TNewValue>(Func<TValue, TNewValue> func)
Task<Result<TValue, TNewError>> MapError<TValue, TError, TNewError>(Func<TError, TNewError> func)

// And
Task<Result<TNewValue, TError>> And<TValue, TError, TNewValue>(Result<TNewValue, TError> result)
Task<Result<TNewValue, TError>> And<TValue, TError, TNewValue>(Func<TValue, TNewValue> resultProvider)
Task<Result<TNewValue, TError>> And<TValue, TError, TNewValue>(Func<TValue, Result<TNewValue, TError>> resultProvider)

// Or
Task<Result<TValue, TNewError>> Or<TValue, TError, TNewError>(Result<TValue, TNewError> result)
Task<Result<TValue, TNewError>> Or<TValue, TError, TNewError>(Func<TError, TNewError> resultProvider)
Task<Result<TValue, TNewError>> Or<TValue, TError, TNewError>(Func<TError, Result<TValue, TNewError>> resultProvider)

// Unwrap / Expect
Task<TValue> Unwrap<TValue, TError>()
Task<TValue> Unwrap<TValue, TError>(TValue defaultValue)
Task<TValue> Unwrap<TValue, TError>(Func<TError, TValue> valueProvider)
Task<TValue?> UnwrapOrDefault<TValue, TError>()
Task<TError> UnwrapError<TValue, TError>()
Task<TValue> Expect<TValue, TError>(string message)
Task<TError> ExpectError<TValue, TError>(string message)

Utility Operations

Flattening

// Flatten nested Results
Result<TValue, TError> Flatten<TValue, TError>(Result<Result<TValue, TError>, TError> result)

Iteration

// Enumerate success values (yields single value for Ok, empty for Err)
IEnumerable<TValue> AsEnumerable()
IEnumerator<TValue> GetEnumerator() // Enables foreach

Metadata

// Add metadata
Result<TValue, TError> WithMetadata(string key, object value)
Result<TValue, TError> WithMetadata(ICollection<KeyValuePair<string, object>> metadata)

// Access metadata
IReadOnlyDictionary<string, object> Metadata { get; }

Testing with Shouldly

using Darp.Results.Shouldly;

// Assert success
result.ShouldBeSuccess();
result.ShouldHaveValue(expectedValue);
result.ShouldHaveValue(value => value.ShouldBeGreaterThan(0));

// Assert error
result.ShouldBeError();
result.ShouldHaveError(expectedError);
result.ShouldHaveError(error => error.ShouldNotBeNull());

// Assert async results
int value = await resultTask.ShouldBeSuccess();

Examples

Basic Usage

Divide two numbers, return a result and attempt to get the value

public Result<int, string> Divide(int a, int b)
{
    if (b == 0)
        return "Cannot divide by zero";
    return a / b;
}

var result = Divide(10, 2);
if (result.TryGetValue(out int value))
{
    Console.WriteLine($"Result: {value}");
}

Early returns

To achieve early returns, rust provides the ? operator. Due to shortcomings of c#, we'll use the TryGet pattern to get the error

public Result<string, StandardError> WorkWithResult(Result<int, StandardError> result)
{
    if (!result.TryGetValue(out int value, out Result.Err<string, StandardError>? err))
        return err;
    // ...
    return value.ToString();
}

Chaining Operations

var result = Result.Ok<int, string>(10)
    .Map(x => x * 2)
    .And(x => x > 15 ? new Result.Ok<string, string>($"Big: {x}") : new Result.Err<string, string>("Too small"))
    .Map(s => s.ToUpper());

result.ShouldHaveValue("BIG: 20");

Switching

Use switch expressions to easily access value/error

public Result<string, StandardError> SwitchOnResult(Result<int, StandardError> result)
{
    return result switch
    {
        Result.Ok<int, StandardError>(var value, var metadata) => value.ToString(),
        Result.Err<int, StandardError>(var error, var metadata) => error,
    };
}

With Metadata

var result = Result.Ok<int, string>(42)
    .WithMetadata("operation", "calculation")
    .WithMetadata("timestamp", DateTime.UtcNow);

// Metadata is preserved through transformations
var transformed = result.Map(x => x.ToString());
// transformed.Metadata still contains the original metadata

License

This project is licensed under the Apache License 2.0.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages