Skip to content

Instantly share code, notes, and snippets.

@mattiasnordqvist
Last active November 27, 2021 13:03
Show Gist options
  • Save mattiasnordqvist/47fcdb445f4468ecc8cf4768d0e5bf73 to your computer and use it in GitHub Desktop.
Save mattiasnordqvist/47fcdb445f4468ecc8cf4768d0e5bf73 to your computer and use it in GitHub Desktop.
The Missing Return Type
public class User
{
public string ConcurrencyToken { get; internal set; }
public Email? Email { get; internal set; }
public Name Name { get; internal set; }
}
public record Email
{
private Email(string value) { Value = value; }
public string Value { get; private set; }
public static Result<Email> Create(string emailCandidate)
{
ArgumentNullException.ThrowIfNull(emailCandidate, nameof(emailCandidate));
var emailResult = Result.Ok;
if (!emailCandidate.Contains("@"))
{
emailResult = Result.Error(new InvalidEmailFormatError());
}
if (!emailCandidate.EndsWith("@snusmus.se"))
{
emailResult += Result.Error(new InvalidEmailDomainError());
}
return emailResult.Success
? Result<Email>.Ok(new Email(emailCandidate) { })
: Result<Email>.Error(emailResult.Errors);
}
}
public record Name
{
private Name(string value) { Value = value; }
public string Value { get; private set; }
public static Result<Name> Create(string nameCandidate)
{
ArgumentNullException.ThrowIfNull(nameCandidate, nameof(nameCandidate));
return string.IsNullOrWhiteSpace(nameCandidate)
? Result<Name>.Error(new NameCannotBeEmptyError())
: Result<Name>.Ok(new Name(nameCandidate));
}
}
public class Service
{
public async Task<Result<Unit>> PutUser(int id, string name, string email, string concurrencyToken)
{
if (id < 0) throw new ArgumentException("Invalid id");
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(concurrencyToken);
return await GetUser(id)
.Map(user => user.ConcurrencyToken != concurrencyToken
? Result<User>.Ok(user)
: Result<User>.Error(new ConcurrencyError()))
.Map(user => UpdateUser(user, name, email))
.Map(async user => await SaveUser(user));
}
private static Result<User> UpdateUser(User user, string name, string? email)
{
var updateResult = email == null
? Result.Ok
: Email.Create(email)
.Map(x => { user.Email = x; return Unit.Instance; } )
+ Name.Create(name)
.Map(x => { user.Name = x; return Unit.Instance; });
return !updateResult.Success
? Result<User>.Error(updateResult.Errors)
: Result<User>.Ok(user);
}
private Task<Result<User>> GetUser(int id)
{
throw new NotImplementedException();
}
private Task<Result<Unit>> SaveUser(User user)
{
throw new NotImplementedException();
}
}
internal class ConcurrencyError : IError
{
}
internal class InvalidEmailFormatError : IError
{
}
internal class InvalidEmailDomainError : IError
{
}
internal class InvalidConcurrencyTokenError : IError
{
}
internal class NameCannotBeEmptyError : IError
{
}
namespace TheMissingReturnType;
public interface IError { }
namespace TheMissingReturnType;
public static class Result
{
public static Result<Unit> Ok => Result<Unit>.Ok(Unit.Instance);
public static Result<Unit> Error(params IError[] errors) => Result<Unit>.Error(errors);
public static Result<Unit> Error(IEnumerable<IError> errors) => Result<Unit>.Error(errors);
public static Result<Unit> Error(IError error, params IError[] errors) => Result<Unit>.Error(errors.Concat(new IError[] { error }));
public static Result<Unit> Error(IError error, IEnumerable<IError> errors) => Result<Unit>.Error(errors.Concat(new IError[] { error }));
}
public record Result<T>
{
public static Result<T> Ok(T t) => new Result<T>(t);
public static Result<T> Error(params IError[] errors) => new Result<T>(errors);
public static Result<T> Error(IError error, params IError[] errors) => new Result<T>(errors.Concat(new IError[] { error }));
public static Result<T> Error(IEnumerable<IError> errors) => new Result<T>(errors.ToArray());
private Result(T value)
{
_value = value;
Errors = Array.Empty<IError>();
}
private Result(params IError[] errors)
{
if (!errors.Any()) throw new InvalidOperationException("Can't create an error result without any errors!");
Errors = errors.ToList();
}
private Result(IEnumerable<IError> errors)
{
if (!errors.Any()) throw new InvalidOperationException("Can't create an error result without any errors!");
Errors = errors.ToList();
}
public T Value => _value ?? throw new InvalidOperationException("Can't get a value from an unsuccesful result.");
private T? _value;
public bool Success => _value != null;
public IEnumerable<IError> Errors { get; }
public static Result<T> operator +(Result<T> a, Result<Unit> b)
{
return !a.Success || !b.Success
? Result<T>.Error(a.Errors.Concat(b.Errors))
: Result<T>.Ok(a.Value);
}
}
namespace TheMissingReturnType;
public static class ResultExtensions
{
// Result<T> -> Result<U>
public static Result<U> Map<T, U>(this Result<T> source, Func<T, Result<U>> next) =>
source.Success
? next(source.Value)
: Result<U>.Error(source.Errors);
// Result<T> -> Task<Result<U>>
public static Task<Result<U>> Map<T, U>(this Result<T> source, Func<T, Task<Result<U>>> next) =>
source.Success
? next(source.Value)
: Task.FromResult(Result<U>.Error(source.Errors));
// Result<T> -> U
public static Result<U> Map<T, U>(this Result<T> source, Func<T, U> next) =>
source.Success
? Result<U>.Ok(next(source.Value))
: Result<U>.Error(source.Errors);
// Result<T> -> Task<U>
public static async Task<Result<U>> Map<T, U>(this Result<T> source, Func<T, Task<U>> next) =>
source.Success
? Result<U>.Ok(await next(source.Value))
: await Task.FromResult(Result<U>.Error(source.Errors));
// Result<T> -> Result<Unit>
public static Result<Unit> Map<T>(this Result<T> source, Func<T, Result<Unit>> next) =>
source.Success
? next(source.Value)
: Result<Unit>.Error(source.Errors);
// Result<T> -> Task<Result<Unit>>
public static async Task<Result<Unit>> Map<T>(this Result<T> source, Func<T, Task<Result<Unit>>> next) =>
source.Success
? await next(source.Value)
: Result<Unit>.Error(source.Errors);
// Task<Result<T>> -> Result<U>
public static async Task<Result<U>> Map<T, U>(this Task<Result<T>> source, Func<T, Result<U>> next) =>
(await source).Success
? next((await source).Value)
: Result<U>.Error((await source).Errors);
// Task<Result<T>> -> Task<Result<U>>
public static async Task<Result<U>> Map<T, U>(this Task<Result<T>> source, Func<T, Task<Result<U>>> next) =>
(await source).Success
? await next((await source).Value)
: Result<U>.Error((await source).Errors);
// Task<Result<T>> -> U
public static async Task<Result<U>> Map<T, U>(this Task<Result<T>> source, Func<T, U> next) =>
(await source).Success
? Result<U>.Ok(next((await source).Value))
: Result<U>.Error((await source).Errors);
// Task<Result<T>> -> Task<U>
public static async Task<Result<U>> Map<T, U>(this Task<Result<T>> source, Func<T, Task<U>> next) =>
(await source).Success
? Result<U>.Ok(await next((await source).Value))
: await Task.FromResult(Result<U>.Error((await source).Errors));
// Task<Result<T>> -> Result<Unit>
public static async Task<Result<Unit>> Map<T>(this Task<Result<T>> source, Func<T, Result<Unit>> next) =>
(await source).Success
? next((await source).Value)
: Result<Unit>.Error((await source).Errors);
// Task<Result<T>> -> Task<Result<Unit>>
public static async Task<Result<Unit>> Map<T>(this Task<Result<T>> source, Func<T, Task<Result<Unit>>> next) =>
(await source).Success
? await next((await source).Value)
: Result<Unit>.Error((await source).Errors);
///
/// Same as above with unit results as source
///
// Result<Unit> -> Result<U>
public static Result<U> Map<U>(this Result<Unit> source, Func<Result<U>> next) =>
source.Success
? next()
: Result<U>.Error(source.Errors);
// Result<Unit> -> Task<Result<U>>
public static Task<Result<U>> Map<U>(this Result<Unit> source, Func<Task<Result<U>>> next) =>
source.Success
? next()
: Task.FromResult(Result<U>.Error(source.Errors));
// Result<Unit> -> U
public static Result<U> Map<U>(this Result<Unit> source, Func<U> next) =>
source.Success
? Result<U>.Ok(next())
: Result<U>.Error(source.Errors);
// Result<Unit> -> Task<U>
public static async Task<Result<U>> Map<U>(this Result<Unit> source, Func<Task<U>> next) =>
source.Success
? Result<U>.Ok(await next())
: await Task.FromResult(Result<U>.Error(source.Errors));
// Result<Unit> -> Result<Unit>
public static Result<Unit> Map(this Result<Unit> source, Func<Result<Unit>> next) =>
source.Success
? next()
: Result<Unit>.Error(source.Errors);
// Result<Unit> -> Task<Result<Unit>>
public static async Task<Result<Unit>> Map(this Result<Unit> source, Func<Task<Result<Unit>>> next) =>
source.Success
? await next()
: Result<Unit>.Error(source.Errors);
// Task<Result<Unit>> -> Result<U>
public static async Task<Result<U>> Map<U>(this Task<Result<Unit>> source, Func<Result<U>> next) =>
(await source).Success
? next()
: Result<U>.Error((await source).Errors);
// Task<Result<Unit>> -> Task<Result<U>>
public static async Task<Result<U>> Map<U>(this Task<Result<Unit>> source, Func<Task<Result<U>>> next) =>
(await source).Success
? await next()
: Result<U>.Error((await source).Errors);
// Task<Result<Unit>> -> U
public static async Task<Result<U>> Map<U>(this Task<Result<Unit>> source, Func<U> next) =>
(await source).Success
? Result<U>.Ok(next())
: Result<U>.Error((await source).Errors);
// Task<Result<Unit>> -> Task<U>
public static async Task<Result<U>> Map<U>(this Task<Result<Unit>> source, Func<Task<U>> next) =>
(await source).Success
? Result<U>.Ok(await next())
: await Task.FromResult(Result<U>.Error((await source).Errors));
// Task<Result<Unit>> -> Result<Unit>
public static async Task<Result<Unit>> Map(this Task<Result<Unit>> source, Func<Result<Unit>> next) =>
(await source).Success
? next()
: Result<Unit>.Error((await source).Errors);
// Task<Result<Unit>> -> Task<Result<Unit>>
public static async Task<Result<Unit>> Map(this Task<Result<Unit>> source, Func<Task<Result<Unit>>> next) =>
(await source).Success
? await next()
: Result<Unit>.Error((await source).Errors);
}
namespace TheMissingReturnType;
public class Unit
{
public static Unit Instance = new Unit();
private Unit() { }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment