Last active
November 27, 2021 13:03
-
-
Save mattiasnordqvist/47fcdb445f4468ecc8cf4768d0e5bf73 to your computer and use it in GitHub Desktop.
The Missing Return Type
This file contains 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
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 | |
{ | |
} |
This file contains 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
namespace TheMissingReturnType; | |
public interface IError { } |
This file contains 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
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); | |
} | |
} |
This file contains 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
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); | |
} |
This file contains 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
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