Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 36 additions & 15 deletions src/RunnethOverStudio.AppToolkit/Core/ProcessResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,24 @@
namespace RunnethOverStudio.AppToolkit.Core;

/// <summary>
/// Represents a result of an operation which can be the actual result or exception.
/// Represents the result of an operation that either produced a value or intentionally captured an exception.
/// </summary>
/// <typeparam name="T">The type of the value stored in the Result.</typeparam>
/// <typeparam name="T">The type of the value stored in the result.</typeparam>
/// <remarks>
/// Heavily inspired by the <see href="https://dotnet.github.io/dotNext/features/core/result.html">Result type</see> from .NEXT (dotNext).
/// <para>
/// Use <see cref="ProcessResult{T}"/> when an exception is part of the operation's explicit result contract, such as
/// when a boundary deliberately captures an exception so the caller can inspect, forward, or defer it.
/// </para>
/// <para>
/// Expected application or domain outcomes should not be converted into exceptions merely to fit this type.
/// When callers are expected to branch on a known set of non-exceptional failure states, use
/// <see cref="ProcessResult{T,TError}"/> instead. Unexpected or exceptional failures should normally continue to
/// propagate as exceptions unless the boundary has a concrete reason to capture them.
/// </para>
/// <para>
/// Heavily inspired by the <see href="https://dotnet.github.io/dotNext/features/core/result.html">Result type</see>
/// from .NEXT (dotNext).
/// </para>
/// </remarks>
[Serializable]
public class ProcessResult<T>
Expand All @@ -27,15 +40,19 @@ public class ProcessResult<T>
public ProcessResult(T value) => this._value = value;

/// <summary>
/// Initializes a new unsuccessful result.
/// Initializes a new unsuccessful result containing a captured exception.
/// </summary>
/// <param name="error">The exception representing error. Cannot be <see langword="null"/>.</param>
/// <param name="error">The exception representing the failure. Cannot be <see langword="null"/>.</param>
/// <remarks>
/// This constructor is intended for failures that are exceptional in nature. Prefer
/// <see cref="ProcessResult{T,TError}"/> for expected failure states that are part of normal application flow.
/// </remarks>
public ProcessResult(Exception error) : this(ExceptionDispatchInfo.Capture(error)) { }

/// <summary>
/// Extracts the actual result.
/// Extracts the successful value or rethrows the captured exception.
/// </summary>
/// <exception cref="Exception">This result is not successful.</exception>
/// <exception cref="Exception">This result contains a captured exception.</exception>
public T Value
{
get
Expand All @@ -46,20 +63,20 @@ public T Value
}

/// <summary>
/// Gets the value if present; otherwise return default value.
/// Gets the value if present; otherwise returns the default value.
/// </summary>
/// <value>The value, if present, otherwise <c>default</c>.</value>
/// <value>The value, if present; otherwise, <c>default</c>.</value>
public T? ValueOrDefault => _value;

/// <summary>
/// Gets exception associated with this result.
/// Gets the exception associated with this result, or <see langword="null"/> when successful.
/// </summary>
public Exception? Error => _exception?.SourceException;

/// <summary>
/// Indicates that the result is successful.
/// Indicates whether the result contains a successful value rather than a captured exception.
/// </summary>
/// <value><see langword="true"/> if this result is successful; <see langword="false"/> if this result represents exception.</value>
/// <value><see langword="true"/> when successful; otherwise, <see langword="false"/>.</value>
[MemberNotNullWhen(false, nameof(Error))]
public bool IsSuccessful => _exception is null;

Expand All @@ -79,12 +96,16 @@ public T Value
/// Creates a failed <see cref="ProcessResult{T}"/> containing the specified exception.
/// </summary>
/// <param name="error">The exception representing the failure. Cannot be <see langword="null"/>.</param>
/// <returns>A <see cref="ProcessResult{T}"/> representing a failed operation.</returns>
/// <returns>A <see cref="ProcessResult{T}"/> representing an exceptional failure.</returns>
/// <remarks>
/// Prefer <see cref="ProcessResult{T,TError}"/> when the failure is an expected outcome that callers should handle
/// without exception semantics.
/// </remarks>
public static ProcessResult<T> Failure(Exception error) => new(error);

