Class Result<TValue, TError>
Represents the result of an operation that can be successful or failed.
Implements
Inherited Members
Namespace: Recore
Assembly: Recore.dll
Syntax
[JsonConverter(typeof(ResultConverter))]
public sealed class Result<TValue, TError> : IEquatable<Result<TValue, TError>>
Type Parameters
| Name | Description |
|---|---|
| TValue | |
| TError |
Remarks
When working with System.Text.Json, deserializing into Result<TValue, TError>
can be ambiguous.
The default deserialization behavior will try first to deserialize
as TValue, and then as TError.
But, this may return the unintended type if the value deserializer can successfully deserialize the JSON
representation of the error.
A common case is when both TValue and TError are POCOs.
In that case, Result<TValue, TError> will always deserialize as TValue,
filling in default values for any missing properties.
For example:
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
class Address
{
public string Street { get; set; }
public string Zip { get; set; }
}
// Deserializes as a `Person`!
JsonSerializer.Deserialize<Result<Person, Address>>("{\"Street\":\"123 Main St\",\"Zip\":\"12345\"}")
You can use OverrideResultConverter<TValue, TError> to specify
how to choose between TValue and TError
based on the properties in the JSON:
// Look at the JSON to decide which type we have
options.Converters.Add(new OverrideResultConverter<Person, Address>(
deserializeAsLeft: json => json.TryGetProperty("Street", out JsonElement _)));
// Deserializes correctly
JsonSerializer.Deserialize<Result<Person, Address>>("{\"Street\":\"123 Main St\",\"Zip\":\"12345\"}", options)
Examples
Result<TValue, TError> is basically a reskin of Either<TLeft, TRight> with some additional semantics.
To explain the use case, let's think about exception-based and status-code-based error handling. (If you haven't read it before, I strongly recommend Joe Duffy's blog post on the topic.)
For an example of exception-based handling, think of Parse(String).
For an example of exception-based handling, think of Int32.TryParse(String, out Int32).
Basically, the debate looks like this:
| Pros | Cons | |
|---|---|---|
| Status codes |
|
|
| Exceptions |
|
|
As you can tell, exceptions are great for handling fatal errors. But what about recoverable errors? Status codes are a better choice, but are a bit primitive.
This is where Result<TValue, TError> comes in. It has the same pros as status codes, but addresses the cons:
- Less disruptive to the method signature; just return Result<TValue, TError>
- Can't get to the value unless you handle the error case
For an example, imagine you're calling HttpClient.GetAsync() to fetch some JSON data and deserialize it. Its signature is
Task<HttpResponseMessage> GetAsync(string requestUri);
With exceptions, you could implement it like this:
async Task<Person> GetPersonAsync(int id)
{
var response = await httpClient.GetAsync($"/api/v1/person/{id}");
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(response.StatusCode.ToString());
}
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<Person>(json);
}
But HTTP calls fail for all kinds of reasons. Who handles that exception? How do you decide to retry? How do you report the error to the user?
A better choice is to go with Result<TValue, TError>:
async Task<Result<Person, HttpStatusCode>> GetPersonAsync(int id)
{
var response = await httpClient.GetAsync($"/api/v1/person/{id}");
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var person = JsonSerializer.Deserialize<Person>(json);
return Result.Success<Person, HttpStatusCode>(person);
}
else
{
return Result.Failure<Person, HttpStatusCode>(response.StatusCode);
}
}
If you're working with an API that returns a special error response in case of errors (such as GitHub), you can even do this:
async Task<Result<Person, Error>> GetPersonAsync(int id)
{
// ...
else
{
var json = await response.Content.ReadAsStringAsync();
var error = JsonSerializer.Deserialize<Error>(json);
return Result.Failure<Person, HttpStatusCode>(error);
}
}
Anyway, let's say we just pass on the status code. Downstream code can then handle the error like this:
async Task<bool> GetPersonAndPrint(int id)
{
bool retry = false;
var personResult = await GetPersonAsync(id);
personResult.Switch(
person => Console.WriteLine(person),
status =>
{
if ((int)status >= 500)
{
retry = true;
}
else
{
Console.Error.WriteLine($"Fatal error: {status}");
}
});
return retry;
}
It also makes it easy to build up an error context as you go along rather than terminating immediately (see this code):
Result<IBlob, IBlob>[] results = await Task.WhenAll(blobsToWrite.Select(blob =>
Result.TryAsync(async () =>
{
await WriteBlobAsync(blob);
return blob;
})
.CatchAsync((Exception e) =>
{
Console.Error.WriteLine(e);
return Task.FromResult(blob);
})));
List<IBlob> successes = results.Successes().ToList();
List<IBlob> failures = results.Failures().ToList();
Constructors
| Improve this Doc View SourceResult(TValue)
Constructs an instance of the type from a value of TValue.
Declaration
public Result(TValue value)
Parameters
| Type | Name | Description |
|---|---|---|
| TValue | value |
Result(TError)
Constructs an instance of the type from a value of TError.
Declaration
public Result(TError error)
Parameters
| Type | Name | Description |
|---|---|---|
| TError | error |
Properties
| Improve this Doc View SourceIsSuccessful
Indicates whether the result is successful.
Declaration
public bool IsSuccessful { get; }
Property Value
| Type | Description |
|---|---|
| Boolean |
Methods
| Improve this Doc View SourceEquals(Result<TValue, TError>)
Compares two instances of Result<TValue, TError> for equality.
Declaration
public bool Equals(Result<TValue, TError> other)
Parameters
| Type | Name | Description |
|---|---|---|
| Result<TValue, TError> | other |
Returns
| Type | Description |
|---|---|
| Boolean |
Remarks
Equality is defined as both objects' underlying values or errors being equal.
Equals(Result<TValue, TError>, IEqualityComparer<TValue>, IEqualityComparer<TError>)
Compares two instances of Result<TValue, TError> for equality using the given IEqualityComparer<T>.
Declaration
public bool Equals(Result<TValue, TError> other, IEqualityComparer<TValue> valueComparer, IEqualityComparer<TError> errorComparer)
Parameters
| Type | Name | Description |
|---|---|---|
| Result<TValue, TError> | other | |
| IEqualityComparer<TValue> | valueComparer | |
| IEqualityComparer<TError> | errorComparer |
Returns
| Type | Description |
|---|---|
| Boolean |
Equals(Object)
Compares this Result<TValue, TError> to another object for equality.
Declaration
public override bool Equals(object obj)
Parameters
| Type | Name | Description |
|---|---|---|
| Object | obj |
Returns
| Type | Description |
|---|---|
| Boolean |
Overrides
Remarks
Two Result<TValue, TError>s are equal only if they have the same type parameters in the same order.
For example, an Result<int, string> and an Result<string, int>
will always be nonequal.
GetError()
Converts Result<TValue, TError>
to Optional<TError>
Declaration
public Optional<TError> GetError()
Returns
| Type | Description |
|---|---|
| Optional<TError> |
GetHashCode()
Returns the hash code of the underlying value.
Declaration
public override int GetHashCode()
Returns
| Type | Description |
|---|---|
| Int32 |
Overrides
| Improve this Doc View SourceGetValue()
Converts Result<TValue, TError>
to Optional<TValue>
Declaration
public Optional<TValue> GetValue()
Returns
| Type | Description |
|---|---|
| Optional<TValue> |
IfError(Action<TError>)
Takes an action only if the the result is failed.
Declaration
public void IfError(Action<TError> onError)
Parameters
| Type | Name | Description |
|---|---|---|
| Action<TError> | onError |
IfValue(Action<TValue>)
Takes an action only if the result is successful.
Declaration
public void IfValue(Action<TValue> onValue)
Parameters
| Type | Name | Description |
|---|---|---|
| Action<TValue> | onValue |
OnError<TResult>(Func<TError, TResult>)
Maps a function over the Result<TValue, TError> only if the result is failed.
Declaration
public Result<TValue, TResult> OnError<TResult>(Func<TError, TResult> onError)
Parameters
| Type | Name | Description |
|---|---|---|
| Func<TError, TResult> | onError |
Returns
| Type | Description |
|---|---|
| Result<TValue, TResult> |
Type Parameters
| Name | Description |
|---|---|
| TResult |
OnValue<TResult>(Func<TValue, TResult>)
Maps a function over the Result<TValue, TError> only if the result is successful.
Declaration
public Result<TResult, TError> OnValue<TResult>(Func<TValue, TResult> onValue)
Parameters
| Type | Name | Description |
|---|---|---|
| Func<TValue, TResult> | onValue |
Returns
| Type | Description |
|---|---|
| Result<TResult, TError> |
Type Parameters
| Name | Description |
|---|---|
| TResult |
Switch(Action<TValue>, Action<TError>)
Takes one of two actions depending on whether the result is successful.
Declaration
public void Switch(Action<TValue> onValue, Action<TError> onError)
Parameters
| Type | Name | Description |
|---|---|---|
| Action<TValue> | onValue | |
| Action<TError> | onError |
Switch<T>(Func<TValue, T>, Func<TError, T>)
Calls one of two functions depending on whether the result is successful.
Declaration
public T Switch<T>(Func<TValue, T> onValue, Func<TError, T> onError)
Parameters
| Type | Name | Description |
|---|---|---|
| Func<TValue, T> | onValue | |
| Func<TError, T> | onError |
Returns
| Type | Description |
|---|---|
| T |
Type Parameters
| Name | Description |
|---|---|
| T |
Then<TResult>(Func<TValue, Result<TResult, TError>>)
Chains another Result<TValue, TError>-producing operation from another.
Declaration
public Result<TResult, TError> Then<TResult>(Func<TValue, Result<TResult, TError>> f)
Parameters
| Type | Name | Description |
|---|---|---|
| Func<TValue, Result<TResult, TError>> | f |
Returns
| Type | Description |
|---|---|
| Result<TResult, TError> |
Type Parameters
| Name | Description |
|---|---|
| TResult |
Remarks
This is a monad bind operation.
Conceptually, it is the same as passing f to OnValue<TResult>(Func<TValue, TResult>)
and then "flattening" the result.
ToString()
Returns the string representation of the underlying value or error.
Declaration
public override string ToString()
Returns
| Type | Description |
|---|---|
| String |
Overrides
Operators
| Improve this Doc View SourceEquality(Result<TValue, TError>, Result<TValue, TError>)
Determines whether two instances of Result<TValue, TError> have the same value.
Declaration
public static bool operator ==(Result<TValue, TError> lhs, Result<TValue, TError> rhs)
Parameters
| Type | Name | Description |
|---|---|---|
| Result<TValue, TError> | lhs | |
| Result<TValue, TError> | rhs |
Returns
| Type | Description |
|---|---|
| Boolean |
Implicit(TValue to Result<TValue, TError>)
Converts an instance of a type to an Result<TValue, TError>.
Declaration
public static implicit operator Result<TValue, TError>(TValue value)
Parameters
| Type | Name | Description |
|---|---|---|
| TValue | value |
Returns
| Type | Description |
|---|---|
| Result<TValue, TError> |
Implicit(TError to Result<TValue, TError>)
Converts an instance of a type to an Result<TValue, TError>.
Declaration
public static implicit operator Result<TValue, TError>(TError error)
Parameters
| Type | Name | Description |
|---|---|---|
| TError | error |
Returns
| Type | Description |
|---|---|
| Result<TValue, TError> |
Inequality(Result<TValue, TError>, Result<TValue, TError>)
Determines whether two instances of Result<TValue, TError> have different values.
Declaration
public static bool operator !=(Result<TValue, TError> lhs, Result<TValue, TError> rhs)
Parameters
| Type | Name | Description |
|---|---|---|
| Result<TValue, TError> | lhs | |
| Result<TValue, TError> | rhs |
Returns
| Type | Description |
|---|---|
| Boolean |