Class Money
- Namespace
- Trellis.Primitives
- Assembly
- Trellis.Primitives.dll
Represents a structured monetary value object composed of an amount and currency code. Provides type-safe arithmetic operations that prevent mixing different currencies.
[JsonConverter(typeof(CompositeValueObjectJsonConverter<Money>))]
public class Money : ValueObject, IComparable<ValueObject>, IComparable, IEquatable<ValueObject>
- Inheritance
-
Money
- Implements
- Inherited Members
- Extension Methods
Remarks
Money is intentionally not a scalar value object because its semantic identity spans both Amount and Currency. Money uses decimal for precision and stores amounts in the currency's base unit. All arithmetic operations enforce currency matching and return Result for error handling. Amounts are automatically rounded to the appropriate number of decimal places for the currency.
Properties
Amount
Gets the monetary amount in the currency's base unit.
public decimal Amount { get; }
Property Value
Currency
Gets the ISO 4217 currency code.
public CurrencyCode Currency { get; }
Property Value
Methods
Add(Money)
Adds two Money amounts if they have the same currency.
public Result<Money> Add(Money other)
Parameters
otherMoney
Returns
Exceptions
- ArgumentNullException
Thrown when
otheris null.
Allocate(params int[])
Allocates money across multiple parts using ratios. Ensures no money is lost to rounding by allocating remainder to first portions.
public Result<Money[]> Allocate(params int[] ratios)
Parameters
ratiosint[]The ratios to split by (e.g., [1, 2, 1] for 25%, 50%, 25%).
Returns
Exceptions
- ArgumentNullException
Thrown when
ratiosis null.
Create(decimal, string)
Creates a Money instance with the specified amount and currency code. Throws an exception if the amount or currency is invalid.
public static Money Create(decimal amount, string currencyCode)
Parameters
amountdecimalThe monetary amount.
currencyCodestringISO 4217 currency code (e.g., "USD", "EUR").
Returns
- Money
The Money instance.
Remarks
Use this method when you know the values are valid (e.g., in tests or with constants). For user input, use TryCreate(decimal, string, string?) instead.
Exceptions
- InvalidOperationException
Thrown when amount is negative or currency is invalid.
Divide(decimal)
Divides the money amount by a scalar value.
public Result<Money> Divide(decimal divisor)
Parameters
divisordecimal
Returns
Divide(int)
Divides the money amount by an integer to split evenly.
public Result<Money> Divide(int divisor)
Parameters
divisorint
Returns
GetEqualityComponents(ref EqualityComponents)
Gets the equality components for value comparison.
protected override void GetEqualityComponents(ref EqualityComponents components)
Parameters
componentsEqualityComponents
IsGreaterThan(Money)
Checks if this money is greater than another money amount (same currency required).
public bool IsGreaterThan(Money other)
Parameters
otherMoney
Returns
Remarks
Returns false for cross-currency comparisons; mismatched currencies are
neither greater than nor less than each other under this contract. Callers that need
to distinguish "smaller" from "different currency" must compare Currency
explicitly.
Exceptions
- ArgumentNullException
Thrown when
otheris null.
IsGreaterThanOrEqual(Money)
Checks if this money is greater than or equal to another amount (same currency required).
public bool IsGreaterThanOrEqual(Money other)
Parameters
otherMoney
Returns
Remarks
Returns false for cross-currency comparisons. See IsGreaterThan(Money).
Exceptions
- ArgumentNullException
Thrown when
otheris null.
IsLessThan(Money)
Checks if this money is less than another money amount (same currency required).
public bool IsLessThan(Money other)
Parameters
otherMoney
Returns
Remarks
Returns false for cross-currency comparisons. See IsGreaterThan(Money).
Exceptions
- ArgumentNullException
Thrown when
otheris null.
IsLessThanOrEqual(Money)
Checks if this money is less than or equal to another amount (same currency required).
public bool IsLessThanOrEqual(Money other)
Parameters
otherMoney
Returns
Remarks
Returns false for cross-currency comparisons. See IsGreaterThan(Money).
Exceptions
- ArgumentNullException
Thrown when
otheris null.
Multiply(decimal)
Multiplies the money amount by a scalar value.
public Result<Money> Multiply(decimal multiplier)
Parameters
multiplierdecimal
Returns
Multiply(int)
Multiplies the money amount by an integer quantity.
public Result<Money> Multiply(int quantity)
Parameters
quantityint
Returns
Subtract(Money)
Subtracts another Money amount if they have the same currency.
public Result<Money> Subtract(Money other)
Parameters
otherMoney
Returns
Exceptions
- ArgumentNullException
Thrown when
otheris null.
Sum(IEnumerable<Money>)
Sums a collection of Money values. All values must share the same currency.
public static Result<Money> Sum(IEnumerable<Money> values)
Parameters
valuesIEnumerable<Money>The monetary values to sum.
Returns
- Result<Money>
Success with the total, or failure if the collection is empty, contains mixed currencies, or overflows.
Remarks
Empty input fails because Money carries currency and there is no canonical
"zero currency". Callers that have a meaningful currency for the empty case should use
Sum(IEnumerable<Money>, Money) and pass a zero-amount Money
in the desired currency as fallback.
Sum(IEnumerable<Money>, Money)
Sums a collection of Money values, returning fallback
when the collection is empty.
public static Result<Money> Sum(IEnumerable<Money> values, Money fallback)
Parameters
valuesIEnumerable<Money>The monetary values to sum.
fallbackMoneyThe value to return when
valuesis empty.
Returns
- Result<Money>
Success with the total when
valuesis non-empty. Success withfallbackwhenvaluesis empty. Failure if the non-empty values contain mixed currencies or the addition overflows.
Examples
// Empty line items return USD 0 instead of failing.
var total = Money.Sum(order.LineItems.Select(li => li.Subtotal), Money.Zero("USD").Unwrap());
Remarks
fallback is consulted only when values is empty.
When values is non-empty the result currency is inferred from the
first element, exactly as in Sum(IEnumerable<Money>) — the fallback's
currency is irrelevant in that case. This mirrors Sum(IEnumerable<MonetaryAmount>)'s
empty-collection-yields-zero ergonomic for currencies that Money can
represent.
ToString()
Returns a string representation of the money amount with currency.
public override string ToString()
Returns
TryCreate(decimal, string, string?)
Creates a Money instance with the specified amount and currency code.
public static Result<Money> TryCreate(decimal amount, string currencyCode, string? fieldName = null)
Parameters
amountdecimalThe monetary amount.
currencyCodestringISO 4217 currency code (e.g., "USD", "EUR"); case-insensitive.
fieldNamestringOptional name of the money field itself; component errors are reported beneath it.
Returns
Remarks
fieldName names the Money value as a whole, and validation
errors are reported against a nested JSON Pointer under it — one segment per component, matching
the serialized shape { "amount": …, "currency": … }. Passing "price" therefore
yields /price/amount for amount failures and /price/currency for currency failures,
so a client can bind each error to the field that produced it. When
fieldName is omitted the pointers are /amount and /currency.
A fieldName that is already a JSON Pointer (starts with '/') is
composed onto rather than escaped, so "/items/0/price" yields /items/0/price/amount.
Use TryCreate(decimal, string, string?, string?) instead when the amount and currency arrive as sibling fields of a flat payload rather than as a nested object.
Currency validation is delegated to TryCreate(string?, string?) and is
syntactic only (three ASCII letters per ISO 4217 format; input is case-insensitive,
stored uppercase via ToUpperInvariant). The ISO 4217 active-code list is not
enforced — values like XXX, XTS, or ZZZ satisfy the format and
will be accepted. Applications that need to restrict to a specific set of currencies
(e.g., those a payment processor supports, or excluding ISO reserved/test codes)
should layer an allow-list at the application boundary before calling
TryCreate(decimal, string, string?).
TryCreate(decimal, string, string?, string?)
Creates a Money instance whose amount and currency are validated against independently named fields, for payloads that carry the two as siblings rather than as a nested object.
public static Result<Money> TryCreate(decimal amount, string currencyCode, string? amountFieldName, string? currencyFieldName)
Parameters
amountdecimalThe monetary amount.
currencyCodestringISO 4217 currency code (e.g., "USD", "EUR"); case-insensitive.
amountFieldNamestringField carrying the amount; defaults to
amountwhen null. An empty string explicitly targets the document root.currencyFieldNamestringField carrying the currency; defaults to
currencywhen null. An empty string explicitly targets the document root.
Returns
Remarks
Use this overload when the incoming document looks like { "price": 10, "currency": "XX" }.
The nested-pointer composition performed by TryCreate(decimal, string, string?)
would report such a failure at /price/currency — a location that does not exist in the
payload. Each argument accepts a simple property name or a full JSON Pointer.
Zero(string)
Creates a zero-value Money instance for the specified currency.
public static Result<Money> Zero(string currencyCode = "USD")
Parameters
currencyCodestringISO 4217 currency code. Defaults to
"USD".
Returns
- Result<Money>
A Result<TValue> wrapping the constructed Money. Returns the success branch when
currencyCodeis a supported currency, and the failure branch when it is not — the currency code is validated at construction time, so this method is not a plain Money factory. Consumers must unwrap the result (typically with.TryGetValue(...)or.Match(...)) before using the value.