Result Pattern Implementation
Overview
The Result pattern provides explicit error handling without exceptions:
- No exceptions for business errors - Exceptions for truly exceptional cases only
- Explicit success/failure - Compiler forces handling of both cases
- Composable errors - Chain operations, fail fast
- Self-documenting - Method signatures show possible outcomes
Quick Reference
| Type | Purpose | Usage |
|---|---|---|
Result | Operation without return value | Update, Delete operations |
Result<T> | Operation with return value | Create, Get operations |
Error | Error information | Code + Description |
Implementation Structure
/Domain/Abstractions/
├── Result.cs # Result and Result<T>
├── Error.cs # Error record
└── ValidationResult.cs # Multiple errors support
Template: Core Result Types
// src/{name}.domain/Abstractions/Error.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Represents an error with a code and description
/// </summary>
public record Error(string Code, string Description)
{
/// <summary>
/// Represents no error (success state)
/// </summary>
public static readonly Error None = new(string.Empty, string.Empty);
/// <summary>
/// Represents a null value error
/// </summary>
public static readonly Error NullValue = new(
"Error.NullValue",
"A null value was provided");
/// <summary>
/// Creates an error from an exception
/// </summary>
public static Error FromException(Exception exception) => new(
"Error.Exception",
exception.Message);
/// <summary>
/// Implicit conversion to string (returns Code)
/// </summary>
public static implicit operator string(Error error) => error.Code;
public override string ToString() => Code;
}
// src/{name}.domain/Abstractions/Result.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Represents the outcome of an operation that doesn't return a value
/// </summary>
public class Result
{
protected Result(bool isSuccess, Error error)
{
if (isSuccess && error != Error.None)
{
throw new InvalidOperationException(
"Cannot create successful result with an error");
}
if (!isSuccess && error == Error.None)
{
throw new InvalidOperationException(
"Cannot create failed result without an error");
}
IsSuccess = isSuccess;
Error = error;
}
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
// ═══════════════════════════════════════════════════════════════
// FACTORY METHODS
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Creates a successful result
/// </summary>
public static Result Success() => new(true, Error.None);
/// <summary>
/// Creates a failed result with the specified error
/// </summary>
public static Result Failure(Error error) => new(false, error);
/// <summary>
/// Creates a successful result with a value
/// </summary>
public static Result<TValue> Success<TValue>(TValue value) =>
new(value, true, Error.None);
/// <summary>
/// Creates a failed result with the specified error
/// </summary>
public static Result<TValue> Failure<TValue>(Error error) =>
new(default, false, error);
/// <summary>
/// Creates a result based on a condition
/// </summary>
public static Result Create(bool condition, Error error) =>
condition ? Success() : Failure(error);
/// <summary>
/// Creates a result based on a condition with a value
/// </summary>
public static Result<TValue> Create<TValue>(TValue? value, Error error) =>
value is not null ? Success(value) : Failure<TValue>(error);
}
/// <summary>
/// Represents the outcome of an operation that returns a value
/// </summary>
public class Result<TValue> : Result
{
private readonly TValue? _value;
protected internal Result(TValue? value, bool isSuccess, Error error)
: base(isSuccess, error)
{
_value = value;
}
/// <summary>
/// Gets the value if successful, throws if failed
/// </summary>
public TValue Value => IsSuccess
? _value!
: throw new InvalidOperationException(
$"Cannot access value of a failed result. Error: {Error.Code}");
/// <summary>
/// Implicit conversion from value to successful Result
/// </summary>
public static implicit operator Result<TValue>(TValue? value) =>
value is not null ? Success(value) : Failure<TValue>(Error.NullValue);
/// <summary>
/// Implicit conversion from Error to failed Result
/// </summary>
public static implicit operator Result<TValue>(Error error) =>
Failure<TValue>(error);
}
Template: Result Extensions (Functional Operations)
// src/{name}.domain/Abstractions/ResultExtensions.cs
namespace {name}.domain.abstractions;
public static class ResultExtensions
{
// ═══════════════════════════════════════════════════════════════
// MAP: Transform success value
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Transforms the value if successful, preserves error if failed
/// </summary>
public static Result<TOut> Map<TIn, TOut>(
this Result<TIn> result,
Func<TIn, TOut> mapper)
{
return result.IsSuccess
? Result.Success(mapper(result.Value))
: Result.Failure<TOut>(result.Error);
}
/// <summary>
/// Async version of Map
/// </summary>
public static async Task<Result<TOut>> Map<TIn, TOut>(
this Task<Result<TIn>> resultTask,
Func<TIn, TOut> mapper)
{
var result = await resultTask;
return result.Map(mapper);
}
// ═══════════════════════════════════════════════════════════════
// BIND: Chain operations that return Result
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Chains another Result-returning operation if successful
/// </summary>
public static Result<TOut> Bind<TIn, TOut>(
this Result<TIn> result,
Func<TIn, Result<TOut>> binder)
{
return result.IsSuccess
? binder(result.Value)
: Result.Failure<TOut>(result.Error);
}
/// <summary>
/// Async version of Bind
/// </summary>
public static async Task<Result<TOut>> Bind<TIn, TOut>(
this Result<TIn> result,
Func<TIn, Task<Result<TOut>>> binder)
{
return result.IsSuccess
? await binder(result.Value)
: Result.Failure<TOut>(result.Error);
}
/// <summary>
/// Async version of Bind for Task results
/// </summary>
public static async Task<Result<TOut>> Bind<TIn, TOut>(
this Task<Result<TIn>> resultTask,
Func<TIn, Result<TOut>> binder)
{
var result = await resultTask;
return result.Bind(binder);
}
/// <summary>
/// Fully async Bind
/// </summary>
public static async Task<Result<TOut>> Bind<TIn, TOut>(
this Task<Result<TIn>> resultTask,
Func<TIn, Task<Result<TOut>>> binder)
{
var result = await resultTask;
return await result.Bind(binder);
}
// ═══════════════════════════════════════════════════════════════
// TAP: Execute side effect without changing result
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Executes an action if successful, returns original result
/// </summary>
public static Result<T> Tap<T>(
this Result<T> result,
Action<T> action)
{
if (result.IsSucc