WTorricos.Results 2.0.4

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

Results

This is a different take on Result that allows you to write error tolerant code that can be composed without losing performance.

Install it from Nuget.

Documentation

Version 2.0.0 contains breaking changes, review the changelog for a migration guide.

Why?

We don't have a consolidated approach for error handling in dotnet. For those of us that like the Result approach we have different options:

They are all a great way to write error tolerant code. However, Results brings a slightly different approach inspired in functional programming that intents to be light and easy to get started with.

If you like the repository please feel free to open an issue or a PR for new features or improvements.

How?

Results uses source generators to generate all the boilerplate code for you avoiding reflection. This way you get simplicity and good performance combined.

Getting Started

No default errors are provided and creating your own custom errors is recommended. In order to create custom errors you just need to decorate your partial records with the [None] attribute:

// You can name your records whatever you want but the Error suffix is recommended as well as making them sealed
[None]
public sealed partial record DivideByZeroError;

By doing this the generator will extend DivideByZeroError and will create a partial DivideByZeroError class as well as a DivideByZeroError<T> generic version.

You can use it like this:

public IMaybe<int> MyDivision(int numerator, int denominator)
{
    if (denominator == 0)
    {
        return DivideByZeroError<int>.Create("Division by zero is not allowed");
    }

    return Maybe.Create(numerator / denominator);
}

Main Operations To Work With IMaybe

As of now there are 3 operations that are supported that allow you to compose your results: Map, FlatMap and Action.

Map example:

// Works with generic results
IMaybe<int> intResult = Maybe.Create(1);
IMaybe<string> stringResult = intResult.Map(x => x.ToString());

// As well as with non generic results
IMaybe result = Maybe.Create();
IMaybe<string> stringResult = result.Map(() => "Hello World");

// You can also map tasks
Task<IMaybe<int>> intResult = Task.FromResult(Maybe.Create(1));
Task<IMaybe<string>> stringResult = intResult.Map(x => x.ToString());

// Or return tasks
IMaybe<int> intResult = Maybe.Create(1);
Task<IMaybe<string>> taskResult = intResult.Map(i => Task.FromResult(i.ToString(CultureInfo.InvariantCulture)));

Flat map supports all the map operations but it "flattens" a result of a result into a single result, for example:

IMaybe<int> intResult = Maybe.Create(1);
// If we use map we would get IMaybe<IMaybe<string>> but with flat map we get IMaybe<string>
IMaybe<string> stringResult = intResult.FlatMap(x => Maybe.Create(x.ToString()));

Action example:

IMaybe<int> intResult = Maybe.Create(1);
intResult.Action(x => Console.WriteLine(x));

Linq Syntax

Maybe also supports Linq syntax, for example:

IMaybe<int> seven = from i in MyDivision(6, 1)
                    from j in MyDivision(2, 2)
                    select i + j;

// which would be equivalent to:
IMaybe<int> some = MyDivision(6, 1)
    .FlatMap(i => MyDivision(2, 2)
        .Map(j => i + j));

// Operations supported with Linq syntax are: from, where and select
IMaybe<int> seven = from i in MyDivision(6, 1)
                    from j in MyDivision(2, 2)
                    where i > j
                    select i + j;
switch (result)
{
    case Some<int> some:
        Console.WriteLine(some.Value);
        break;
    // PredicateFailedError can be used to handle the case when the where clause failed
    case PredicateFailedError<int> err:
        Console.WriteLine(err.Message);
        break;
    // handle custom errors
    case MyError<int> err:
        Console.WriteLine(err.Message);
        break;
    // Handle any other errors
    case INone err:
        Console.WriteLine(err.Message);
        break;
}

More Examples

Review the unit tests for more examples:

Exit Maybe World

What if I want to get out of the IMaybe world? IsSucess or similar properties are intentionally not exposed and more well suited approaches are enforced. For example:

// Using switch or switch expressions is the recommended approach as will indirectly motivate you to handle all cases
IMaybe<int> some = MyDivision(5, 0);
switch (some)
{
    case Some<int> result:
        Console.WriteLine($"result: {result.Value}");
        break;
    case DivideByZeroError<int> err:
        Console.WriteLine($"error: {err.Message}");
        break;
    case INone err:
        Console.WriteLine($"unexpected error ${err.Message}");
        break;
}

