Skip to content

Results

John Rippington edited this page Feb 21, 2025 · 2 revisions

UKHO.ADDS.Infrastructure.Results

This package provides a simple and flexible implementation of the Result Pattern. The Result Pattern is a way of representing the outcome of an operation, whether it's successful or has encountered an error, in a more explicit and structured manner.

The package is lightweight, extensible, immutable and thread-safe.

Getting Started

The package consists of only three classes Result, Result<TValue>, and Error (with associated interfaces)

  • The Result class represents a generic result indicating success or failure.
  • The Result<TValue> class represents a success or failure result with a value.
  • The Error class represents an error with a message and optional associated metadata.

Creating a successful result

Successful results can be created using the Success method.

var successResult = Result.Success();

var successResultWithValue = Result.Success(349.4);

Creating a failure result

Failed results can be created using the Failure method.

var failureResult = Result.Failure();

var failureResultWithMessage = Result.Failure("Operation failure!");

var failureResultWithMessageAndMetadata = Result.Failure("Operation failure!", ("UserId", userId));

var failureResultWithMessageAndException = Result.Failure("Operation failure!", ex);

Checking the state of a result

There are two methods used to check a result, IsSuccess() and IsFailed(). Both of which have several overloads to obtain the value and error.

if (result.IsSuccess())
{
    // The result is successful.
}

if (result.IsFailure(out var error))
{
    // The result is failure.
    if (error.Message.Length > 0)
        Console.WriteLine(error.Message);
    else
        Console.WriteLine("An unknown error occured!");
}

Getting the value

The value from a successful result can be retrieved through the out parameter of the Success() method.

if (result.IsSuccess(out var value))
{
    Console.WriteLine($"Value is {value}");
}

Creating errors

Errors can be created with or without a message.

var errorWithoutMessage = new Error();

var errorWithMessage = new Error("Something went wrong!");

Or with a message and metadata.

var errorWithMetadataTuple = new Error("Something went wrong!", ("Key", "Value"));

var metadata = new Dictionary<string, object> { { "Key", "Value" } };
var errorWithMetadataDictionary = new Error("Something went wrong!", metadata);

Custom errors

The best way to represent specific errors is to make custom error classes that inherit from Error and define the error message as a base constructor parameter.

public sealed class NotFoundError : Error
{
    public NotFoundError()
        : base("The resource cannot be found.")
    {
    }
}

var notFoundError = new NotFoundError();
var notFoundResult = Result.Failure(notFoundError);

Then the result can be checked against that error type.

if (result.IsFailure(out var error) && error is NotFoundError)
{
    // Looks like the resource was not found, we better do something about that!
}

Or checked to see if there are any errors of that type.

if (result.IsFailure() && result.HasError<NotFoundError>())
{
    // At least one of the errors was a NotFoundError.
}

This can be especially useful when combined with metadata that is related to a specific type of error.

public sealed class HttpError : Error
{
    public HttpError(HttpStatusCode statusCode)
        : base("An HTTP error occured.", ("StatusCode", statusCode))
    {
    }
}

We can further simplify creating errors by creating an error factory.

public static AppError
{
    public Result NotFound()
    {
        var notFoundError = new NotFoundError();
        return Result.Failure(notFoundError);
    }

    public Result HttpError(HttpStatusCode statusCode)
    {
        var httpError = new HttpError(statusCode)
        return Result.Failure(httpError);
    }
}

Which clearly and explicitly describes the results.

public Result GetPerson(int id)
{
    var person = _database.GetPerson(id);
    
    if (person is null)
        return AppError.NotFound();
    
    return Result.Success();
}

Handling Exceptions

Specific overloads have been added to Failure() and Failure<TValue>() to simplify using try-catch blocks and return from them with a result instead of throwing.

public Result DoSomeWork()
{
    try
    {
        // We must not throw an exception in this method!
    }
    catch(Exception ex)
    {
        return Result.Failure(ex);
    }
    
    return Result.Success();
}

Error Factory

The package provides the ErrorFactory class to simplify creation of common error types:

IError CreateError(HttpStatusCode statusCode) will create the following:

HttpStatusCode Error Type
BadRequest BadRequestError
Unauthorized UnauthorizedError
ServiceUnavailable ServiceUnavailableError
BadGateway DownstreamServiceError
NotFound NotFoundError
InternalServerError InternalServerError
(other)) HttpError

This is based on the LightResults package, found here, with some modifications and additions.