diff --git a/src/RunnethOverStudio.AppToolkit/Core/ProcessResult.cs b/src/RunnethOverStudio.AppToolkit/Core/ProcessResult.cs index 56830a1..ac7146f 100644 --- a/src/RunnethOverStudio.AppToolkit/Core/ProcessResult.cs +++ b/src/RunnethOverStudio.AppToolkit/Core/ProcessResult.cs @@ -8,11 +8,24 @@ namespace RunnethOverStudio.AppToolkit.Core; /// -/// 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. /// -/// The type of the value stored in the Result. +/// The type of the value stored in the result. /// -/// Heavily inspired by the Result type from .NEXT (dotNext). +/// +/// Use 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. +/// +/// +/// 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 +/// instead. Unexpected or exceptional failures should normally continue to +/// propagate as exceptions unless the boundary has a concrete reason to capture them. +/// +/// +/// Heavily inspired by the Result type +/// from .NEXT (dotNext). +/// /// [Serializable] public class ProcessResult @@ -27,15 +40,19 @@ public class ProcessResult public ProcessResult(T value) => this._value = value; /// - /// Initializes a new unsuccessful result. + /// Initializes a new unsuccessful result containing a captured exception. /// - /// The exception representing error. Cannot be . + /// The exception representing the failure. Cannot be . + /// + /// This constructor is intended for failures that are exceptional in nature. Prefer + /// for expected failure states that are part of normal application flow. + /// public ProcessResult(Exception error) : this(ExceptionDispatchInfo.Capture(error)) { } /// - /// Extracts the actual result. + /// Extracts the successful value or rethrows the captured exception. /// - /// This result is not successful. + /// This result contains a captured exception. public T Value { get @@ -46,20 +63,20 @@ public T Value } /// - /// Gets the value if present; otherwise return default value. + /// Gets the value if present; otherwise returns the default value. /// - /// The value, if present, otherwise default. + /// The value, if present; otherwise, default. public T? ValueOrDefault => _value; /// - /// Gets exception associated with this result. + /// Gets the exception associated with this result, or when successful. /// public Exception? Error => _exception?.SourceException; /// - /// Indicates that the result is successful. + /// Indicates whether the result contains a successful value rather than a captured exception. /// - /// if this result is successful; if this result represents exception. + /// when successful; otherwise, . [MemberNotNullWhen(false, nameof(Error))] public bool IsSuccessful => _exception is null; @@ -79,12 +96,16 @@ public T Value /// Creates a failed containing the specified exception. /// /// The exception representing the failure. Cannot be . - /// A representing a failed operation. + /// A representing an exceptional failure. + /// + /// Prefer when the failure is an expected outcome that callers should handle + /// without exception semantics. + /// public static ProcessResult Failure(Exception error) => new(error); /// /// Logs a failure message and exception using the specified logger and log level, so long as logging is enabled. - /// Then returns a failed containing a new exception with the provided message and + /// Then returns a failed containing a new exception with the provided message and /// the original exception as its inner exception. /// /// The message to log and to use as the new exception's message. @@ -109,7 +130,7 @@ public static ProcessResult LogAndForwardException(string message, Exception /// /// The result to evaluate. /// - /// true if the result is successful; otherwise, false. + /// if the result is successful; otherwise, . /// public static implicit operator bool(ProcessResult result) => result.IsSuccessful; diff --git a/src/RunnethOverStudio.AppToolkit/Core/ProcessResultOfError.cs b/src/RunnethOverStudio.AppToolkit/Core/ProcessResultOfError.cs new file mode 100644 index 0000000..e90e47f --- /dev/null +++ b/src/RunnethOverStudio.AppToolkit/Core/ProcessResultOfError.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace RunnethOverStudio.AppToolkit.Core; + +/// +/// Represents the result of an operation that either produced a value or an expected, explicitly modeled error. +/// +/// The type of the successful value stored in the result. +/// +/// An enumeration describing expected failure states. The default enumeration value is reserved to represent success. +/// +/// +/// +/// Use 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. +/// +/// +/// 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 +/// instead. +/// +/// +/// The default value of is reserved for success. Define a neutral member such as +/// None = 0 when naming that state improves readability, and use non-default values for actual errors. +/// +/// +/// Heavily inspired by the Result type +/// from .NEXT (dotNext). +/// +/// +[Serializable] +public class ProcessResult where TError : struct, Enum +{ + private readonly T _value; + private readonly TError _error; + + /// + /// Initializes a new successful result. + /// + /// The value to be stored as the successful result. + public ProcessResult(T value) + { + _value = value; + _error = default; + } + + /// + /// Initializes a new unsuccessful result containing an expected error. + /// + /// The expected error describing why the operation was unsuccessful. + /// + /// is the default value of , which is reserved for success. + /// + public ProcessResult(TError error) + { + if (EqualityComparer.Default.Equals(error, default)) + { + throw new ArgumentOutOfRangeException(nameof(error), "The default error value is reserved for successful results."); + } + + _value = default!; + _error = error; + } + + /// + /// Extracts the successful value. + /// + /// + /// This result contains an expected error rather than a successful value. + /// + public T Value + { + get + { + Validate(); + return _value; + } + } + + /// + /// Gets the successful value when present; otherwise returns the default value. + /// + /// The successful value, if present; otherwise, default. + public T? ValueOrDefault => IsSuccessful ? _value : default; + + /// + /// Gets the expected error associated with the result. + /// + /// + /// The default value represents success and is not an error. Check before interpreting + /// this property as a failure state. + /// + public TError Error => _error; + + /// + /// Indicates whether the result contains a successful value rather than an expected error. + /// + /// when successful; otherwise, . + public bool IsSuccessful => EqualityComparer.Default.Equals(_error, default); + + /// + /// Returns a string that represents the current result, indicating success or failure and the associated value or error. + /// + public override string ToString() => IsSuccessful ? $"Success({_value})" : $"Failure({_error})"; + + /// + /// Creates a successful containing the specified value. + /// + /// The value to store in the successful result. + /// A representing a successful operation. + public static ProcessResult Success(T value) => new(value); + + /// + /// Creates a failed containing the specified expected error. + /// + /// The expected error describing why the operation was unsuccessful. + /// A representing an expected failure. + /// + /// is the default value of , which is reserved for success. + /// + public static ProcessResult Failure(TError error) => new(error); + + /// + /// Attempts to extract the successful value. + /// + /// The successful value when present; otherwise, the default value. + /// when successful; otherwise, . + public bool TryGet([MaybeNullWhen(false)] out T value) + { + value = _value; + return IsSuccessful; + } + + /// + /// Defines an implicit conversion from to . + /// + /// The result to evaluate. + /// + /// if the result is successful; otherwise, . + /// + public static implicit operator bool(ProcessResult result) => result.IsSuccessful; + + /// + /// Defines an explicit conversion from to the underlying value of type . + /// + /// The result to extract the value from. + /// The value contained in the result if it is successful. + /// + /// Thrown if the result is not successful and an attempt is made to extract the value. + /// + public static explicit operator T(ProcessResult result) => result.Value; + + [StackTraceHidden] + private void Validate() + { + if (!IsSuccessful) + { + throw new InvalidOperationException($"The process result is unsuccessful with error '{_error}'."); + } + } +}