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 |