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.