/// <summary>
/// Logs a failure message and exception using the specified logger and log level, so long as logging is enabled.
/// Then returns a failed <see cref="ProcessResult{T}"/> containing a new exception with the provided message and
/// Then returns a failed <see cref="ProcessResult{T}"/> containing a new exception with the provided message and
/// the original exception as its inner exception.
/// </summary>
/// <param name="message">The message to log and to use as the new exception's message.</param>
Expand All @@ -109,7 +130,7 @@ public static ProcessResult<T> LogAndForwardException(string message, Exception
/// </summary>
/// <param name="result">The result to evaluate.</param>
/// <returns>
/// <c>true</c> if the result is successful; otherwise, <c>false</c>.
/// <see langword="true"/> if the result is successful; otherwise, <see langword="false"/>.
/// </returns>
public static implicit operator bool(ProcessResult<T> result) => result.IsSuccessful;

Expand Down
165 changes: 165 additions & 0 deletions src/RunnethOverStudio.AppToolkit/Core/ProcessResultOfError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace RunnethOverStudio.AppToolkit.Core;

/// <summary>
/// Represents the result of an operation that either produced a value or an expected, explicitly modeled error.
/// </summary>
/// <typeparam name="T">The type of the successful value stored in the result.</typeparam>
/// <typeparam name="TError">
/// An enumeration describing expected failure states. The default enumeration value is reserved to represent success.
/// </typeparam>
/// <remarks>
/// <para>
/// Use <see cref="ProcessResult{T,TError}"/> when failure is an anticipated part of normal application or domain flow
/// and callers are expected to branch on a known error state. Examples include a requested resource not being found,
/// a destination no longer existing, or another explicitly modeled business outcome.
/// </para>
/// <para>
/// This type is intentionally not an exception container. Unexpected or exceptional failures should normally propagate
/// as exceptions. When a boundary deliberately needs to capture an exception as part of its result contract, use
/// <see cref="ProcessResult{T}"/> instead.
/// </para>
/// <para>
/// The default value of <typeparamref name="TError"/> is reserved for success. Define a neutral member such as
/// <c>None = 0</c> when naming that state improves readability, and use non-default values for actual errors.
/// </para>
/// <para>
/// Heavily inspired by the <see href="https://dotnet.github.io/dotNext/features/core/result.html">Result type</see>
/// from .NEXT (dotNext).
/// </para>
/// </remarks>
[Serializable]
public class ProcessResult<T, TError> where TError : struct, Enum
{
private readonly T _value;
private readonly TError _error;

/// <summary>
/// Initializes a new successful result.
/// </summary>
/// <param name="value">The value to be stored as the successful result.</param>
public ProcessResult(T value)
{
_value = value;
_error = default;
}

/// <summary>
/// Initializes a new unsuccessful result containing an expected error.
/// </summary>
/// <param name="error">The expected error describing why the operation was unsuccessful.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="error"/> is the default value of <typeparamref name="TError"/>, which is reserved for success.
/// </exception>
public ProcessResult(TError error)
{
if (EqualityComparer<TError>.Default.Equals(error, default))
{
throw new ArgumentOutOfRangeException(nameof(error), "The default error value is reserved for successful results.");
}

_value = default!;
_error = error;
}

/// <summary>
/// Extracts the successful value.
/// </summary>
/// <exception cref="InvalidOperationException">
/// This result contains an expected error rather than a successful value.
/// </exception>
public T Value
{
get
{
Validate();
return _value;
}
}

/// <summary>
/// Gets the successful value when present; otherwise returns the default value.
/// </summary>
/// <value>The successful value, if present; otherwise, <c>default</c>.</value>
public T? ValueOrDefault => IsSuccessful ? _value : default;

/// <summary>
/// Gets the expected error associated with the result.
/// </summary>
/// <remarks>
/// The default value represents success and is not an error. Check <see cref="IsSuccessful"/> before interpreting
/// this property as a failure state.
/// </remarks>
public TError Error => _error;

/// <summary>
/// Indicates whether the result contains a successful value rather than an expected error.
/// </summary>
/// <value><see langword="true"/> when successful; otherwise, <see langword="false"/>.</value>
public bool IsSuccessful => EqualityComparer<TError>.Default.Equals(_error, default);

/// <summary>
/// Returns a string that represents the current result, indicating success or failure and the associated value or error.
/// </summary>
public override string ToString() => IsSuccessful ? $"Success({_value})" : $"Failure({_error})";

/// <summary>
/// Creates a successful <see cref="ProcessResult{T,TError}"/> containing the specified value.
/// </summary>
/// <param name="value">The value to store in the successful result.</param>
/// <returns>A <see cref="ProcessResult{T,TError}"/> representing a successful operation.</returns>
public static ProcessResult<T, TError> Success(T value) => new(value);

/// <summary>
/// Creates a failed <see cref="ProcessResult{T,TError}"/> containing the specified expected error.
/// </summary>
/// <param name="error">The expected error describing why the operation was unsuccessful.</param>
/// <returns>A <see cref="ProcessResult{T,TError}"/> representing an expected failure.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="error"/> is the default value of <typeparamref name="TError"/>, which is reserved for success.
/// </exception>
public static ProcessResult<T, TError> Failure(TError error) => new(error);

/// <summary>
/// Attempts to extract the successful value.
/// </summary>
/// <param name="value">The successful value when present; otherwise, the default value.</param>
/// <returns><see langword="true"/> when successful; otherwise, <see langword="false"/>.</returns>
public bool TryGet([MaybeNullWhen(false)] out T value)
{
value = _value;
return IsSuccessful;
}

/// <summary>
/// Defines an implicit conversion from <see cref="ProcessResult{T,TError}"/> to <see cref="bool"/>.
/// </summary>
/// <param name="result">The result to evaluate.</param>
/// <returns>
/// <see langword="true"/> if the result is successful; otherwise, <see langword="false"/>.
/// </returns>
public static implicit operator bool(ProcessResult<T, TError> result) => result.IsSuccessful;

/// <summary>
/// Defines an explicit conversion from <see cref="ProcessResult{T,TError}"/> to the underlying value of type <typeparamref name="T"/>.
/// </summary>
/// <param name="result">The result to extract the value from.</param>
/// <returns>The value contained in the result if it is successful.</returns>
/// <exception cref="InvalidOperationException">
/// Thrown if the result is not successful and an attempt is made to extract the value.
/// </exception>
public static explicit operator T(ProcessResult<T, TError> result) => result.Value;

[StackTraceHidden]
private void Validate()
{
if (!IsSuccessful)
{
throw new InvalidOperationException($"The process result is unsuccessful with error '{_error}'.");
}
}
}
Loading