CurrenCSharp is a .NET library for handling monetary values.
It provides:
- a
Moneytype (amount + currency), - a
Wallettype (a collection of monetary values, potentially across multiple currencies), - a
ContextedMoney/ContextedWalletpair that attaches an exchange-rate context for conversion and cross-currency comparison.
The library supports arithmetic, comparison, conversion, and distribution of amounts.
For exchange rates you implement IExchangeRateProvider and plug in your own source.
Note: Not done yet! The library is in early development and not yet published on NuGet. For now, clone the repository and reference the project directly. It should be available for installation via NuGet in the near future.
Install the core package:
dotnet add package CurrenCSharpInstall the optional ISO 4217 package (predefined currencies):
dotnet add package CurrenCSharp.CurrenciesCurrency: an ISO 4217 currency definition (AlphaCode,NumericCode,MinorUnits).Money: one monetary amount in one currency (for exampleEUR 47.11).Wallet: a collection ofMoneyvalues, potentially across multiple currencies.ExchangeRateContext: exchange rates for a base currency at a reference timestamp.ContextedMoney/ContextedWallet: aMoney/Walletbound to anExchangeRateContextvia.In(context), required for conversion or cross-currency comparison.IExchangeRateProvider: asynchronous source forExchangeRateContextvalues (latest or historical).
The examples below mirror the end-to-end walkthrough in
example/CurrenCSharp.Example/Program.cs. They assumeusing CurrenCSharp;andusing CurrenCSharp.Currencies;.
CurrenC.UseDefaultCurrency sets an ambient default currency for the current
async scope. The returned IDisposable restores the previous default on
dispose (scopes must be disposed in LIFO order).
using var defaultCurrencyScope = CurrenC.UseDefaultCurrency(Iso4217.EUR);While the scope is active, Currency.Default and Money.Zero() resolve to EUR.
Money default_zero = Money.Zero(); // EUR 0.00 (uses default currency)
Money usd_zero = Money.Zero(Iso4217.USD); // USD 0.00
Money eur_47_11 = new(47.11m, Iso4217.EUR);
Money usd_47_11 = new(47.11m, Iso4217.USD);
Money chf_23_42 = new(23.42m, Iso4217.CHF);Money is an immutable record struct and exposes convenience properties
IsZero, IsPositive, and IsNegative.
Note:
Moneyis a value type.default(Money)is intentionally invalid — accessingCurrencythrowsNoCurrencyException. Always construct vianew Money(amount, currency)orMoney.Zero(currency).
Wallet empty = Wallet.Empty;
Wallet simple = Wallet.Of(eur_47_11);
Wallet multiple = Wallet.Of(eur_47_11, usd_47_11, chf_23_42);
Wallet collection = Wallet.Of([eur_47_11, usd_47_11, chf_23_42]);A Wallet aggregates money per currency automatically. It is enumerable
(IEnumerable<Money>) and provides structural equality.
Implement IExchangeRateProvider to plug in any rate source (REST API,
database, cache, ...):
public interface IExchangeRateProvider
{
Task<ExchangeRateContext> GetLatestAsync(CancellationToken cancellationToken = default);
Task<ExchangeRateContext> GetHistoricalAsync(DateTimeOffset date, CancellationToken cancellationToken = default);
}IExchangeRateProvider provider = new ExampleExchangeRateProvider();
DateTimeOffset exchangeRateDate = new(new DateTime(2020, 1, 1), TimeSpan.Zero);
ExchangeRateContext latest = await provider.GetLatestAsync();
ExchangeRateContext historical = await provider.GetHistoricalAsync(exchangeRateDate);See
example/CurrenCSharp.Example/ExampleExchangeRateProvider.cs
for a minimal reference implementation.
Use .In(context) to attach an exchange-rate context and enable conversion
and cross-currency comparison.
ContextedMoney latest_money = eur_47_11.In(latest);
ContextedMoney historical_money = eur_47_11.In(historical);
ContextedWallet latest_wallet = collection.In(latest);
ContextedWallet historical_wallet = collection.In(historical);ContextedMoney.Convert converts the amount into another currency using
the bound context:
Money latest_money_usd = latest_money.Convert(Iso4217.USD); // USD 51.41
Money historical_money_usd = historical_money.Convert(Iso4217.USD); // USD 42.08Rounding is controlled via ConversionOptions:
Money custom = latest_money.Convert(
Iso4217.USD,
new ConversionOptions(
RoundResult: true,
RoundingMode: MidpointRounding.AwayFromZero,
Scale: new Scale(4)));ExchangeRateContext automatically handles inverse rates and cross rates
via the base currency.
ContextedWallet.Total() sums the wallet into a single Money. Without a
target currency it uses the wallet's resolved currency (the single currency
in the wallet, or the ambient default).
Money latest_total_KeyCurrency = latest_wallet.Total(); // EUR 140.10
Money latest_total_usd = latest_wallet.Total(Iso4217.USD); // USD 152.87Money negate = -eur_47_11; // EUR -47.11
Money sum = eur_47_11 + eur_23_42; // EUR 70.53
Money diff = eur_47_11 - eur_23_42; // EUR 23.69
Money multiply = eur_47_11 * 2; // EUR 94.22
decimal quote = eur_47_11 / eur_23_42; // 2.01Mixing currencies in +, -, or / throws DifferentCurrencyException.
Wallet negate = -collection; // EUR -47.11, USD -47.11, CHF -47.11
Wallet addition = collection + eur_23_42; // EUR 70.53, USD 47.11, CHF 47.11
Wallet subtraction = collection - eur_23_42; // EUR 23.69, USD 47.11, CHF 47.11
Wallet multiply = collection * 3; // EUR 141.33, USD 141.33, CHF 141.33
Wallet division = collection / 2; // EUR 23.56, USD 23.56, CHF 23.56Adding or subtracting Money updates the matching currency bucket; adding
two wallets merges them per currency. Scalar * / / apply to every entry.
Equality and ordering operators are defined for Money, Wallet, and their
context-aware counterparts. Operators that cross currencies require a
context-bound operand.
bool isEqual = eur_47_11 == eur_47_11; // True
bool isNotEqual = eur_47_11 != eur_23_42; // True
bool isGreater = eur_47_11 > eur_23_42; // True
bool isGreaterOrEqual = eur_47_11 >= eur_23_42; // True
bool isLess = eur_47_11 < eur_23_42; // False
bool isLessOrEqual = eur_47_11 <= eur_23_42; // Falsebool a = usd_47_11 == latest_money; // False
bool b = latest_money > usd_47_11; // True
bool c = usd_47_11 <= latest_money; // Truebool a = eur_47_11 == latest_wallet; // False
bool b = latest_wallet > eur_47_11; // True
bool c = eur_47_11 <= latest_wallet; // Truebool a = collection == latest_money; // False
bool b = collection > latest_money; // True
bool c = latest_money <= collection; // Truebool a = collection == latest_wallet; // True
bool b = collection >= latest_wallet; // True
bool c = latest_wallet <= collection; // TrueMoney.Distribute splits an amount into parts without losing minor units.
Remaining units are allocated to the largest ratios (ties broken by index).
var parts = eur_47_11.Distribute(3);
// EUR 15.71, EUR 15.70, EUR 15.70var parts = eur_47_11.Distribute(3, 1, 2, 0);
// EUR 23.56, EUR 7.85, EUR 15.70, EUR 0.00Ratio is a non-negative value type with implicit conversion from
decimal. The sum of ratios must be greater than zero.
AlphaCode and NumericCode validate ISO 4217 codes and offer
Parse / TryParse:
AlphaCode alpha = AlphaCode.Parse("EUR");
NumericCode numeric = NumericCode.Parse("978");
Currency eur = new(alpha, numeric, 2);CurrenCSharp.Currencies ships predefined Currency instances for all
ISO 4217 codes and a lookup cache:
using CurrenCSharp.Currencies;
Currency eur = Iso4217.EUR;
Currency foundByAlpha = Iso4217.FindByAlphaCode("USD");
Currency foundByNumeric = Iso4217.FindByNumericCode(840);FindByAlphaCode and FindByNumericCode throw
InvalidOperationException when the code is not defined in ISO 4217.
See LICENSE.