If we have more complex validation logic, we could introduce a factory method and a Result
object to handle the validations better:
public record Result<T> { public bool IsSuccess { get; private init; } public T? Value { get; private init; } public string? ErrorMessage { get; private init; } private Result(){} public static Result<T> Success(T value) => new() { IsSuccess = true, Value = value }; public static Result<T> Failure(string errorMessage) => new() { IsSuccess = false, ErrorMessage = errorMessage }; }
Here we declare the generic Result
record, so now let’s see how to create the factory method for the Money
value object:
public record Money { private static readonly IReadOnlyCollection<string> SupportedCurrencies = new[]{"USD", "EUR"}; public decimal Amount { get; } public string Currency { get; } private Money(decimal amount, string currency) { Amount = amount; Currency = currency; } public static Result<Money> Create(decimal amount, string currency) { if(string.IsNullOrWhiteSpace(currency)) return Result<Money>.Failure($"{nameof(currency)} cannot be null or whitespace."); if(!SupportedCurrencies.Contains(currency.ToUpperInvariant())) return Result<Money>.Failure($"'{currency}' is not supported."); return Result<Money>.Success(new(amount, currency)); } }
Instead of throwing exceptions (or simply returning False), we return a Failure
result (with a specific message), allowing the caller to handle this in a cleaner fashion.