Money Bags
A money bag is a collection that holds one MonetaryValue per currency. It is the type to reach for whenever an amount can be spread across currencies: a multi-currency account balance, a shopping cart that accepts several currencies, or a report total. This guide covers the four bag types, how values combine, and the operations they support.
The four types
| Type | Mutable | Ordered by currency code |
|---|---|---|
| MoneyBag | Yes | No |
| SortedMoneyBag | Yes | Yes |
| ImmutableMoneyBag | No | No |
| ImmutableSortedMoneyBag | No | Yes |
All four implement IReadOnlyMoneyBag, the mutable ones implement IMoneyBag, and the immutable ones implement IImmutableMoneyBag, whose mutating methods return a new bag. The unsorted bags are hash-based and slightly faster; the sorted bags enumerate in a stable order, which matters for display and for deterministic output. IsSorted reports which kind a bag is when only the interface is known.
Creating Bags
Bags support collection expressions, collection initializers, constructors and factory methods:
MoneyBag bag = [new(100m, "USD"), new(50m, "EUR")];
var bag2 = new MoneyBag { new(100m, "USD"), new(50m, "EUR") };
var bag3 = new MoneyBag(values);
ImmutableSortedMoneyBag snapshot = [new(1m, "CAD"), new(2m, "USD")];
var snapshot2 = ImmutableMoneyBag.CreateRange(values);
IReadOnlyMoneyBag readOnly = [new(1m, "CAD")]; // creates a MoneyBag
Every bag is bound to a CurrencyRegistry, available through Registry, which defaults to Default. Adding a value whose currency is not in the registry throws ArgumentException. Pass a registry to the constructor or factory to restrict a bag to a specific set of currencies. See Currencies and Registries.
The MoneyCollectionExtensions methods such as ToImmutableMoneyBag convert between bag types and from any sequence of values.
Adding and Subtracting
Bags combine values by currency. Adding a value in a currency the bag already holds adds to the existing amount, and subtracting works the same way. A currency stays in the bag when its amount reaches zero until it is explicitly removed or trimmed:
var bag = new MoneyBag();
bag.Add(new MonetaryValue(100m, "USD"));
bag.Add(25m, "USD"); // USD 125
bag.Subtract(125m, "USD"); // USD 0, still present
bag.AddRange(otherBag);
bag.SubtractRange(refunds);
bag.TrimZeroAmounts(); // removes USD 0
Default values are ignored when added or subtracted, which lets accumulators start from Default without special cases.
SetValue and SetAmount replace a currency's amount instead of combining with it. Remove and RemoveAll drop currencies entirely.
Immutable bags expose the same operations returning new instances:
var balance = ImmutableMoneyBag.Create(new MonetaryValue(100m, "USD"));
var updated = balance.Add(50m, "EUR").Subtract(10m, "USD");
Reading Values
The indexer returns the value for a currency, or the default value when the bag does not contain it, so lookups never throw for a missing currency:
var usd = bag["USD"]; // USD 125
var jpy = bag["JPY"]; // default value, IsDefault is true
if (bag.TryGetValue("EUR", out var eur)) { }
if (bag.TryGetAmount("EUR", out decimal amount)) { }
bag.Count; // number of currencies
bag.Currencies; // the currencies present
bag.ContainsCurrency("USD");
bag.Contains(new MonetaryValue(125m, "USD"));
Bags enumerate as MonetaryValue sequences, so LINQ works directly. Because equality across currencies is safe and comparison is not, group or filter by currency before ordering by amount.
Transforming Values
TransformAmounts and TransformValues apply a function to every value in the bag. The overloads that take a function returning a nullable amount remove the currency when the function returns null:
bag.TransformAmounts(amount => amount * 1.13m); // apply tax to everything
bag.TransformValues(value => value.Amount > 0 ? value.Amount : null); // drop non-positive values
Round and RoundToCash round every value according to its own currency's policy, which is the usual last step after applying rates or percentages. See Rounding and Allocation.
Formatting
Bags format as a comma-separated list of their values using the same format strings as MonetaryValue, with an optional ! prefix that omits zero amounts. See Formatting.
Next Steps
Continue with these related articles:
- Monetary Values - The values a bag contains.
- Currencies and Registries - Restricting a bag to a registry.
- Formatting - Bag format strings.