int value = MyDivision(5, 0) switch
{
    Some<int> success => success.Value,
    DivideByZeroError<int> error => 0, // Fallback value
    INone<int> error => throw new InvalidOperationException(error.GetDisplayMessage()),
    _ => throw new InvalidOperationException("Unknown error")
};

// You can also use pattern matching
IMaybe<int> intResult = MyDivision(5, 0);
if (intResult is Some<int> success)
{
    Console.WriteLine(success.Value);
}
else if (intResult is DivideByZeroError<int> error)
{
    Console.WriteLine(error.GetDisplayMessage());
}

// Don't forget you have is not at your dispossal as well
if (intResult is not Some<int>)
{
    Console.WriteLine("Not a success");
}

// IMaybe classes also expose a Match method that takes two arguments:
// onSome which is invoked when the Maybe result is of tipe Some and
// onNone which an Optional argument and is invoked when there is an error
IMaybe<int> maybeInt = Maybe.Create(value: 1);
IMaybe<string> result = maybeInt.Match(
    value => value.ToString(CultureInfo.InvariantCulture),
    err => Console.WriteLine(err));

// Discouraged but in case you are completely sure you have a value you can
// use GetValueOrThrow extension method
IMaybe<int> some = Maybe.Create(1);
int one = some.GetValueOrThrow();

Cast

The recommend approach is to use the fluent extensions (Map, FlatMap, Action) and the Linq syntax to work with Maybe results. However, there may be cases where you can find Cast useful, specially if you want to create your own extension methods.

As the name suggest Cast is used to cast IMaybe<TypeA> to IMaybe<TypeB> for example:

IMaybe<string> ValidateDivision(int numerator, int denominator)
{
    IMaybe<int> result = MyDivision(numerator, denominator);

    if (result is INone error)
    {
        // cannot return error directly because it is of IMaybe<int> type
        return error.Cast<string>();
    }

    return Maybe.Create("Is Valid!");
}

ValidateDivision(1, 0).Should().BeAssignableTo<INone<string>>();
ValidateDivision(1, 0).Should().BeOfType<DivideByZeroError<string>>();
ValidateDivision(1, 1).Should().BeAssignableTo<Some<string>>();

Integrating it with other libraries

Here are some examples of how you can use IMaybe and integrate it with other libraries.

FluentValidation

Here is a gist of an Error that plays well with the FluentValidation library.

// 1. Create a custom Maybe Error
[None]
public sealed partial record ValidationErrorResult<T>
{
    // Add a custom constructor to create a ValidationErrorResult<T> from a FluentValidation ValidationResult
    public ValidationErrorResult(ValidationResult validationResult)
    {
        // A valid validationResult means it's a dev error that needs to be fixed immediately
        // for that reason it's ok to throw an exception here.
        if (validationResult.IsValid)
        {
            throw new InvalidOperationException("ValidationErrorResult cannot be created from a successful validation");
        }

        Message = typeof(T).Name;

        List<NoneDetail> errorList = new(validationResult.Errors.Count);
        errorList.AddRange(validationResult.Errors.Select(e => new NoneDetail(e.PropertyName, e.ErrorMessage)));
        Details = errorList;
    }
}

// 2. Add an AbstractValidator extension method to validate entities
public static class AbstractValidatorExtensions
{
    public static IMaybe<T> ValidateToMaybe<T>(this AbstractValidator<T> validator, T target)
    {
        ValidationResult? result = validator.Validate(target);
        return result is null || result.IsValid
            ? Maybe.Create(target)
            : new ValidationErrorResult<T>(result);
    }
}

// 3. Use it to validate your objects and get an IMaybe result
IMaybe<MyObject> validatedObject = validator.ValidateToMaybe(MyObject);

MediatR

Here is a gist of a Behavior that plays well with the MediatR library.

It will catch any unhandled exception inside a request handler of type IRequestHandler<TIn, IMaybe> or IRequestHandler<TIn, IMaybe<TOut>> and will guaranty that it doesn't throw exceptions.

// 1. We create a generic error that will be returned for unhandled exceptions
[None]
public sealed partial record InternalError;

