Last active
September 28, 2024 08:39
-
-
Save fakhrulhilal/ce1639fa967da591c7e3a2b13a7850e2 to your computer and use it in GitHub Desktop.
C# functional style for wrapping result without using actual exception
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
internal readonly struct Result<T>(bool successful, T result, Exception exception) | |
{ | |
private bool Successful { get; } = successful; | |
public static implicit operator T(Result<T> wrapped) => wrapped.UnwrapOrThrow(); | |
public static implicit operator Result<T>(T result) => new(true, result, default!); | |
public static implicit operator Result<T>(Exception error) => new(false, default!, error); | |
/// <summary> | |
/// Get backing value and convert it to another type | |
/// </summary> | |
/// <param name="success">A factory when successful</param> | |
/// <param name="error">A factory when not successful</param> | |
/// <typeparam name="TOutput">Output type</typeparam> | |
public TOutput Match<TOutput>(Func<T, TOutput> success, Func<Exception, TOutput> error) => | |
Successful ? success(result) : error(exception); | |
/// <summary> | |
/// Convert backing value and wrap again for successful result | |
/// </summary> | |
/// <param name="success"></param> | |
/// <typeparam name="TOutput"></typeparam> | |
/// <returns>Wrapped result with different value</returns> | |
public Result<TOutput> Map<TOutput>(Func<T, TOutput> success) => Successful ? success(result) : exception; | |
/// <summary> | |
/// Get the actual value or throw when not successful | |
/// </summary> | |
/// <returns>The backing value</returns> | |
/// <exception cref="Exception">Exception thrown when not successful</exception> | |
public T UnwrapOrThrow() => Successful ? result : throw exception; | |
/// <summary> | |
/// Invoke an action when successful | |
/// </summary> | |
public async Task InvokeAsync(Func<T, Task> action) { | |
if (Successful) { | |
await action(result); | |
} | |
} | |
} | |
internal static class Functional | |
{ | |
internal readonly struct Ctx<T1, T2>(T1 a, T2 b) | |
{ | |
public T1 A { get; } = a; | |
public T2 B { get; } = b; | |
} | |
internal readonly struct Ctx<T1, T2, T3>(T1 a, T2 b, T3 c) | |
{ | |
public T1 A { get; } = a; | |
public T2 B { get; } = b; | |
public T3 C { get; } = c; | |
} | |
/// <summary> | |
/// Expression wrapper. Used when expression is needed, but need some statements before returning response. | |
/// Useful when language becomes limitation when doing something, f.e. calculating result inside switch expression. | |
/// </summary> | |
/// <param name="factory"></param> | |
/// <typeparam name="T"></typeparam> | |
/// <returns></returns> | |
public static T Build<T>(Func<T> factory) => factory(); | |
public static async Task<Result<T>> ThrowWhenNull<T>(this Task<T?> fetch, string message) where T : class { | |
var result = await fetch; | |
return result is null ? new NullReferenceException(message) : result; | |
} | |
/// <summary> | |
/// Convert from input to an output when not <c>null</c> | |
/// </summary> | |
/// <param name="fetch">A factory to get input to be converted</param> | |
/// <param name="next"> | |
/// Map previous <typeparamref name="T"/> to <typeparamref name="TOutput"/> | |
/// when <typeparamref name="T"/> is not <c>null</c> | |
/// </param> | |
/// <param name="notFoundMessage">Exception message when <c>null</c></param> | |
/// <returns>Wrapped result which can be an instance or exception when <c>null</c></returns> | |
public static async Task<Result<Ctx<T, TOutput>>> Then<T, TOutput>(this Task<T?> fetch, | |
Func<T, TOutput> next, string notFoundMessage) where T : class { | |
var result = await fetch; | |
return result is null ? new NullReferenceException(notFoundMessage) : new Ctx<T, TOutput>(result, next(result)); | |
} | |
public static async Task<Result<Ctx<TInput, TOutput>>> Then<TInput, TOutput>( | |
this Task<Result<TInput>> fetch, | |
Func<TInput, TOutput> next) { | |
var outcome = await fetch; | |
return outcome.Map(input => new Ctx<TInput, TOutput>(input, next(input))); | |
} | |
/// <summary> | |
/// Convert from input to an output when not <c>null</c> and previous outcome is successful | |
/// </summary> | |
/// <param name="fetch">A factory to get previous outcome to be converted</param> | |
/// <param name="whenNotNull">A factory conversion when <typeparamref name="T2"/> is not null</param> | |
/// <param name="whenNull">A factory conversion when <typeparamref name="T2"/> is null</param> | |
/// <typeparam name="T1">Additional context for conversion</typeparam> | |
/// <typeparam name="T2">Input</typeparam> | |
/// <typeparam name="TOutput">Conversion output</typeparam> | |
/// <returns>Wrapped result which can be an instance or exception when <c>null</c></returns> | |
public static async Task<Result<Ctx<T1, T2?, TOutput>>> Then<T1, T2, TOutput>( | |
this Task<Result<Ctx<T1, T2?>>> fetch, | |
Func<T2, TOutput> whenNotNull, Func<TOutput> whenNull) where T2 : class { | |
var outcome = await fetch; | |
return outcome.Map(ctx => | |
new Ctx<T1, T2?, TOutput>(ctx.A, ctx.B, ctx.B is null ? whenNull() : whenNotNull(ctx.B))); | |
} | |
/// <summary> | |
/// Convert from input to an output when not <c>null</c> and previous outcome is successful | |
/// </summary> | |
/// <param name="fetch">A factory to get previous outcome to be converted</param> | |
/// <param name="whenNotNull">A factory conversion when <typeparamref name="T2"/> is not null</param> | |
/// <param name="whenNull">A factory conversion when <typeparamref name="T2"/> is null</param> | |
/// <typeparam name="T1">Additional context for conversion</typeparam> | |
/// <typeparam name="T2">Input</typeparam> | |
/// <typeparam name="TOutput">Conversion output</typeparam> | |
/// <returns>Wrapped result which can be an instance or exception when <c>null</c></returns> | |
public static async Task<Result<Ctx<T1, T2?, TOutput>>> Then<T1, T2, TOutput>( | |
this Task<Result<Ctx<T1, T2?>>> fetch, | |
Func<T1, T2, TOutput> whenNotNull, Func<T1, TOutput> whenNull) where T2 : class { | |
var outcome = await fetch; | |
return outcome.Map(ctx => | |
new Ctx<T1, T2?, TOutput>(ctx.A, ctx.B, | |
ctx.B is null ? whenNull(ctx.A) : whenNotNull(ctx.A, ctx.B))); | |
} | |
/// <summary> | |
/// Invoke an action when <typeparamref name="T3"/> is successful | |
/// </summary> | |
/// <param name="fetch">Previous outcome</param> | |
/// <param name="action">An asynchronous action to be invoked when successful</param> | |
/// <typeparam name="T1"></typeparam> | |
/// <typeparam name="T2"></typeparam> | |
/// <typeparam name="T3"></typeparam> | |
public static async Task<Result<Ctx<T1, T2?, T3>>> Then<T1, T2, T3>( | |
this Task<Result<Ctx<T1, T2?, T3>>> fetch, | |
Func<T1, T2, Task> action) { | |
var outcome = await fetch; | |
await outcome.InvokeAsync(async ctx => | |
{ | |
if (ctx.B is not null) { | |
await action(ctx.A, ctx.B); | |
} | |
}); | |
return outcome; | |
} | |
/// <summary> | |
/// Unwrap the last result from outcome or throw exception when not successful | |
/// </summary> | |
/// <param name="outcome">Wrapped outcome</param> | |
/// <typeparam name="T1"></typeparam> | |
/// <typeparam name="T2"></typeparam> | |
/// <typeparam name="T3"></typeparam> | |
/// <returns><typeparamref name="T3"/> or throwing exception</returns> | |
/// <exception cref="Exception">An exception captured during previous execution</exception> | |
public static async Task<T3> UnwrapLastResult<T1, T2, T3>(this Task<Result<Ctx<T1, T2, T3>>> outcome) { | |
var result = await outcome; | |
return result.Match(ctx => ctx.C, exception => throw exception); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment