Skip to content

A practical Result data structure implementation for C#

License

Notifications You must be signed in to change notification settings

lucasteles/Resulteles

Repository files navigation

CI Nuget

https://editorconfig.org/

Resulteles

Resulteles is a practical and lightweight library that define and facilitate the use of a success|failure data structure called Result (aka Either).

Getting started

NuGet package available:

$ dotnet add package Resulteles

Creating Results

You can create any result using the static methods on Result

// explicitly
var resultSuccess = Result.Ok<int, string>(42);
var resultError = Result.Error<int, string>("Something wrong!");

var processResult = Result.Try(() => { return 1; });
var processAsyncResult = Result.TryAsync(async () => { return await Task.FromResult(2); });

In most of the scenarios you will be using result as a return of a method or function, because of that it implements implicit convert operators from the generic types, reducing the generic type definition noise

public Result<int, string> GetEvenNumber(int number)
{
    if (number % 2 is 0)
        return number;
    else
        return $"{number} is not even!";
}

Reading Result Value

The Result type acts like a blackbox, by default there is no way to just get the typed value mostly because it can be two things, an Ok or an Error.

Result<int, string> result = GetEvenNumber(2);

// return -1 on error
int value1 = result.Match(
    n => n,
    error => -1
);

result.Switch(
    n => Console.WriteLine($"the number {n} is even"),
    error => Console.WriteLine(error)
);

// return 0 on error
var value2 = result.DefaultValue(0);

// lazily define the bad case value
var value3 = result.DefaultWith(e => e.Length);

// get the value or throw an exeption
var value4 = result.GetValueOrThrow();

// try/out pattern
if (result.TryGet(out var ok, out var error))
    Console.WriteLine($"the number {ok} is even");
else
    Console.WriteLine(error);

Operators

Most types you not even need to extract the value, you can just apply functions or combine with other Result types:

var result1 = Result.Ok<int, string>(1);

// Map the result value only when it is Ok
var result2 = result1.Select(n => n + 1);

// map for ok or error case
Result<string, int> result3 = result1.Select(n => n.ToString(), err => err.Length);

// SelectMany / Bind / FlatMamap
// Nested results transformation
Result<int, string> resultCombined = result1.SelectMany(n => GetEvenNumber(n));

// Zip Results together into a tuple
// only resolves as Ok when both are Ok
Result<(int, int), string> resultZip = result1.Zip(result2);

// Zip Results together
// only resolves as Ok when both are Ok
Result<int, string> resultSum = result1.Zip(result2, (a, b) => a + b);

// Enumerate result, empty when error, singleton when error (see ToArray() also)
IEnumerable<int> resultEnumerable = result1.AsEnumerable();

// LINQ QUERY
// you can use linq query to easily express map and bind

// simple map
var result2Linq = from r in result1 select r + 1;

// combining multiple results (fail fast at first non ok value)
var resultEvenSum =
    from a in GetEvenNumber(2)
    from b in GetEvenNumber(4)
    from c in GetEvenNumber(6)
    select a + b + c;
}

Async/Await

This library implements some helpers to seamless deal with async Result code

var result = GetEvenNumber(2);

// async match overload
int value1 = await result.Match(
    async n =>
    {
        await Task.Delay(n);
        return n;
    },
    _ => -1
);

await result.SwitchAsync(
    async n => await Console.Out.WriteLineAsync($"the number {n} is even"),
    async error => await Console.Error.WriteLineAsync(error)
);

// async projection counterparts
Result<int, string> resultWithDelay = await result.SelectAsync(async n =>
{
    await Task.Delay(n);
    return n;
});

// Transform Result<Task<int>, string> in a awaitable Task<Result<int, string>>
Result<int, string> resultWithDelay2 = await result.Select(async n =>
{
    await Task.Delay(n);
    return n;
}).ToTask();

Helper extensions

// result collection
var results = new[] { GetEvenNumber(2), GetEvenNumber(3), GetEvenNumber(4) };

// filter ok values
IEnumerable<int> okResultsOnly = results.GetOkValues(); // [2,4]

// map/reduce based on a result function
var numbers = new[] { 1, 2, 3, 4 };
var calculatedResults = numbers.ChooseResult(x => GetEvenNumber(x)); // [2,4]

// group a collection of result into a Result of collection
// collapse to error if any error is in the collection
Result<IReadOnlyList<int>, string> singleResult = results.GroupResults();

// Transform Result<Task<int>, string> in a awaitable Task<Result<int, string>>
Result<int, string> resultWithDelay2 = await results[0].Select(async n =>
{
    await Task.Delay(n);
    return n;
}).ToTask();

FluentAssertions Extensions

NuGet package available:

$ dotnet add package Resulteles.FluentAssertions

Alternatives