// 2. We create a generic behavior that will capture unhandled exceptions (truly exceptional exceptions)
/// <summary>
///     By default, we capture every TResponse of type IMaybe inside a try-catch block,
///     in order to guaranty that they always return an IMaybe, even on unexpected exceptions.
/// </summary>
public sealed class RequestErrorHandlerBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
    where TResponse : IMaybe
{
    public async ValueTask<TResponse> Handle(
        TRequest message,
        CancellationToken cancellationToken,
        MessageHandlerDelegate<TRequest, TResponse> next)
    {
        try
        {
            return await next(message: message, cancellationToken: cancellationToken).ConfigureAwait(false);
        }
#pragma warning disable CA1031 // catch a more specific exception
        catch (Exception e)
#pragma warning restore CA1031
        {
            Type responseType = typeof(TResponse);
            Type[] genericArguments = responseType.GetGenericArguments();

            Log.Error("Failed to create response {responseType} for {requestType}", responseType.Name, message.GetType().Name);

            // if there are no generic arguments it means TResponse is of type Result
            if (genericArguments.Length == 0)
            {
                return (TResponse)InternalError.Create(e.Message);
            }

            // if there are generic arguments it means TResponse is of type Result<T>
            Type genericErrorType = typeof(InternalError<>);
            Type responseErrorType = genericErrorType.MakeGenericType(genericArguments);

            // We use the constructor instead of the Create factory method for convenience
            object error = Activator.CreateInstance(type: responseErrorType, e.Message, Array.Empty<NoneDetail>())!;
            return (TResponse)error;
        }
    }
}

// 3. We register the behavior in the DI container (remember that order matters when you register this behavior)
services
    .AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestErrorHandlerBehavior<,>));

Api

Here is a gist that plays well with APIs.

It allows you to map IMaybe results to Microsoft.AspNetCore.Http.IResult results.

// define your errors, for example:
[None]
public sealed partial record InternalError;
[None]
public sealed partial record DomainError;
[None]
public sealed partial record ConflictError;
[None]
public sealed partial record NotFoundError;
[None]
public sealed partial record UnauthorizedError;
[None]
public sealed partial record ForbiddenError;

// Extension method that maps to ErrorDetails
public static class ApiExtensions
{
    public static Results<Ok<TResponse>, ProblemHttpResult, ValidationProblem, Conflict> ToHttpResult<TResponse>(
        this IMaybe<TResponse> maybe) =>
        maybe switch
        {
            Some<TResponse> some => TypedResults.Ok(some.Value),
            DomainError<TResponse> error => TypedResults.Problem(
                new()
                {
                    Status = StatusCodes.Status400BadRequest,
                    Detail = error.Message
                }),
            ValidationErrorResult<TResponse> error => TypedResults.ValidationProblem(
                errors: error.Details.ToDictionary(e => e.Code, e => (string[]) [e.Description]),
                detail: error.Message),
            UnauthorizedError<TResponse> error => TypedResults.Problem(
                new()
                {
                    Status = StatusCodes.Status401Unauthorized,
                    Detail = error.Message
                }),
            ForbiddenError<TResponse> error => TypedResults.Problem(
                new()
                {
                    Status = StatusCodes.Status403Forbidden,
                    Detail = error.Message
                }),
            NotFoundError<TResponse> error => TypedResults.Problem(
                new()
                {
                    Status = StatusCodes.Status404NotFound,
                    Detail = error.Message
                }),
            ConflictError<TResponse> => TypedResults.Conflict(),
            _ => TypedResults.Problem(
                new()
                {
                    Status = StatusCodes.Status500InternalServerError,
                    Detail = "Unexpected error occurred."
                })
        };
}

// Sample usage
builder.MapPost(
    "api/todo",
    async (
        [FromBody] CreateTodoRequest request,
        [FromServices] IMediator mediator
    ) =>
    {
        IMaybe<CreateCountryResponse> result = await mediator.Send(request).ConfigureAwait(false);
        return result.ToHttpResult();
    });
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.4 2,038 1/21/2025
2.0.3 438 1/21/2025
2.0.2 441 1/20/2025
2.0.1 437 1/19/2025
2.0.0 541 1/4/2025
1.3.4 4,316 1/29/2024
1.3.3 801 8/8/2023
1.3.2 392 4/28/2023
1.3.1 308 4/28/2023
1.3.0 331 4/20/2023
1.2.0 336 4/18/2023
1.1.1 312 4/16/2023
1.1.0 311 4/16/2023
1.0.0 330 4/15/2023

Version 2