rd-net/Lifetimes/Core/Result.cs (317 lines of code) (raw):

using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using JetBrains.Lifetimes; using System.Threading.Tasks; using JetBrains.Diagnostics; using JetBrains.Threading; using System.Runtime.ExceptionServices; #nullable disable namespace JetBrains.Core { /// <summary> /// Helper methods for <see cref="Result{T}" /> and <see cref="Result{T,T}"/> building /// </summary> [PublicAPI] public static class Result { /// <summary> /// Message that is being applied to Result.Fail when no message provided /// </summary> public const string EmptyFailMessage = "<<empty fail message>>"; /// <summary> /// Creates successful <see cref="Result{T}"/> with value <see cref="value"/> /// </summary> /// <param name="value"></param> /// <typeparam name="T"></typeparam> /// <returns>Result with <see cref="Result{T}.Succeed"/> == true</returns> public static Result<T> Success<T>(T value) { return new Result<T>(value, null); } /// <summary> /// Creates failed <see cref="Result{T}"/> /// </summary> /// <param name="exception"></param> /// <param name="captureStackTrace">Try to capture exception stack (if any), could be unwind by <see cref="Result{T}.Unwrap"/> </param> /// <returns>Result with <see cref="Result{T}.Succeed"/> == false. Returned type could be implicitly casted to any <see cref="Result{T}"/></returns> /// <exception cref="ArgumentNullException">if <see cref="exception"/> is null</exception> public static Result<Nothing> Fail([NotNull] Exception exception, bool captureStackTrace = false) { if (exception == null) throw new ArgumentNullException(nameof(exception)); return new Result<Nothing>(null, captureStackTrace ? ExceptionDispatchInfo.Capture(exception) : (object) exception); } /// <summary> /// Creates failed <see cref="Result{T, T}"/> with corresponding <see cref="failValue"/> /// </summary> /// <param name="exception"></param> /// <param name="failValue">Special user-defined value provided for failed Result</param> /// <param name="captureStackTrace">Try to capture exception stack (if any), could be unwind by <see cref="Result{T}.Unwrap"/> </param> /// <returns>Result with <see cref="Result{TSuccess,TFailure}.Succeed"/> == false. Returned type could be implicitly casted to any <see cref="Result{T, T}"/></returns> /// <exception cref="ArgumentNullException">if <see cref="exception"/> is null</exception> public static Result<Nothing, TFailure> Fail<TFailure>([NotNull] Exception exception, TFailure failValue, bool captureStackTrace = false) { if (exception == null) throw new ArgumentNullException(nameof(exception)); return new Result<Nothing, TFailure>(null, captureStackTrace ? ExceptionDispatchInfo.Capture(exception) : (object) exception, failValue); } /// <summary> /// Creates failed <see cref="Result{T}"/> with <see cref="ResultException"/> that wraps provided <see cref="message"/> /// </summary> /// <param name="message">Reason of failure. If not defined, <see cref="EmptyFailMessage"/> is used.</param> /// <returns>Result with <see cref="Result{T}.Succeed"/> == false. Returned type could be implicitly casted to any <see cref="Result{T}"/></returns> public static Result<Nothing> Fail(string message = null) { return Fail(new ResultException(message ?? EmptyFailMessage)); } /// <summary> /// Creates failed <see cref="Result{T, T}"/> with <see cref="ResultException"/> that wraps provided <see cref="message"/> /// </summary> /// <param name="message">Reason of failure. If not defined, <see cref="EmptyFailMessage"/> is used.</param> /// <param name="failValue">Special user-defined value provided for failed Result</param> /// <returns>Result with <see cref="Result{TSuccess,TFailure}.Succeed"/> == false. Returned type could be implicitly casted to any <see cref="Result{T}"/></returns> public static Result<Nothing, TFailure> Fail<TFailure>(string message, TFailure failValue) { return Fail(new ResultException(message ?? EmptyFailMessage), failValue); } /// <summary> /// Creates failed <see cref="Result{T}"/> with message= <see cref="EmptyFailMessage"/> and user-defined failure parameter /// </summary> /// <param name="failValue">Special user-defined value provided for failed Result</param> /// <returns>Result with <see cref="Result{TSuccess,TFailure}.Succeed"/> == false. Returned type could be implicitly casted to any <see cref="Result{T}"/></returns> public static Result<Nothing, TFailure> FailWithValue<TFailure>(TFailure failValue) { return Fail((string) null, failValue); } /// <summary> /// Creates special failed <see cref="Result{T}"/> that wraps <see cref="OperationCanceledException"/> /// </summary> /// <returns>Result with <see cref="Result{T}.Succeed"/> == false and <see cref="Result{T}.Canceled"/> == true. Returned type could be implicitly casted to any <see cref="Result{T}"/></returns> public static Result<Nothing> Canceled() { return Canceled(new OperationCanceledException( Lifetime.Terminated )); } /// <summary> /// Creates special failed <see cref="Result{T}"/> that wraps <see cref="OperationCanceledException"/> /// </summary> /// <param name="exception">Captured OCE that lead to this cancellation</param> /// <param name="captureStackTrace">Try to capture exception stack (if any), could be unwind by <see cref="Result{T}.Unwrap"/>. </param> /// <returns>Result with <see cref="Result{T}.Succeed"/> == false and <see cref="Result{T}.Canceled"/> == true. Returned type could be implicitly casted to any <see cref="Result{T}"/></returns> /// <exception cref="ArgumentNullException">if <see cref="exception"/> is null</exception> public static Result<Nothing> Canceled([NotNull] OperationCanceledException exception, bool captureStackTrace = false) { return Fail(exception, captureStackTrace); } /// <summary> /// Void succeed result for <see cref="Unit"/> type /// </summary> public static Result<Unit> Unit = Success(Core.Unit.Instance); /// <summary> /// Wrap execution of <see cref="f"/>() into <see cref="Result{TRes}"/>. /// </summary> /// <param name="f">Function to execute</param> /// <typeparam name="TRes">type argument of returned Result</typeparam> /// <returns>Succeed result with <see cref="Result{T}.Value"/> == f() if no exception happened during <see cref="f"/> execution. Failed result with corresponding exception otherwise </returns> #if !NETCOREAPP [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions] #endif public static Result<TRes> Wrap<TRes>([NotNull] Func<TRes> f) { try { return Success(f()); } catch (Exception e) { return Fail(e, true); } } /// <summary> /// Wrap execution of <see cref="f"/>() into <see cref="Result{Unit}"/>. /// </summary> /// <param name="f">Action to execute</param> /// <returns>Succeed result with <see cref="Result.Unit"/> if no exception happened during <see cref="f"/> execution. Failed result with corresponding exception otherwise </returns> #if !NETCOREAPP [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions] #endif public static Result<Unit> Wrap([NotNull] Action f) { try { f(); return Unit; } catch (Exception e) { return Fail(e, true); } } /// <summary> /// Wrap execution of <see cref="f"/>(<see cref="param"/>) into <see cref="Result{Unit}"/>. /// </summary> /// <param name="f">Action with parameter to execute</param> /// <param name="param">function argument</param> /// <typeparam name="T"><see cref="param"/> type</typeparam> /// <returns>Succeed result with <see cref="Result.Unit"/> if no exception happened during <see cref="f"/> execution. Failed result with corresponding exception otherwise </returns> #if !NETCOREAPP [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions] #endif public static Result<Unit> Wrap<T>([NotNull] Action<T> f, T param) { try { f(param); return Unit; } catch (Exception e) { return Fail(e, true); } } /// <summary> /// Wrap execution of <see cref="f"/>(<see cref="param"/>) into <see cref="Result{TRes}"/>. /// </summary> /// <param name="f">Function with parameter to execute</param> /// <param name="param">function argument</param> /// <typeparam name="T"><see cref="param"/> type</typeparam> /// <typeparam name="TRes">type argument of returned Result</typeparam> /// <returns>Succeed result with <see cref="Result{T}.Value"/> == f(param) if no exception happened during <see cref="f"/> execution. Failed result with corresponding exception otherwise </returns> #if !NETCOREAPP [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions] #endif public static Result<TRes> Wrap<T, TRes>([NotNull] Func<T, TRes> f, T param) { try { return Success(f(param)); } catch (Exception e) { return Fail(e, true); } } /// <summary> /// Transforms this <see cref="Result"/> into <see cref="Task"/>. /// <see cref="Result{T}.Succeed"/> corresponds to <see cref="Task"/> in <see cref="Result{Task}.Value"/>. /// <see cref="Result{Task}.Canceled"/> corresponds to completed task with <see cref="Task.IsCanceled"/> /// <see cref="Result{Task}.FailedNotCanceled"/> corresponds to completed task with <see cref="Task.IsFaulted"/> and <see cref="Exception"/> /// </summary> /// <param name="result">this</param> /// <returns><see cref="Task"/> that corresponds <see cref="result"/></returns> public static Task UnwrapTask(this Result<Task> result) { if (result.Succeed) return result.Value; if (!result.Canceled) return Task.FromException(result.Exception); if (result.Exception is OperationCanceledException oce) return Task.FromCanceled(oce.CancellationToken); else return Task.FromCanceled(Lifetime.Terminated); } /// <summary> /// Transforms this <see cref="Result"/> into <see cref="Task"/>. /// <see cref="Result{T}.Succeed"/> corresponds to <see cref="Task"/> in <see cref="Result{Task}.Value"/>. /// <see cref="Result{Task}.Canceled"/> corresponds to completed task with <see cref="Task.IsCanceled"/> /// <see cref="Result{Task}.FailedNotCanceled"/> corresponds to completed task with <see cref="Task.IsFaulted"/> and <see cref="Exception"/> /// </summary> /// <param name="result">this</param> /// <typeparam name="T">type parameter of returning task</typeparam> /// <returns><see cref="Task"/> that corresponds <see cref="result"/></returns> public static Task<T> UnwrapTask<T>(this Result<Task<T>> result) { if (result.Succeed) return result.Value; if (!result.Canceled) return Task.FromException<T>(result.Exception); if (result.Exception is OperationCanceledException oce) return Task.FromCanceled<T>(oce.CancellationToken); else return Task.FromCanceled<T>(Lifetime.Terminated); } /// <summary> /// Wrap completed task's result into <see cref="Result{T}"/> or throw <see cref="InvalidOperationException"/> is task is <c>!</c><see cref="Task.IsCompleted"/> /// </summary> /// <param name="task">Must be finished (<see cref="Task.IsCompleted"/><c>==true</c>) or <see cref="InvalidOperationException"/> will be throws</param> /// <typeparam name="T"></typeparam> /// <returns><see cref="Success{T}"/>(task.<see cref="Task{T}.Result"/>) or <see cref="Fail(System.Exception, bool)"/>(task.<see cref="Task.Exception"/>)</returns> /// <exception cref="InvalidOperationException">in case of <c>!task.</c><see cref="Task.IsCompleted"/></exception> public static Result<T> FromCompletedTask<T>(Task<T> task) { if (!task.IsCompleted) throw new InvalidOperationException($"Task must be completed to convert into result but was in state: {task.Status}"); return task.Status == TaskStatus.RanToCompletion ? Success(task.Result) : Fail(task.Exception.NotNull($"Exception must always exist for task with status: {task.Status}")); } } /// <summary> /// Monad that can can have two states: <see cref="Succeed"/> and Fail (!<see cref="Succeed"/>). Also we distinct special type of Fail: <see cref="Canceled"/>. /// </summary> /// <typeparam name="T"></typeparam> [PublicAPI] public readonly struct Result<T> : IEquatable<Result<T>> { /// <summary> /// Value in case of <see cref="Succeed"/>, default(T) otherwise /// </summary> public readonly T Value; /// <summary> /// It this field not null, this Result is !<see cref="Succeed"/> and vise versa. /// </summary> internal readonly object ExceptionOrExceptionDispatchInfo; /// <summary> /// Exception in case of (!<see cref="Succeed"/>), null otherwise /// </summary> public Exception Exception => ExceptionOrExceptionDispatchInfo is Exception ex ? ex : (ExceptionOrExceptionDispatchInfo as ExceptionDispatchInfo)?.SourceException; /// <summary> /// Exception message in case of (!<see cref="Succeed"/>), null otherwise /// </summary> public string FailMessage => Exception?.Message; /// <summary> /// Shouldn't be invoked in user's code /// </summary> /// <param name="success"></param> /// <param name="failure"></param> internal Result(T success, object failure) { Value = success; ExceptionOrExceptionDispatchInfo = failure; } /// <summary> /// Is result successful /// </summary> public bool Succeed => ExceptionOrExceptionDispatchInfo == null; /// <summary> /// (!<see cref="Succeed"/>) and (<see cref="Canceled"/> /// </summary> public bool FailedNotCanceled => !Succeed && !Canceled; /// <summary> /// Exception has specials type of <see cref="OperationCanceledException"/> or <see cref="AggregateException"/> that has <see cref="OperationCanceledException"/> inside. /// </summary> public bool Canceled => Exception.IsOperationCanceled(); public static implicit operator Result<T>(Result<Nothing> me) { return new Result<T>(default(T), me.ExceptionOrExceptionDispatchInfo); } /// <summary> /// Transform this result into new one with given function. if !<see cref="Succeed"/>, stays untouched./> /// </summary> /// <param name="transform"></param> /// <typeparam name="TRes"></typeparam> /// <returns></returns> public Result<TRes> Map<TRes>(Func<T, TRes> transform) { return Succeed ? Result.Wrap(transform, Value) : Result.Fail(Exception); } /// <summary> /// Map without lambda. Success{Anything} -> Success{<see cref="successValue"/>}. Fail -> Fail /// </summary> /// <param name="successValue">In case of success we always create successful result with this value</param> /// <typeparam name="TRes"></typeparam> /// <returns></returns> public Result<TRes> Map<TRes>(TRes successValue) { return Succeed ? Result.Success(successValue) : Result.Fail(Exception); } /// <summary> /// Returns <see cref="Value"/> if <see cref="Succeed"/>, throws <see cref="Exception"/> otherwise /// </summary> /// <returns> <see cref="Value"/> if <see cref="Succeed"/> </returns> /// <exception cref="Exception">if !<see cref="Succeed"/></exception> public T Unwrap() { if (Succeed) return Value; switch (ExceptionOrExceptionDispatchInfo) { case Exception ex: throw ex; case ExceptionDispatchInfo edi: edi.Throw(); return Nothing.Unreachable<T>(); default: return Nothing.Unreachable<T>(); } } /// <summary> /// Transforms this <see cref="Result"/> into <see cref="Task"/> in <see cref="Task.IsCompleted"/> state state. /// <see cref="Succeed"/> corresponds to <see cref="Task.IsRanToCompletion"/>. /// <see cref="Canceled"/> corresponds to <see cref="Task.IsCanceled"/> /// <see cref="FailedNotCanceled"/> corresponds to <see cref="Task.IsFaulted"/> with <see cref="Exception"/> /// </summary> /// <returns><see cref="Task"/> in <see cref="Task.IsCompleted"/> state</returns> public Task<T> AsCompletedTask() { if (Succeed) return Task.FromResult(Value); if (!Canceled) return Task.FromException<T>(Exception); if (Exception is OperationCanceledException oce) return Task.FromCanceled<T>(oce.CancellationToken); else return Task.FromCanceled<T>(Lifetime.Terminated); } public override string ToString() { var status = Succeed ? "Success(" + Value +")": Canceled ? "Canceled" : "Fail(" + FailMessage+")"; return "Result." + status; } public bool Equals(Result<T> other) { return EqualityComparer<T>.Default.Equals(Value, other.Value) && Equals(Exception, other.Exception); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; return obj is Result<T> other && Equals(other); } public override int GetHashCode() { unchecked { return (EqualityComparer<T>.Default.GetHashCode(Value) * 397) ^ (Exception != null ? Exception.GetHashCode() : 0); } } public static bool operator ==(Result<T> left, Result<T> right) { return left.Equals(right); } public static bool operator !=(Result<T> left, Result<T> right) { return !left.Equals(right); } } /// <summary> /// Special kind of <see cref="Result{T}"/> to store custom <see cref="FailValue"/> in case of !<see cref="Succeed"/>. /// </summary> /// <typeparam name="TSuccess"></typeparam> /// <typeparam name="TFailure"></typeparam> [PublicAPI] public readonly struct Result<TSuccess, TFailure> : IEquatable<Result<TSuccess, TFailure>> { /// <summary> /// Value in case of <see cref="Succeed"/>, default(T) otherwise /// </summary> public readonly TSuccess Value; /// <summary> /// It this field not null, this Result is !<see cref="Succeed"/> and vise versa. /// </summary> internal readonly object ExceptionOrExceptionDispatchInfo; /// <summary> /// Exception in case of (!<see cref="Succeed"/>), null otherwise /// </summary> public Exception Exception => ExceptionOrExceptionDispatchInfo is Exception ex ? ex : (ExceptionOrExceptionDispatchInfo as ExceptionDispatchInfo)?.SourceException; public string FailMessage => Exception?.Message; public readonly TFailure FailValue; internal Result(TSuccess success, object failure, TFailure failValue) { Value = success; ExceptionOrExceptionDispatchInfo = failure; FailValue = failValue; } public bool Succeed => ExceptionOrExceptionDispatchInfo == null; public bool FailedNotCanceled => !Succeed && !Canceled; public bool Canceled => Exception is OperationCanceledException ||((Exception as AggregateException)?.Flatten().InnerExceptions.Any(ex => ex is OperationCanceledException) ?? false) ; public static implicit operator Result<TSuccess, TFailure>(Result<Nothing, TFailure> me) { return new Result<TSuccess, TFailure>(default(TSuccess), me.ExceptionOrExceptionDispatchInfo, me.FailValue); } public static implicit operator Result<TSuccess>(Result<TSuccess, TFailure> me) { return new Result<TSuccess>(me.Value, me.ExceptionOrExceptionDispatchInfo); } public static implicit operator Result<TSuccess, TFailure>(Result<TSuccess> me) { return new Result<TSuccess, TFailure>(me.Value, me.ExceptionOrExceptionDispatchInfo, default(TFailure)); } public TSuccess Unwrap() { if (Succeed) return Value; switch (ExceptionOrExceptionDispatchInfo) { case Exception ex: throw ex; case ExceptionDispatchInfo edi: edi.Throw(); return Nothing.Unreachable<TSuccess>(); default: return Nothing.Unreachable<TSuccess>(); } } public override string ToString() { var status = Succeed ? "Success(" + Value +")": Canceled ? "Canceled" : $"Fail({FailMessage}, {FailValue})"; return "Result." + status; } public bool Equals(Result<TSuccess, TFailure> other) { return EqualityComparer<TSuccess>.Default.Equals(Value, other.Value) && Equals(Exception, other.Exception) && EqualityComparer<TFailure>.Default.Equals(FailValue, other.FailValue); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; return obj is Result<TSuccess, TFailure> other && Equals(other); } public override int GetHashCode() { unchecked { var hashCode = EqualityComparer<TSuccess>.Default.GetHashCode(Value); hashCode = (hashCode * 397) ^ (Exception != null ? Exception.GetHashCode() : 0); hashCode = (hashCode * 397) ^ EqualityComparer<TFailure>.Default.GetHashCode(FailValue); return hashCode; } } public static bool operator ==(Result<TSuccess, TFailure> left, Result<TSuccess, TFailure> right) { return left.Equals(right); } public static bool operator !=(Result<TSuccess, TFailure> left, Result<TSuccess, TFailure> right) { return !left.Equals(right); } } /// <summary> /// Exception arising in <see cref="Result"/> when do not specify exception explicitly: <see cref="Result.Fail(string)"/> /// </summary> public class ResultException : Exception { public ResultException(string message) : base(message) { } } }