LightResults is an extremely light and modern .NET library that 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. This project is heavily inspired by Michael Altmann's excellent work with FluentResults.
This library targets .NET Standard 2.0, .NET 6.0, .NET 7.0, .NET 8.0, and .NET 9.0.
This library has no dependencies.
- 🪶 Lightweight — Only contains what's necessary to implement the Result Pattern.
- ⚙️ Extensible — Simple interfaces and base classes make it easy to adapt.
- 🧱 Immutable — Results and errors are immutable and cannot be changed after being created.
- 🧵 Thread-safe — Error and metadata collections are read-only.
- ✨ Modern — Built against the latest version of .NET using the most recent best practices.
- 🧪 Native — Written, compiled, and tested against the latest versions of .NET.
- ❤️ Compatible — Available for dozens of versions of .NET as a .NET Standard 2.0 library.
- 🪚 Trimmable — Compatible with ahead-of-time compilation (AOT) as of .NET 7.0.
- 🚀 Performant — Heavily optimized and benchmarked to aim for the highest possible performance.
Several extensions are available to simplify implementation that use LightResults.
Make sure to read the docs for the full API.
LightResults consists of only three types: Result
, Result<TValue>
, and Error
.
- 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.
Successful results can be created using the Success
method.
var successResult = Result.Success();
var successResultWithValue = Result.Success(349.4);
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);
There are two methods used to check a result, IsSuccess()
and IsFailure()
. 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 occurred!");
}
The value from a successful result can be retrieved through the out
parameter of the IsSuccess()
method.
if (result.IsSuccess(out var value))
{
Console.WriteLine($"Value is {value}");
}
A failed result can be converted to another result type using AsFailure
.
var result = Result.Failure("Invalid input");
var typed = result.AsFailure<int>();
var backToNonGeneric = typed.AsFailure();
Errors can be created with or without a message.
var emptyError = Error.Empty;
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);
var errorWithMetadataKeyValuePair = new Error("Something went wrong!", new KeyValuePair<string, object>("Key", "Value"));
var errorWithMetadataEnumerable = new Error("Something went wrong!", new[] { new KeyValuePair<string, object>("Key", "Value") });
var ex = new InvalidOperationException();
var errorWithException = new Error(ex);
var errorWithMessageAndException = new Error("Something went wrong!", ex);
When an error contains an exception, it can be accessed through the Exception
property available on both Error
and IError
.
var ex = new InvalidOperationException("Something went wrong!");
var error = new Error(ex);
if (error.Exception != null)
{
Console.WriteLine($"Exception: {error.Exception.Message}");
}
// Also works with IError interface
IError interfaceError = error;
if (interfaceError.Exception != null)
{
Console.WriteLine($"Exception: {interfaceError.Exception.Message}");
}
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 occurred.", ("StatusCode", statusCode))
{
}
}
Error
implements IEquatable<Error>
so instances with the same message and metadata are considered equal.
var error1 = new Error("Invalid", ("Code", 42));
var error2 = new Error("Invalid", ("Code", 42));
if (error1 == error2)
{
// Errors are equal
}
We can further simplify creating errors by creating an error factory.
public static class AppError
{
public static Result NotFound()
{
var notFoundError = new NotFoundError();
return Result.Failure(notFoundError);
}
public static 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();
}
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();
}
Note: Applies to .NET 7.0 (C# 11.0) or higher only!
Thanks to the static abstract members in interfaces introduced in .NET 7.0 (C# 11.0), it is possible to use generics to obtain access to the methods of the generic variant of the result. As such the error factory can be enhanced to take advantage of that.
public static class AppError
{
public static Result NotFound()
{
var notFoundError = new NotFoundError();
return Result.Failure(notFoundError);
}
public static TResult NotFound<TResult>()
{
var notFoundError = new NotFoundError();
return TResult.Failure(notFoundError);
}
}
The API for LightResults was completely redesigned for v9.0 to improve performance and remove any potential pits of failure caused by the prior version's use of properties that could result in exceptions being thrown when values were accessed without checking the state of the result. Thus, there are several breaking changes, detailed below, that developers must be aware of when upgrading from v8.0 to v9.0.
- Factory methods for creating generic results have changed names.
Result.Ok()
has been renamed toResult.Success()
.Result.Fail()
has been renamed toResult.Failure()
.
- Factory methods for creating results with values have changed names and type to allow omitting the type when it is known.
Result<TValue>.Ok()
has been renamed and moved toResult.Success<TValue>()
.Result<TValue>.Fail()
has been renamed toResult.Failure<TValue>()
.
- The
Value
andError
properties have been removed.result.Value
has been replaced byresult.IsSuccess(out var value)
.result.Error
has been replaced byresult.IsFailure(out var error)
.
- Several constructors of the
Error
type have been removed or have changed.Error((string Key, object Value) metadata)
has been removed.Error(IDictionary<string, object> metadata)
has been removed.Error(string message, IDictionary<string, object> metadata)
has been changed toError(string message, IReadOnlyDictionary<string, object> metadata)
.
The following steps in the following order will reduce the amount of manual work required to migrate and refactor code to use the new API.
- Find and replace all instances of
Result.Ok(
withResult.Success(
. - Find and replace all instances of
Result.Fail(
withResult.Failure(
. - Regex find and replace all instances of
Result(<[^>]+>)\.Ok\(
withResult.Success$1(
. - Regex find and replace all instances of
Result(<[^>]+>)\.Fail\(
withResult.Failure$1(
. - Find and replace all instances of
.IsSuccess
withIsSuccess(out var value)
. - Find and replace all instances of
.IsFailed
withIsFailure(out var error)
. - Find instances of
result.Value
and refactor them to the use thevalue
exposed by theIsSuccess()
method. - Find instances of
result.Error
and refactor them to the use theerror
exposed by theIsFailure()
method.
- New overloads have been added for
KeyValuePair<string, object>
metadata.Result.Failure(string errorMessage, KeyValuePair<string, object> metadata)
has been added.Result.Failure<TValue>(string errorMessage, KeyValuePair<string, object> metadata)
has been added.
- New overloads have been added to simplify handling exceptions.
Result.Failure(Exception ex)
has been added.Result.Failure(string errorMessage, Exception ex)
has been added.Result.Failure<TValue>(Exception ex)
has been added.Result.Failure<TValue>(string errorMessage, Exception ex)
has been added.
- New overloads were added to access the value.
result.IsSuccess(out TValue value)
has been added.result.IsFailure(out IError error, out TValue value)
has been added.
- New overloads were added to access the first error.
result.IsFailure(out IError error)
has been added.result.IsSuccess(out TValue value, out IError error)
has been added.result.HasError<TError>(out TError error)
has been added.
- New property initializers were added to
Error
.Message { get; }
has changed toMessage { get; init; }
.Metadata { get; }
has changed toMetadata { get; init; }
.Error(Exception exception)
has been added.Error(string message, Exception exception)
has been added.Error.Empty
is now publicly accessible.Exception { get; }
has been added to bothError
andIError
.
- New helper methods were added to convert failed results.
result.AsFailure()
andresult.AsFailure<T>()
convert an existing result into a failure result of another type.
- Additional
Error
constructors were introduced for metadata collections.Error(string message, KeyValuePair<string, object?> metadata)
has been added.Error(string message, IEnumerable<KeyValuePair<string, object?>> metadata)
has been added.