![]()
Last year, I read this blog, and then I implemented a simple one.
last year version
using System.Diagnostics.CodeAnalysis;
namespace OALY2000.Results;
public class Result<T>
{
public T? Value { get; private init; }
public Exception? Error { get; private init; }
private Result(T value)
{
IsSuccess = true;
Value = value;
Error = null;
}
private Result(Exception error)
{
IsSuccess = false;
Value = default;
Error = error;
}
[MemberNotNullWhen(true, nameof(Value))]
[MemberNotNullWhen(false, nameof(Error))]
public bool IsSuccess { get; private init; }
public static Result<T> Success(T value) => new(value);
public static Result<T> Fail(Exception error) => new(error);
public static implicit operator Result<T>(T value) => Success(value);
public static implicit operator Result<T>(Exception error) => Fail(error);
}
public static class ResultExtensions
{
public static Result<TReturn> Switch<T, TReturn>(this Result<T> source, Func<T, Result<TReturn>> onSuccess, Func<Exception, Result<TReturn>> onFailure)
{
if (source.IsSuccess) return onSuccess(source.Value);
else return onFailure(source.Error);
}
public static Result<TResult> Select<TFrom, TResult>(this Result<TFrom> source, Func<TFrom, TResult> selector)
{
return source.Switch(r => selector(r), Result<TResult>.Fail);
}
public static Result<TResult> SelectMany<TSource, TMiddle, TResult>(
this Result<TSource> source,
Func<TSource, Result<TMiddle>> collectionSelector,
Func<TSource, TMiddle, TResult> resultSelector)
{
if (source.IsSuccess) return collectionSelector(source.Value).Select(v => resultSelector(source.Value!, v));
else return source.Error;
}
public static async Task<Result<TResult>> Select<TFrom, TResult>(this Task<Result<TFrom>> source, Func<TFrom, TResult> selector)
=> (await source).Select(selector);
public static async Task<Result<TResult>> SelectMany<TSource, TMiddle, TResult>(
this Task<Result<TSource>> source,
Func<TSource, Task<Result<TMiddle>>> collectionSelector,
Func<TSource, TMiddle, TResult> resultSelector)
{
var result = await source;
if (result.IsSuccess) return (await collectionSelector(result.Value)).Select(v => resultSelector(result.Value!, v));
else return result.Error;
}
}integrate with ASP.NET Core
In practice, I found that it makes some boilerplate code:
app.MapGet("/", () =>
{
Result<string> result = GetSomething();
if (result.IsSuccess)
{
return result.Value;
}
else
{
throw result.Error;
}
});
app.MapGet("/alternative", IResult () =>
{
Result<string> result = GetSomething();
if (result.IsSuccess)
{
return TypedResults.Ok(result.Value)
}
else
{
return TypedResults.Problem(result.Error.Message)
}
});The first one need a IExceptionHandler or middleware to handle the exception. The second one break openapi document. And both of them are not so elegant.
After Reading source code of TypedResults.Ok, I found the way to remove boilerplate code: IResult to define serialization and IEndpointMetadataProvider to define openapi metadata.
public class Result<T> : IResult, IEndpointMetadataProvider
{
// ...
public Task ExecuteAsync(HttpContext httpContext)
{
if (IsSuccess)
{
return TypedResults.Ok(Value).ExecuteAsync(httpContext);
}
if (httpContext.RequestServices.GetService<IFailedResultHandler>() is { } handler)
{
return handler.ExecuteAsync(httpContext, Error);
}
return TypedResults.Problem(title: Error.Message).ExecuteAsync(httpContext);
}
static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
ArgumentNullException.ThrowIfNull(method);
ArgumentNullException.ThrowIfNull(builder);
builder.Metadata.Add(new ProducesResponseTypeMetadata(200, typeof(T), ["application/json"]));
}
}
public interface IFailedResultHandler
{
Task ExecuteAsync(HttpContext context, Exception error);
}Now, we can write api like this:
app.MapGet("/", () => GetSomething());