diff --git a/NMoneys.sln b/NMoneys.sln
index 476dfb2..12bdbd3 100644
--- a/NMoneys.sln
+++ b/NMoneys.sln
@@ -15,6 +15,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "archive", "archive", "{DB66
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NMoneys.Exchange", "archive\NMoneys.Exchange\NMoneys.Exchange.csproj", "{97C58515-92D8-4ECF-86B6-CFAABD66279D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NMoneys.Serialization", "src\NMoneys.Serialization\NMoneys.Serialization.csproj", "{F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NMoneys.Serialization.Tests", "tests\NMoneys.Serialization.Tests\NMoneys.Serialization.Tests.csproj", "{A28BEBB8-EC70-4FC5-AFE7-9989F816347B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -36,10 +40,20 @@ Global
{97C58515-92D8-4ECF-86B6-CFAABD66279D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97C58515-92D8-4ECF-86B6-CFAABD66279D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97C58515-92D8-4ECF-86B6-CFAABD66279D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A28BEBB8-EC70-4FC5-AFE7-9989F816347B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A28BEBB8-EC70-4FC5-AFE7-9989F816347B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A28BEBB8-EC70-4FC5-AFE7-9989F816347B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A28BEBB8-EC70-4FC5-AFE7-9989F816347B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0484566C-1D24-433E-90F2-B8B44BEF2FB2} = {558BFE88-EB25-4BFA-91B8-4CFF8696CED4}
{891B60F6-888D-488A-89D0-6E25FE1A7883} = {6454722E-5710-44B0-8EDE-1DE92929E641}
{97C58515-92D8-4ECF-86B6-CFAABD66279D} = {DB66F4E1-075E-4E1C-931E-5DD8F956E484}
+ {F61AA4C5-EAF0-425F-BF23-8B04DB85A1FF} = {6454722E-5710-44B0-8EDE-1DE92929E641}
+ {A28BEBB8-EC70-4FC5-AFE7-9989F816347B} = {558BFE88-EB25-4BFA-91B8-4CFF8696CED4}
EndGlobalSection
EndGlobal
diff --git a/src/NMoneys.Serialization/BSON/MoneySerializer.cs b/src/NMoneys.Serialization/BSON/MoneySerializer.cs
new file mode 100644
index 0000000..54ee910
--- /dev/null
+++ b/src/NMoneys.Serialization/BSON/MoneySerializer.cs
@@ -0,0 +1,101 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using MongoDB.Bson;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+
+namespace NMoneys.Serialization.BSON;
+
+///
+/// Allows customizing the serialization of monetary quantities
+///
+public class MoneySerializer : StructSerializerBase
+{
+ // maps cannot be changed so the looked-up instance is cached
+ private readonly Lazy> _map = new(() =>
+ (BsonClassMap)BsonClassMap.LookupClassMap(typeof(Money)));
+
+ ///
+ public override void Serialize([NotNull]BsonSerializationContext context, BsonSerializationArgs args, Money value)
+ {
+ context.Writer.WriteStartDocument();
+
+ writeAmount(context.Writer, value);
+
+ writeCurrency(context, args, value);
+
+ context.Writer.WriteEndDocument();
+ }
+
+ ///
+ public override Money Deserialize([NotNull]BsonDeserializationContext context, BsonDeserializationArgs args)
+ {
+ context.Reader.ReadStartDocument();
+
+ decimal amount = readAmount(context.Reader);
+ CurrencyIsoCode currency = readCurrency(context, args);
+
+ context.Reader.ReadEndDocument();
+
+ var deserialized = new Money(amount, currency);
+ return deserialized;
+ }
+
+ #region write
+
+ private void writeAmount(IBsonWriter writer, Money value)
+ {
+ string amountName = _map.Value.GetMemberMap(m => m.Amount).ElementName;
+ writer.WriteName(amountName);
+ writer.WriteDecimal128(value.Amount);
+ }
+
+ private void writeCurrency(BsonSerializationContext context, BsonSerializationArgs args, Money value)
+ {
+ var currencyMap = _map.Value.GetMemberMap(c => c.CurrencyCode);
+ string currencyName = char.IsLower(currencyMap.ElementName, 0) ? "currency" : "Currency";
+ context.Writer.WriteName(currencyName);
+ currencyMap.GetSerializer().Serialize(context, args, value.CurrencyCode);
+ }
+
+ #endregion
+
+ #region read
+
+ private decimal readAmount(IBsonReader reader)
+ {
+ var amountMap = _map.Value.GetMemberMap(m => m.Amount);
+ reader.ReadName(amountMap.ElementName);
+ BsonType type = reader.GetCurrentBsonType();
+ decimal amount;
+ switch (type)
+ {
+ // does not support floating point conversion
+ case BsonType.String:
+ string str = reader.ReadString();
+ amount = decimal.Parse(str, CultureInfo.InvariantCulture);
+ break;
+ case BsonType.Decimal128:
+ Decimal128 dec = reader.ReadDecimal128();
+ amount = Decimal128.ToDecimal(dec);
+ break;
+ default:
+ throw new NotSupportedException($"Cannot extract a monetary amount out of '{type.ToString()}'.");
+ }
+
+ return amount;
+ }
+
+ private CurrencyIsoCode readCurrency(BsonDeserializationContext context, BsonDeserializationArgs args)
+ {
+ var currencyMap = _map.Value.GetMemberMap(m => m.CurrencyCode);
+ string currencyName = char.IsLower(currencyMap.ElementName, 0) ? "currency" : "Currency";
+ context.Reader.ReadName(currencyName);
+ var currency = (CurrencyIsoCode)currencyMap.GetSerializer()
+ .Deserialize(context, args);
+ return currency;
+ }
+
+ #endregion
+}
diff --git a/src/NMoneys.Serialization/BSON/QuantitySerializer.cs b/src/NMoneys.Serialization/BSON/QuantitySerializer.cs
new file mode 100644
index 0000000..fe01aad
--- /dev/null
+++ b/src/NMoneys.Serialization/BSON/QuantitySerializer.cs
@@ -0,0 +1,23 @@
+using System.Diagnostics.CodeAnalysis;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+
+namespace NMoneys.Serialization.BSON;
+
+public class QuantitySerializer : StructSerializerBase
+{
+ ///
+ public override void Serialize([NotNull]BsonSerializationContext context, BsonSerializationArgs args, Money value)
+ {
+ context.Writer.WriteString(value.AsQuantity());
+ }
+
+ ///
+ public override Money Deserialize([NotNull]BsonDeserializationContext context, BsonDeserializationArgs args)
+ {
+ string quantity = context.Reader.ReadString();
+
+ Money deserialized = Money.Parse(quantity);
+ return deserialized;
+ }
+}
diff --git a/src/NMoneys.Serialization/EFCore/JsonConverter.cs b/src/NMoneys.Serialization/EFCore/JsonConverter.cs
new file mode 100644
index 0000000..0da02d3
--- /dev/null
+++ b/src/NMoneys.Serialization/EFCore/JsonConverter.cs
@@ -0,0 +1,17 @@
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using System.Text.Json;
+
+namespace NMoneys.Serialization.EFCore;
+
+///
+/// Defines conversions from an instance of in a model to a in the store.
+///
+public class JsonConverter : ValueConverter
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JsonConverter(JsonSerializerOptions options) : base(
+ m => JsonSerializer.Serialize(m, options),
+ str => JsonSerializer.Deserialize(str, options)) { }
+}
diff --git a/src/NMoneys.Serialization/EFCore/MoneyComparer.cs b/src/NMoneys.Serialization/EFCore/MoneyComparer.cs
new file mode 100644
index 0000000..9a179ed
--- /dev/null
+++ b/src/NMoneys.Serialization/EFCore/MoneyComparer.cs
@@ -0,0 +1,14 @@
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace NMoneys.Serialization.EFCore;
+
+///
+/// Specifies custom value snapshotting and comparison for .
+///
+public class MoneyComparer : ValueComparer
+{
+ ///
+ /// Creates a new . A shallow copy will be used for the snapshot.
+ ///
+ public MoneyComparer() : base((x, y) => x.Equals(y), m => m.GetHashCode()) { }
+}
diff --git a/src/NMoneys.Serialization/EFCore/QuantityConverter.cs b/src/NMoneys.Serialization/EFCore/QuantityConverter.cs
new file mode 100644
index 0000000..72c6d80
--- /dev/null
+++ b/src/NMoneys.Serialization/EFCore/QuantityConverter.cs
@@ -0,0 +1,16 @@
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace NMoneys.Serialization.EFCore;
+
+///
+/// Defines conversions from an instance of in a model to a in the store.
+///
+public class QuantityConverter : ValueConverter
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public QuantityConverter() : base(
+ m => m.AsQuantity(),
+ str => Money.Parse(str)) { }
+}
diff --git a/src/NMoneys.Serialization/Json_NET/DescriptiveMoneyConverter.cs b/src/NMoneys.Serialization/Json_NET/DescriptiveMoneyConverter.cs
new file mode 100644
index 0000000..230226e
--- /dev/null
+++ b/src/NMoneys.Serialization/Json_NET/DescriptiveMoneyConverter.cs
@@ -0,0 +1,190 @@
+using System.Diagnostics.CodeAnalysis;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+
+namespace NMoneys.Serialization.Json_NET;
+
+///
+/// Converts an monetary quantity to and from JSON.
+///
+///
+/// The serialized quantity would look something like: {"Amount": 0, "Currency": "XXX"}.
+/// Provides better JSON pointers for deserialization errors: better suited when not in control of serialization.
+///
+public class DescriptiveMoneyConverter : JsonConverter
+{
+ private readonly bool _forceStringEnum;
+
+ /// Ignore enum value configuration and force string representation of the
+ /// when serializing.
+ public DescriptiveMoneyConverter(bool forceStringEnum = false)
+ {
+ _forceStringEnum = forceStringEnum;
+ }
+
+ #region write
+
+ ///
+ public override void WriteJson([NotNull] JsonWriter writer, Money value, [NotNull] JsonSerializer serializer)
+ {
+ DefaultContractResolver? resolver = serializer.ContractResolver as DefaultContractResolver;
+
+ writer.WriteStartObject();
+
+ writeAmount(value.Amount, writer, resolver);
+ writeCurrency(value.CurrencyCode, writer, serializer, resolver);
+
+ writer.WriteEndObject();
+ }
+
+ private static void writeAmount(decimal amount, JsonWriter writer, DefaultContractResolver? resolver)
+ {
+ // non-pascal if "weird" resolver
+ string amountName = resolver?.GetResolvedPropertyName("Amount") ?? "amount";
+ writer.WritePropertyName(amountName);
+ writer.WriteValue(amount);
+ }
+
+ private void writeCurrency(CurrencyIsoCode currency,
+ JsonWriter writer, JsonSerializer serializer, DefaultContractResolver? resolver)
+ {
+ // non-pascal if "weird" resolver
+ string currencyName = resolver?.GetResolvedPropertyName("Currency") ?? "currency";
+ writer.WritePropertyName(currencyName);
+ if (_forceStringEnum)
+ {
+ // ignore configured enum value convention, string it is
+ writer.WriteValue(currency.ToString());
+ }
+ else
+ {
+ // follow configured enum value convention
+ serializer.Serialize(writer, currency, typeof(CurrencyIsoCode));
+ }
+ }
+
+ #endregion
+
+ #region read
+
+ ///
+ public override Money ReadJson([NotNull] JsonReader reader,
+ Type objectType, Money existingValue,
+ bool hasExistingValue, [NotNull] JsonSerializer serializer)
+ {
+ DefaultContractResolver? resolver = serializer.ContractResolver as DefaultContractResolver;
+
+ // no need to read StartObject token (already read)
+
+ decimal amount = readAmount(reader, resolver);
+ CurrencyIsoCode currency = readCurrency(reader, serializer, resolver);
+
+ // but EndObject needs to be read
+ readEndObject(reader);
+
+ return new Money(amount, currency);
+ }
+
+ private static decimal readAmount(JsonReader reader, DefaultContractResolver? resolver)
+ {
+ readProperty(reader);
+ ensurePropertyName("Amount", reader, resolver);
+ decimal amount = readAmountValue(reader);
+ return amount;
+ }
+
+ private static CurrencyIsoCode readCurrency(JsonReader reader, JsonSerializer serializer, DefaultContractResolver? resolver)
+ {
+ readProperty(reader);
+ ensurePropertyName("Currency", reader, resolver);
+ CurrencyIsoCode currency = readCurrencyValue(reader, serializer);
+ return currency;
+ }
+
+ private static void readEndObject(JsonReader reader)
+ {
+ bool read = reader.Read();
+ if (!read || reader.TokenType != JsonToken.EndObject)
+ {
+ throw buildException($"Expected token type '{JsonToken.EndObject}', but got '{reader.TokenType}'.", reader);
+ }
+ }
+
+ private static JsonSerializationException buildException(string message, JsonReader reader, Exception? inner = null)
+ {
+ IJsonLineInfo? info = reader as IJsonLineInfo;
+ JsonSerializationException exception = (info == null || info.HasLineInfo()) ?
+ new JsonSerializationException(message) :
+ new JsonSerializationException(message, reader.Path, info.LineNumber, info.LinePosition, inner);
+ return exception;
+ }
+
+
+ private static void readProperty(JsonReader reader)
+ {
+ bool isRead = reader.Read();
+ if (!isRead || reader.TokenType != JsonToken.PropertyName)
+ {
+ throw buildException($"Expected token type '{JsonToken.PropertyName}', but got '{reader.TokenType}'.", reader);
+ }
+ }
+
+ private static void ensurePropertyName(string pascalSingleName, JsonReader reader, DefaultContractResolver? resolver)
+ {
+#pragma warning disable CA1308
+ string propName = resolver?.GetResolvedPropertyName(pascalSingleName) ?? pascalSingleName.ToLowerInvariant();
+#pragma warning restore CA1308
+ bool matchAmount = StringComparer.Ordinal.Equals(reader.Value, propName);
+ if (!matchAmount)
+ {
+ throw buildException($"Expected property '{propName}', but got '{reader.Value}'.", reader);
+ }
+ }
+
+ private static decimal readAmountValue(JsonReader reader)
+ {
+ try
+ {
+ var amount = reader.ReadAsDecimal();
+ if (!amount.HasValue)
+ {
+ throw buildException("Amount should not be nullable.", reader);
+ }
+
+ return amount.Value;
+ }
+ catch (Exception ex)
+ {
+ throw buildException("Could not read amount value.", reader, ex);
+ }
+ }
+
+ private static CurrencyIsoCode readCurrencyValue(JsonReader reader, JsonSerializer serializer)
+ {
+ bool read = reader.Read();
+ if (!read)
+ {
+ throw buildException("Expected value token type.", reader);
+ }
+
+ CurrencyIsoCode currency = serializer.Deserialize(reader);
+ ensureDefined(currency, reader);
+
+ return currency;
+ }
+
+ private static void ensureDefined(CurrencyIsoCode maybeCurrency, JsonReader reader)
+ {
+ try
+ {
+ maybeCurrency.AssertDefined();
+ }
+ catch (Exception ex)
+ {
+ throw buildException($"Currency '{maybeCurrency}' not defined.", reader, ex);
+ }
+ }
+
+
+ #endregion
+}
diff --git a/src/NMoneys.Serialization/Json_NET/MoneyConverter.cs b/src/NMoneys.Serialization/Json_NET/MoneyConverter.cs
new file mode 100644
index 0000000..3c17645
--- /dev/null
+++ b/src/NMoneys.Serialization/Json_NET/MoneyConverter.cs
@@ -0,0 +1,119 @@
+using System.Diagnostics.CodeAnalysis;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Newtonsoft.Json.Serialization;
+
+namespace NMoneys.Serialization.Json_NET;
+
+///
+/// Converts an monetary quantity to and from JSON.
+///
+///
+/// The serialized quantity would look something like: {"Amount": 0, "Currency": "XXX"}.
+/// Provides limited JSON pointers for deserialization errors: better suited when in control of serialization.
+///
+public class MoneyConverter : JsonConverter
+{
+ private readonly bool _forceStringEnum;
+
+ /// Ignore enum value configuration and force string representation of the
+ /// when serializing.
+ public MoneyConverter(bool forceStringEnum = false)
+ {
+ _forceStringEnum = forceStringEnum;
+ }
+
+ #region read
+
+ ///
+ public override Money ReadJson([NotNull] JsonReader reader,
+ Type objectType, Money existingValue,
+ bool hasExistingValue, [NotNull] JsonSerializer serializer)
+ {
+ JObject obj = readObject(reader);
+ decimal amount = getAmount(obj);
+ CurrencyIsoCode currency = getCurrency(obj);
+ return new Money(amount, currency);
+ }
+
+ private static JObject readObject(JsonReader reader)
+ {
+ JToken objToken = JToken.ReadFrom(reader);
+ if (objToken.Type != JTokenType.Object)
+ {
+ throw new JsonSerializationException($"Expected token '{JTokenType.Object}', but got '{objToken.Type}'.");
+ }
+
+ return (JObject)objToken;
+ }
+
+ private static decimal getAmount(JObject obj)
+ {
+ string propName = "Amount";
+ JProperty amountProp = getProperty(obj, propName);
+ decimal? amount = amountProp.Value.Value();
+ return amount ?? throw new JsonSerializationException($"'{propName}' cannot be null.");
+ }
+
+ private static CurrencyIsoCode getCurrency(JObject obj)
+ {
+ string propName = "Currency";
+ JProperty currencyProp = getProperty(obj, propName);
+ var currency = currencyProp.Value.ToObject();
+ currency.AssertDefined();
+ return currency;
+ }
+
+ private static JProperty getProperty(JObject obj, string singleWordPropName)
+ {
+ // since props are single-word, case ignoring cover most common: pascal, camel, snake, kebab
+ JProperty? amountProp = obj.Property(singleWordPropName, StringComparison.OrdinalIgnoreCase) ??
+ throw new JsonSerializationException($"Missing property '{singleWordPropName}'.");
+ return amountProp;
+ }
+
+ #endregion
+
+ #region write
+
+ ///
+ public override void WriteJson([NotNull] JsonWriter writer, Money value, [NotNull] JsonSerializer serializer)
+ {
+ DefaultContractResolver? resolver = serializer.ContractResolver as DefaultContractResolver;
+
+ writer.WriteStartObject();
+
+ writeAmount(value.Amount, writer, resolver);
+ writeCurrency(value.CurrencyCode, writer, serializer, resolver);
+
+ writer.WriteEndObject();
+ }
+
+ private static void writeAmount(decimal amount, JsonWriter writer, DefaultContractResolver? resolver)
+ {
+ // non-pascal if "weird" resolver
+ string amountName = resolver?.GetResolvedPropertyName("Amount") ?? "Amount";
+ writer.WritePropertyName(amountName);
+ writer.WriteValue(amount);
+ }
+
+ private void writeCurrency(CurrencyIsoCode currency,
+ JsonWriter writer, JsonSerializer serializer, DefaultContractResolver? resolver)
+ {
+ // non-pascal if "weird" resolver
+ string currencyName = resolver?.GetResolvedPropertyName("Currency") ?? "Currency";
+ writer.WritePropertyName(currencyName);
+ if (_forceStringEnum)
+ {
+ // ignore configured enum value convention, string it is
+ writer.WriteValue(currency.ToString());
+ }
+ else
+ {
+ // follow configured enum value convention
+ serializer.Serialize(writer, currency, typeof(CurrencyIsoCode));
+ }
+ }
+
+ #endregion
+}
diff --git a/src/NMoneys.Serialization/Json_NET/QuantityConverter.cs b/src/NMoneys.Serialization/Json_NET/QuantityConverter.cs
new file mode 100644
index 0000000..a4d10cf
--- /dev/null
+++ b/src/NMoneys.Serialization/Json_NET/QuantityConverter.cs
@@ -0,0 +1,37 @@
+using System.Diagnostics.CodeAnalysis;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace NMoneys.Serialization.Json_NET;
+
+///
+/// Converts an monetary quantity to and from JSON.
+///
+///
+/// The serialized quantity would look something like: "XTS 0".
+///
+public class QuantityConverter : JsonConverter
+{
+ ///
+ public override Money ReadJson([NotNull] JsonReader reader,
+ Type objectType, Money existingValue,
+ bool hasExistingValue, [NotNull] JsonSerializer serializer)
+ {
+ JToken strToken = JToken.ReadFrom(reader);
+ if (strToken.Type != JTokenType.String)
+ {
+ throw new JsonSerializationException($"Expected token '{JTokenType.String}', but got '{strToken.Type}'.");
+ }
+
+ string quantity = strToken.Value() ?? throw new JsonSerializationException($"Quantity cannot be null.");
+
+ Money parsed = Money.Parse(quantity);
+ return parsed;
+ }
+
+ ///
+ public override void WriteJson([NotNull] JsonWriter writer, Money value, [NotNull] JsonSerializer serializer)
+ {
+ writer.WriteValue(value.AsQuantity());
+ }
+}
diff --git a/src/NMoneys.Serialization/NMoneys.Serialization.csproj b/src/NMoneys.Serialization/NMoneys.Serialization.csproj
new file mode 100644
index 0000000..e5ada77
--- /dev/null
+++ b/src/NMoneys.Serialization/NMoneys.Serialization.csproj
@@ -0,0 +1,54 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ true
+ true
+
+
+
+ Custom serialization/deserialization code samples of monetary quantities with several popular serialization libraries.
+ NMoneys.Serialization
+ Copyright © Daniel Gonzalez Garcia 2024
+
+
+
+ https://github.com/dgg/nmoneys.git
+ git
+ .net;dotnet;C#;currency;money;iso;monetary;quantity;iso4217;serialization;Json.NET;BSON;Xml;System.Text.Json;Entity Framework
+
+
+
+ 5.0.0
+ 5.0.0.0
+ 5.0.0.0
+
+
+
+
+ <_Parameter1>false
+
+
+
+
+
+
+
+
+
+
+ CA1707,CA1508
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/NMoneys.Serialization/Text_Json/MoneyConverter.cs b/src/NMoneys.Serialization/Text_Json/MoneyConverter.cs
new file mode 100644
index 0000000..80cc2b7
--- /dev/null
+++ b/src/NMoneys.Serialization/Text_Json/MoneyConverter.cs
@@ -0,0 +1,98 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+
+namespace NMoneys.Serialization.Text_Json;
+
+///
+/// Converts an monetary quantity to and from JSON.
+///
+///
+/// The serialized quantity would look something like: {"Amount": 0, "Currency": "XXX"}.
+/// Provides limited JSON pointers for deserialization errors: better suited when in control of serialization.
+///
+public class MoneyConverter : JsonConverter
+{
+ // forcing enum string argument is not a good idea as deserialization will fail
+
+ #region read
+
+ public override Money Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ JsonObject obj = parseObject(ref reader);
+
+ decimal amount = getAmount(obj);
+ CurrencyIsoCode currency = getCurrency(obj, options);
+
+ return new Money(amount, currency);
+ }
+
+ private static JsonObject parseObject(ref Utf8JsonReader reader)
+ {
+ var nodeOpts = new JsonNodeOptions { PropertyNameCaseInsensitive = true };
+
+ JsonNode? objNode = JsonNode.Parse(ref reader, nodeOpts) ?? throw new JsonException($"Expected '{nameof(JsonObject)}' but was 'null'.");
+ JsonObject obj = objNode.AsObject();
+ return obj;
+ }
+
+ private static decimal getAmount(JsonObject obj)
+ {
+ string propName = "Amount";
+ JsonNode amountNode = getProperty(obj, propName);
+ var amount = amountNode.GetValue();
+ return amount;
+ }
+
+ private static CurrencyIsoCode getCurrency(JsonObject obj, JsonSerializerOptions options)
+ {
+ string propName = "Currency";
+ JsonNode currencyNode = getProperty(obj, propName);
+ var currency = currencyNode.Deserialize(options);
+ currency.AssertDefined();
+ return currency;
+ }
+
+ private static JsonNode getProperty(JsonObject obj, string singleWordPropName)
+ {
+ if (!obj.TryGetPropertyValue(singleWordPropName, out JsonNode? propertyNode) || propertyNode == null)
+ {
+ throw new JsonException($"Missing property '{singleWordPropName}'.");
+ }
+
+ return propertyNode;
+ }
+
+ #endregion
+
+ #region write
+
+ public override void Write([NotNull] Utf8JsonWriter writer, Money value, [NotNull] JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+
+ writeAmount(value.Amount, writer, options);
+ writeCurrency(value.CurrencyCode, writer, options);
+
+ writer.WriteEndObject();
+ }
+
+ private static void writeAmount(decimal amount, Utf8JsonWriter writer, JsonSerializerOptions options)
+ {
+ // pascal if no policy override
+ string amountName = options.PropertyNamingPolicy?.ConvertName("Amount") ?? "Amount";
+ writer.WriteNumber(amountName, amount);
+ }
+
+ private static void writeCurrency(CurrencyIsoCode currency, Utf8JsonWriter writer, JsonSerializerOptions options)
+ {
+ // pascal if no policy override
+ string currencyName = options.PropertyNamingPolicy?.ConvertName("Currency") ?? "Currency";
+
+ writer.WritePropertyName(currencyName);
+ JsonSerializer.Serialize(writer, currency, options);
+ }
+
+ #endregion
+}
diff --git a/src/NMoneys.Serialization/Text_Json/QuantityConverter.cs b/src/NMoneys.Serialization/Text_Json/QuantityConverter.cs
new file mode 100644
index 0000000..2e24548
--- /dev/null
+++ b/src/NMoneys.Serialization/Text_Json/QuantityConverter.cs
@@ -0,0 +1,29 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+
+namespace NMoneys.Serialization.Text_Json;
+
+///
+/// Converts an monetary quantity to and from JSON.
+///
+///
+/// The serialized quantity would look something like: "XXX 0".
+///
+public class QuantityConverter : JsonConverter
+{
+ public override Money Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ JsonNode strNode = JsonNode.Parse(ref reader) ?? throw new JsonException($"Expected '{nameof(JsonValue)}' but was 'null'.");;
+ JsonValue value = strNode.AsValue();
+ string quantity = value.GetValue();
+ Money parsed = Money.Parse(quantity);
+ return parsed;
+ }
+
+ public override void Write([NotNull] Utf8JsonWriter writer, Money value, [NotNull] JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.AsQuantity());
+ }
+}
diff --git a/src/NMoneys/CurrencyIsoCode.Extensions.cs b/src/NMoneys/CurrencyIsoCode.Extensions.cs
index bf910a3..fd8a957 100644
--- a/src/NMoneys/CurrencyIsoCode.Extensions.cs
+++ b/src/NMoneys/CurrencyIsoCode.Extensions.cs
@@ -76,7 +76,7 @@ public static bool Equals(this CurrencyIsoCode code, CurrencyIsoCode other)
/// Asserts whether the code exists in the enumeration, throwing if it doesn't.
///
/// The enumeration value to assert against.
- /// is not defined within .
+ /// is not defined within .
public static void AssertDefined(this CurrencyIsoCode code)
{
if (!CheckDefined(code))
diff --git a/src/NMoneys/MonetaryQuantity.cs b/src/NMoneys/MonetaryQuantity.cs
new file mode 100644
index 0000000..6ab334d
--- /dev/null
+++ b/src/NMoneys/MonetaryQuantity.cs
@@ -0,0 +1,87 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace NMoneys;
+
+///
+/// Represents the data of a monetary quantity.
+///
+/// Useful when serialization platform cannot be configured or it is very complex.
+/// In that scenario, one can use this class in the Data-Transfer Objects.
+///
+public class MonetaryQuantity
+{
+ ///
+ /// Initializes an instance of with an amount of 0 and no currency ().
+ ///
+ public MonetaryQuantity()
+ {
+ Currency = CurrencyIsoCode.XXX;
+ }
+
+ ///
+ /// Initializes an instance of with the provided amount and currency code ().
+ ///
+ /// The amount of a monetary quantity data.
+ /// The ISO 4217 code of the currency of a monetary quantity data.
+ public MonetaryQuantity(decimal amount, CurrencyIsoCode currency)
+ {
+ Amount = amount;
+ Currency = currency;
+ }
+
+ ///
+ /// The amount of a monetary quantity data.
+ ///
+ public decimal Amount { get; set; }
+ ///
+ /// The ISO 4217 code of the currency of a monetary quantity data.
+ ///
+ public CurrencyIsoCode Currency { get; set; }
+
+ ///
+ /// Defines an explicit conversion of a monetary quantity data () to a monetary quantity ().
+ ///
+ /// The monetary quantity data to convert.
+ /// The converted monetary quantity.
+ public static explicit operator Money([NotNull]MonetaryQuantity quantity) => new(quantity.Amount, quantity.Currency);
+ ///
+ /// Defines a conversion of a monetary quantity data () to a monetary quantity ().
+ ///
+ /// The monetary quantity data to convert.
+ /// The converted monetary quantity.
+ public static Money ToMoney(MonetaryQuantity quantity)
+ {
+ return (Money)quantity;
+ }
+ ///
+ /// Defines a conversion of the current monetary quantity data () to a monetary quantity ().
+ ///
+ /// The converted monetary quantity.
+ public Money ToMoney()
+ {
+ return (Money)this;
+ }
+
+ ///
+ /// Defines an explicit conversion of a monetary quantity () to a monetary quantity data ().
+ ///
+ /// The monetary quantity to convert.
+ /// The converted monetary quantity data.
+ public static explicit operator MonetaryQuantity(Money money) => new(money.Amount, money.CurrencyCode);
+ ///
+ /// Defines a conversion of a monetary quantity () to a monetary quantity data ().
+ ///
+ /// The monetary quantity to convert.
+ /// The converted monetary quantity data.
+ public static MonetaryQuantity FromMoney(Money money)
+ {
+ return (MonetaryQuantity)money;
+ }
+
+ ///
+ /// A string with the format '{three_letter_currency_code} {invariant_numeric_amount}' is used due to ease of parsing and compactness.
+ public override string ToString()
+ {
+ return Money.AsQuantity(Amount, Currency);
+ }
+}
diff --git a/src/NMoneys/MonetaryRecord.cs b/src/NMoneys/MonetaryRecord.cs
new file mode 100644
index 0000000..42178cd
--- /dev/null
+++ b/src/NMoneys/MonetaryRecord.cs
@@ -0,0 +1,61 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace NMoneys;
+
+///
+/// Represents the data of a monetary quantity.
+///
+/// Useful when serialization platform cannot be configured or it is very complex.
+/// In that scenario, one can use this record in the Data-Transfer Objects.
+///
+/// The amount of a monetary quantity data.
+/// The ISO 4217 code of the currency of a monetary quantity data.
+public record MonetaryRecord(decimal Amount = Decimal.Zero, CurrencyIsoCode Currency = CurrencyIsoCode.XXX)
+{
+ ///
+ /// Defines an explicit conversion of a monetary quantity data () to a monetary quantity ().
+ ///
+ /// The monetary quantity data to convert.
+ /// The converted monetary quantity.
+ public static explicit operator Money([NotNull]MonetaryRecord quantity) => new(quantity.Amount, quantity.Currency);
+ ///
+ /// Defines a conversion of a monetary quantity data () to a monetary quantity ().
+ ///
+ /// The monetary quantity data to convert.
+ /// The converted monetary quantity.
+ public static Money ToMoney(MonetaryRecord quantity)
+ {
+ return (Money)quantity;
+ }
+ ///
+ /// Defines a conversion of the current monetary quantity data () to a monetary quantity ().
+ ///
+ /// The converted monetary quantity.
+ public Money ToMoney()
+ {
+ return (Money)this;
+ }
+
+ ///
+ /// Defines an explicit conversion of a monetary quantity () to a monetary quantity data ().
+ ///
+ /// The monetary quantity to convert.
+ /// The converted monetary quantity data.
+ public static explicit operator MonetaryRecord(Money money) => new(money.Amount, money.CurrencyCode);
+ ///
+ /// Defines a conversion of a monetary quantity () to a monetary quantity data ().
+ ///
+ /// The monetary quantity to convert.
+ /// The converted monetary quantity data.
+ public static MonetaryRecord FromMoney(Money money)
+ {
+ return (MonetaryRecord)money;
+ }
+
+ ///
+ /// A string with the format '{three_letter_currency_code} {invariant_numeric_amount}' is used due to ease of parsing and compactness.
+ public override string ToString()
+ {
+ return Money.AsQuantity(Amount, Currency);
+ }
+};
diff --git a/src/NMoneys/Money.Formatting.cs b/src/NMoneys/Money.Formatting.cs
index 8dc84dc..cfcdfae 100644
--- a/src/NMoneys/Money.Formatting.cs
+++ b/src/NMoneys/Money.Formatting.cs
@@ -1,8 +1,9 @@
using System.Diagnostics.Contracts;
+using System.Globalization;
namespace NMoneys;
-public partial struct Money
+public partial struct Money
{
///
/// Converts the numeric value of the to its equivalent string representation using an instance of the
@@ -60,91 +61,123 @@ public string ToString(string format, IFormatProvider provider)
}
///
- /// Replaces the format item in a specified string
with information from the
- /// identified by the instance's .
- /// An instance of the identified by will be supplying culture-specific formatting information.
- ///
- ///
- /// The following table describes the tokens that will be replaced in the :
- ///
- ///
- /// Token
- /// Description
- ///
- /// -
- /// {0}
- /// This token represents the of the current instance.
- ///
- /// -
- /// {1}
- /// This token represents the for the currency of the current instance
- ///
- /// -
- /// {2}
- /// This token represents the for the currency of the current instance
- ///
- /// -
- /// {3}
- /// This token represents the for the currency of the current instance
- ///
- /// -
- /// {4}
- /// This token represents the for the currency of the current instance
- ///
- ///
- ///
- /// A composite format string that can contain tokens to be replaced by properties of the
- /// identified by the instance's .
- /// A copy of in which the format items have been replaced by the string representation of the corresponding tokens.
- [Pure]
- public string Format(string format)
- {
- Currency currency = Currency.Get(CurrencyCode);
- return string.Format(currency, format, Amount, currency.Symbol, currency.IsoCode, currency.EnglishName, currency.NativeName);
- }
+ /// Returns a string representation of the monetary quantity suitable for serialization/storage.
+ ///
+ ///
+ /// A string with the format '{three_letter_currency_code} {invariant_numeric_amount}' is used due to ease of parsing and compactness.
+ ///
+ /// A compact, unequivocal representation of the monetary quantity.
+ [Pure]
+ public string AsQuantity()
+ {
+ string quantity = AsQuantity(Amount, CurrencyCode);
+ return quantity;
+ }
+
+ ///
+ /// Returns a string representation of the monetary quantity suitable for serialization/storage.
+ ///
+ ///
+ /// A string with the format '{three_letter_currency_code} {invariant_numeric_amount}' is used due to ease of parsing and compactness.
+ ///
+ /// The amount of a monetary quantity.
+ /// The ISO 4217 code of the currency of a monetary quantity.
+ /// A compact, unequivocal representation of the monetary quantity.
+ [Pure]
+ internal static string AsQuantity(decimal amount, CurrencyIsoCode currency)
+ {
+ string quantity = string.Format(NumberFormatInfo.InvariantInfo, "{0} {1:r}", currency, amount);
+ return quantity;
+ }
///
- /// Replaces the format item in a specified string
with information from the
- /// identified by the instance's .
- /// The will be supplying culture-specific formatting information.
- ///
- ///
- /// The following table describes the tokens that will be replaced in the :
- ///
- ///
- /// Token
- /// Description
- ///
- /// -
- /// {0}
- /// This token represents the of the current instance.
- ///
- /// -
- /// {1}
- /// This token represents the for the currency of the current instance
- ///
- /// -
- /// {2}
- /// This token represents the for the currency of the current instance
- ///
- /// -
- /// {3}
- /// This token represents the for the currency of the current instance
- ///
- /// -
- /// {4}
- /// This token represents the for the currency of the current instance
- ///
- ///
- ///
- /// A composite format string that can contain tokens to be replaced by properties of the
- /// identified by the instance's .
- /// An object that supplies culture-specific formatting information.
- /// A copy of in which the format items have been replaced by the string representation of the corresponding tokens.
- [Pure]
- public string Format(string format, IFormatProvider provider)
- {
- Currency currency = Currency.Get(CurrencyCode);
- return string.Format(provider, format, Amount, currency.Symbol, currency.IsoCode, currency.EnglishName, currency.NativeName);
- }
+ /// Replaces the format item in a specified string
with information from the
+ /// identified by the instance's .
+ /// An instance of the identified by will be supplying culture-specific formatting information.
+ ///
+ ///
+ /// The following table describes the tokens that will be replaced in the :
+ ///
+ ///
+ /// Token
+ /// Description
+ ///
+ /// -
+ /// {0}
+ /// This token represents the of the current instance.
+ ///
+ /// -
+ /// {1}
+ /// This token represents the for the currency of the current instance
+ ///
+ /// -
+ /// {2}
+ /// This token represents the for the currency of the current instance
+ ///
+ /// -
+ /// {3}
+ /// This token represents the for the currency of the current instance
+ ///
+ /// -
+ /// {4}
+ /// This token represents the for the currency of the current instance
+ ///
+ ///
+ ///
+ /// A composite format string that can contain tokens to be replaced by properties of the
+ /// identified by the instance's .
+ /// A copy of in which the format items have been replaced by the string representation of the corresponding tokens.
+ [Pure]
+ public string Format(string format)
+ {
+ Currency currency = Currency.Get(CurrencyCode);
+ return string.Format(currency, format, Amount, currency.Symbol, currency.IsoCode, currency.EnglishName,
+ currency.NativeName);
+ }
+
+ ///
+ /// Replaces the format item in a specified string
with information from the
+ /// identified by the instance's .
+ /// The will be supplying culture-specific formatting information.
+ ///
+ ///
+ /// The following table describes the tokens that will be replaced in the :
+ ///
+ ///
+ /// Token
+ /// Description
+ ///
+ /// -
+ /// {0}
+ /// This token represents the of the current instance.
+ ///
+ /// -
+ /// {1}
+ /// This token represents the for the currency of the current instance
+ ///
+ /// -
+ /// {2}
+ /// This token represents the for the currency of the current instance
+ ///
+ /// -
+ /// {3}
+ /// This token represents the for the currency of the current instance
+ ///
+ /// -
+ /// {4}
+ /// This token represents the for the currency of the current instance
+ ///
+ ///
+ ///
+ /// A composite format string that can contain tokens to be replaced by properties of the
+ /// identified by the instance's .
+ /// An object that supplies culture-specific formatting information.
+ /// A copy of in which the format items have been replaced by the string representation of the corresponding tokens.
+ [Pure]
+ public string Format(string format, IFormatProvider provider)
+ {
+ Currency currency = Currency.Get(CurrencyCode);
+ return string.Format(provider, format, Amount, currency.Symbol, currency.IsoCode, currency.EnglishName,
+ currency.NativeName);
+ }
}
diff --git a/src/NMoneys/Money.Parsing.cs b/src/NMoneys/Money.Parsing.cs
index c0eee35..ee845c4 100644
--- a/src/NMoneys/Money.Parsing.cs
+++ b/src/NMoneys/Money.Parsing.cs
@@ -5,146 +5,200 @@ namespace NMoneys;
public partial struct Money
{
-///
- /// Converts the string representation of a monetary quantity to its equivalent
- /// using the style and the specified currency as format information.
- ///
- /// This method assumes to have a style.
- /// The string representation of the monetary quantity to convert.
- /// Expected currency of that provides format information.
- /// The equivalent to the monetary quantity contained in as specified by the .
- /// is not in the correct format.
- /// represents a monetary quantity less than or greater than .
- /// is null.
- ///
- [Pure]
- public static Money Parse(string s, Currency currency)
- {
- return Parse(s, NumberStyles.Currency, currency);
- }
+ ///
+ /// Converts the string representation of a monetary quantity () to its equivalent.
+ ///
+ /// The currency value does not contain enumeration information.
+ /// The amount value is not in the correct format.
+ /// The currency value does not contain a defined .
+ /// Components of the representation are missing.
+ /// Monetary quantity equivalent to its representation.
+ [Pure]
+ public static Money Parse(string quantity)
+ {
+ ReadOnlySpan span = quantity.AsSpan();
+ CurrencyIsoCode currency = Enum.Parse(span[..3], ignoreCase: false);
+ currency.AssertDefined();
+ decimal amount = decimal.Parse(span[4..],
+ style: NumberStyles.Float,
+ provider: NumberFormatInfo.InvariantInfo);
+ return new Money(amount, currency);
+ }
- ///
- /// Converts the string representation of a monetary quantity to its equivalent
- /// using the specified style and the specified currency as format information.
- ///
- /// Use this method when is suspected not to have a style or more control over the operation is needed.
- /// The string representation of the monetary quantity to convert.
- /// A bitwise combination of values that indicates the style elements that can be present in .
- /// A typical value to specify is .
- /// Expected currency of that provides format information.
- /// The equivalent to the monetary quantity contained in as specified by and .
- /// is not in the correct format.
- /// represents a monetary quantity less than or greater than .
- /// is null.
- /// is not a value
- /// -or-
- /// is the value.
- ///
- ///
- [Pure]
- public static Money Parse(string s, NumberStyles style, Currency currency)
+ ///
+ /// Tries to converts the string representation of a monetary quantity () to its equivalent.
+ ///
+ ///
+ /// When this method returns, contains the monetary quantity equivalent to its representation contained in ,
+ /// if the conversion succeeded, or null if the conversion failed.
+ /// true if was converted successfully; otherwise, false.
+ [Pure]
+ public static bool TryParse(string quantity, out Money? money)
+ {
+ ReadOnlySpan span = quantity.AsSpan();
+ try
{
- decimal amount = decimal.Parse(s, style, currency);
-
- return new Money(amount, currency);
+ if (Enum.TryParse(span[..3], out CurrencyIsoCode currencyCode) &&
+ currencyCode.CheckDefined() &&
+ decimal.TryParse(span[4..], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out decimal amount))
+ {
+ money = new Money(amount, currencyCode);
+ return true;
+ }
}
-
- ///
- /// Converts the string representation of a monetary quantity to its equivalent
- /// using and the provided currency as format information.
- /// A return value indicates whether the conversion succeeded or failed.
- ///
- /// The string representation of the monetary quantity to convert.
- /// Expected currency of that provides format information.
- /// When this method returns, contains the that is equivalent to the monetary quantity contained in ,
- /// if the conversion succeeded, or is null if the conversion failed.
- /// The conversion fails if the parameter is null, is not in a format compliant with currency style,
- /// or represents a number less than or greater than .
- /// This parameter is passed uninitialized.
- /// true if s was converted successfully; otherwise, false.
- ///
- [Pure]
- public static bool TryParse(string s, Currency currency, out Money? money)
+#pragma warning disable CA1031
+ catch
{
- return TryParse(s, NumberStyles.Currency, currency, out money);
+ // span indexing operation can throw
}
+#pragma warning restore CA1031
- ///
- /// Converts the string representation of a monetary quantity to its equivalent
- /// using the specified style and the provided currency as format information.
- /// A return value indicates whether the conversion succeeded or failed.
- ///
- /// The string representation of the monetary quantity to convert.
- /// A bitwise combination of enumeration values that indicates the permitted format of .
- /// A typical value to specify is .
- /// Expected currency of that provides format information.
- /// When this method returns, contains the that is equivalent to the monetary quantity contained in ,
- /// if the conversion succeeded, or is null if the conversion failed.
- /// The conversion fails if the parameter is null, is not in a format compliant with currency style,
- /// or represents a number less than or greater than .
- /// This parameter is passed uninitialized.
- /// true if s was converted successfully; otherwise, false.
- /// is not a value
- /// -or-
- /// is the value.
- ///
- /// is null.
- ///
- [Pure]
- public static bool TryParse(string s, NumberStyles style, Currency currency, out Money? money)
- {
- ArgumentNullException.ThrowIfNull(currency, nameof(currency));
- money = tryParse(s, style, currency.FormatInfo, currency.IsoCode);
- return money.HasValue;
- }
+ money = null;
+ return false;
+ }
- ///
- /// Converts the string representation of a monetary quantity to its equivalent
- /// using the specified style, the and the symbol of the provided
- /// as format information.
- /// A return value indicates whether the conversion succeeded or failed.
- ///
- /// The string representation of the monetary quantity to convert.
- /// A bitwise combination of enumeration values that indicates the permitted format of .
- /// A typical value to specify is .
- ///
- /// The format info for the textual representation of the currency. This may be different from
- /// how the currency would normally be represented in the country that uses it. For example, the
- /// for en-GB would
- /// allow the string "€10,000.00" for , even though this would normally be written as
- /// "10.000,00 €"
- ///
- /// Resultant currency of (only symbol is used for formatting).
- ///
- /// When this method returns, contains the that is equivalent to the monetary quantity
- /// contained in ,
- /// if the conversion succeeded, or is null if the conversion failed.
- /// The conversion fails if the parameter is null, is not in a format compliant with currency
- /// style,
- /// or represents a number less than or greater than
- /// .
- /// This parameter is passed uninitialized.
- ///
- /// true if s was converted successfully; otherwise, false.
- /// or is null.
- [Pure]
- public static bool TryParse(string s, NumberStyles style, NumberFormatInfo numberFormatInfo, Currency currency, out Money? money)
- {
- ArgumentNullException.ThrowIfNull(numberFormatInfo, nameof(numberFormatInfo));
- ArgumentNullException.ThrowIfNull(currency, nameof(currency));
+ ///
+ /// Converts the string representation of a monetary quantity to its equivalent
+ /// using the style and the specified currency as format information.
+ ///
+ /// This method assumes to have a style.
+ /// The string representation of the monetary quantity to convert.
+ /// Expected currency of that provides format information.
+ /// The equivalent to the monetary quantity contained in as specified by the .
+ /// is not in the correct format.
+ /// represents a monetary quantity less than or greater than .
+ /// is null.
+ ///
+ [Pure]
+ public static Money Parse(string s, Currency currency)
+ {
+ return Parse(s, NumberStyles.Currency, currency);
+ }
- var mergedNumberFormatInfo = (NumberFormatInfo)numberFormatInfo.Clone();
- mergedNumberFormatInfo.CurrencySymbol = currency.Symbol;
+ ///
+ /// Converts the string representation of a monetary quantity to its equivalent
+ /// using the specified style and the specified currency as format information.
+ ///
+ /// Use this method when is suspected not to have a style or more control over the operation is needed.
+ /// The string representation of the monetary quantity to convert.
+ /// A bitwise combination of values that indicates the style elements that can be present in .
+ /// A typical value to specify is .
+ /// Expected currency of that provides format information.
+ /// The equivalent to the monetary quantity contained in as specified by and .
+ /// is not in the correct format.
+ /// represents a monetary quantity less than or greater than .
+ /// is null.
+ /// is not a value
+ /// -or-
+ /// is the value.
+ ///
+ ///
+ [Pure]
+ public static Money Parse(string s, NumberStyles style, Currency currency)
+ {
+ decimal amount = decimal.Parse(s, style, currency);
- money = tryParse(s, style, mergedNumberFormatInfo, currency.IsoCode);
+ return new Money(amount, currency);
+ }
- return money.HasValue;
- }
+ ///
+ /// Converts the string representation of a monetary quantity to its equivalent
+ /// using and the provided currency as format information.
+ /// A return value indicates whether the conversion succeeded or failed.
+ ///
+ /// The string representation of the monetary quantity to convert.
+ /// Expected currency of that provides format information.
+ /// When this method returns, contains the that is equivalent to the monetary quantity contained in ,
+ /// if the conversion succeeded, or is null if the conversion failed.
+ /// The conversion fails if the parameter is null, is not in a format compliant with currency style,
+ /// or represents a number less than or greater than .
+ /// This parameter is passed uninitialized.
+ /// true if s was converted successfully; otherwise, false.
+ ///
+ [Pure]
+ public static bool TryParse(string s, Currency currency, out Money? money)
+ {
+ return TryParse(s, NumberStyles.Currency, currency, out money);
+ }
- private static Money? tryParse(string s, NumberStyles style, NumberFormatInfo formatProvider, CurrencyIsoCode currency)
- {
- decimal amount;
- bool result = decimal.TryParse(s, style, formatProvider, out amount);
- return result ? new Money(amount, currency) : null;
- }
+ ///
+ /// Converts the string representation of a monetary quantity to its equivalent
+ /// using the specified style and the provided currency as format information.
+ /// A return value indicates whether the conversion succeeded or failed.
+ ///
+ /// The string representation of the monetary quantity to convert.
+ /// A bitwise combination of enumeration values that indicates the permitted format of .
+ /// A typical value to specify is .
+ /// Expected currency of that provides format information.
+ /// When this method returns, contains the that is equivalent to the monetary quantity contained in ,
+ /// if the conversion succeeded, or is null if the conversion failed.
+ /// The conversion fails if the parameter is null, is not in a format compliant with currency style,
+ /// or represents a number less than or greater than .
+ /// This parameter is passed uninitialized.
+ /// true if s was converted successfully; otherwise, false.
+ /// is not a value
+ /// -or-
+ /// is the value.
+ ///
+ /// is null.
+ ///
+ [Pure]
+ public static bool TryParse(string s, NumberStyles style, Currency currency, out Money? money)
+ {
+ ArgumentNullException.ThrowIfNull(currency, nameof(currency));
+ money = tryParse(s, style, currency.FormatInfo, currency.IsoCode);
+ return money.HasValue;
+ }
+
+ ///
+ /// Converts the string representation of a monetary quantity to its equivalent
+ /// using the specified style, the and the symbol of the provided
+ /// as format information.
+ /// A return value indicates whether the conversion succeeded or failed.
+ ///
+ /// The string representation of the monetary quantity to convert.
+ /// A bitwise combination of enumeration values that indicates the permitted format of .
+ /// A typical value to specify is .
+ ///
+ /// The format info for the textual representation of the currency. This may be different from
+ /// how the currency would normally be represented in the country that uses it. For example, the
+ /// for en-GB would
+ /// allow the string "€10,000.00" for , even though this would normally be written as
+ /// "10.000,00 €"
+ ///
+ /// Resultant currency of (only symbol is used for formatting).
+ ///
+ /// When this method returns, contains the that is equivalent to the monetary quantity
+ /// contained in ,
+ /// if the conversion succeeded, or is null if the conversion failed.
+ /// The conversion fails if the parameter is null, is not in a format compliant with currency
+ /// style,
+ /// or represents a number less than or greater than
+ /// .
+ /// This parameter is passed uninitialized.
+ ///
+ /// true if s was converted successfully; otherwise, false.
+ /// or is null.
+ [Pure]
+ public static bool TryParse(string s, NumberStyles style, NumberFormatInfo numberFormatInfo, Currency currency,
+ out Money? money)
+ {
+ ArgumentNullException.ThrowIfNull(numberFormatInfo, nameof(numberFormatInfo));
+ ArgumentNullException.ThrowIfNull(currency, nameof(currency));
+
+ var mergedNumberFormatInfo = (NumberFormatInfo)numberFormatInfo.Clone();
+ mergedNumberFormatInfo.CurrencySymbol = currency.Symbol;
+
+ money = tryParse(s, style, mergedNumberFormatInfo, currency.IsoCode);
+
+ return money.HasValue;
+ }
+
+ private static Money? tryParse(string s, NumberStyles style, NumberFormatInfo formatProvider,
+ CurrencyIsoCode currency)
+ {
+ decimal amount;
+ bool result = decimal.TryParse(s, style, formatProvider, out amount);
+ return result ? new Money(amount, currency) : null;
+ }
}
diff --git a/src/NMoneys/NMoneys.csproj b/src/NMoneys/NMoneys.csproj
index 21514b2..3befd19 100644
--- a/src/NMoneys/NMoneys.csproj
+++ b/src/NMoneys/NMoneys.csproj
@@ -31,10 +31,10 @@
- 7.0.0
- 7.0.0.0
- 7.0.0.0
- 7.0.0
+ 7.1.0
+ 7.1.0.0
+ 7.1.0.0
+ 7.1.0
diff --git a/tests/NMoneys.Serialization.Tests/BSON/MoneySerializerTester.cs b/tests/NMoneys.Serialization.Tests/BSON/MoneySerializerTester.cs
new file mode 100644
index 0000000..0e32c53
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/BSON/MoneySerializerTester.cs
@@ -0,0 +1,160 @@
+using MongoDB.Bson;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+
+using NMoneys.Serialization.BSON;
+using NMoneys.Serialization.Tests.Support;
+using Testing.Commons.Serialization;
+
+namespace NMoneys.Serialization.Tests.BSON;
+
+[TestFixture]
+public class MoneySerializerTester
+{
+
+ # region serialization
+
+ [Test]
+ public void Serialize_DefaultConvention_PascalCasedAndNumericCurrency()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneySerializer();
+ string json = toSerialize.ToJson(serializer: subject).Compact();
+
+ Assert.That(json, Is.EqualTo("{'Amount':NumberDecimal('14.3'),'Currency':963}".Jsonify()));
+ }
+
+ #endregion
+
+ #region deserialization
+
+ [Test]
+ public void Deserialize_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'Amount':NumberDecimal('14.3'),'Currency':963}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneySerializer();
+ Money deserialized = subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(json)));
+
+ Assert.That(deserialized, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneySerializer();
+ var json = original.ToJson(serializer: subject);
+ Money deserialized = subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(json)));
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ #endregion
+
+ #region error handling
+
+ [Test]
+ public void Deserialize_NotAnObject_Exception()
+ {
+ string notAJsonObject = "'str'".Jsonify();
+ var subject = new MoneySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(notAJsonObject))),
+ Throws.InstanceOf()
+ .With.Message.Contains("CurrentBsonType is Document")
+ .And.Message.Contains("CurrentBsonType is String"));
+ }
+
+ [Test]
+ public void Deserialize_MissingAmount_Exception()
+ {
+ string missingAmount = "{}".Jsonify();
+ var subject = new MoneySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(missingAmount))),
+ Throws.InstanceOf()
+ .With.Message.Contains("when State is Name")
+ .And.Message.Contains("not when State is EndOfDocument"));
+ }
+
+ [Test]
+ public void Deserialize_NonNumericAmount_Exception()
+ {
+ string nonNumericAmount = "{'Amount':[]}".Jsonify();
+ var subject = new MoneySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(nonNumericAmount))),
+ Throws.InstanceOf()
+ .With.Message.Contains("Cannot extract a monetary amount out of")
+ .And.Message.Contains("'Array'"));
+ }
+
+ [Test]
+ public void Deserialize_CaseDoesNotMatch_Exception()
+ {
+ string caseMismatch = "{'amount':'1','currency':963}".Jsonify();
+ var subject = new MoneySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(caseMismatch))),
+ Throws.InstanceOf()
+ .With.Message.Contains("element name to be 'Amount'")
+ .And.Message.Contains("not 'amount'"));
+ }
+
+ [Test]
+ public void Deserialize_MissingCurrency_Exception()
+ {
+ string missingCurrency = "{'Amount':'1','other':1}".Jsonify();
+ var subject = new MoneySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(missingCurrency))),
+ Throws.InstanceOf()
+ .With.Message.Contains("element name to be 'Currency'"));
+ }
+
+ [Test]
+ public void Deserialize_MismatchCurrencyFormat_DoesNotMatter()
+ {
+ string currencyFormatMismatch = "{'Amount':'2','Currency':'XTS'}".Jsonify();
+ var subject = new MoneySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(currencyFormatMismatch))),
+ Throws.Nothing);
+ }
+
+ [Test]
+ public void Deserialize_FunkyCurrencyValueCasing_DoesNotMatter()
+ {
+ string funkyCurrencyValue = "{'Amount':'1','Currency':'XtS'}".Jsonify();
+ var subject = new MoneySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(funkyCurrencyValue))),
+ Throws.Nothing);
+ }
+
+ [Test]
+ public void Deserialize_UndefinedCurrency_Exception()
+ {
+ string undefinedCurrency = "{'Amount':'1','Currency':1}".Jsonify();
+ var subject = new MoneySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(undefinedCurrency))),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_PoorlyConstructedJson_Exception()
+ {
+ string missingObjectClose = "{'Amount':'1','Currency':1".Jsonify();
+ var subject = new MoneySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(missingObjectClose))),
+ Throws.InstanceOf());
+ }
+
+ #endregion
+}
diff --git a/tests/NMoneys.Serialization.Tests/BSON/MoneySerializerWithCustomConventionsTester.cs b/tests/NMoneys.Serialization.Tests/BSON/MoneySerializerWithCustomConventionsTester.cs
new file mode 100644
index 0000000..2164ec7
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/BSON/MoneySerializerWithCustomConventionsTester.cs
@@ -0,0 +1,162 @@
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Conventions;
+using NMoneys.Serialization.BSON;
+using NMoneys.Serialization.Tests.Support;
+using Testing.Commons.Serialization;
+
+namespace NMoneys.Serialization.Tests.BSON;
+
+[TestFixture, Explicit("cannot change conventions mid-test :-(")]
+public class MoneySerializerWithCustomConventionsTester
+{
+ [OneTimeSetUp]
+ public void RegisterSerializer()
+ {
+ var subject = new MoneySerializer();
+ BsonSerializer.RegisterSerializer(subject);
+ ConventionPack custom = new()
+ {
+ new CamelCaseElementNameConvention(),
+ new EnumRepresentationConvention(BsonType.String)
+ };
+ ConventionRegistry.Register(nameof(custom), custom, _ => true);
+ }
+
+ #region serialize
+
+ [Test]
+ public void Serialize_CustomPolicyAndEnumFormat_FollowsCaseAndCurrencyFormat()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ string json = toSerialize.ToJson().Compact();
+
+ Assert.That(json, Is.EqualTo("{'amount':NumberDecimal('14.3'),'currency':'XTS'}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_Null_DefaultNullable()
+ {
+ Money? @null = default(Money?);
+
+ string json = @null.ToJson().Compact();
+ Assert.That(json, Is.EqualTo("null"));
+ }
+
+ [Test]
+ public void Serialize_Record_FollowsConfiguration()
+ {
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var toSerialize = new MoneyRecord("s", money, 42);
+
+ string json = toSerialize.ToJson().Compact();
+
+ Assert.That(json, Is.EqualTo("{'s':'s','m':{'amount':NumberDecimal('14.3'),'currency':'XTS'},'n':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_Container_FollowsConfiguration()
+ {
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var toSerialize = new MoneyContainer
+ {
+ S = "s",
+ M = money,
+ N = 42
+ };
+
+ string json = toSerialize.ToJson().Compact();
+
+ Assert.That(json, Is.EqualTo("{'s':'s','m':{'amount':NumberDecimal('14.3'),'currency':'XTS'},'n':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_NullableContainer_FollowsConfiguration()
+ {
+ var toSerialize = new NullableMoneyContainer
+ {
+ N = 42
+ };
+
+ string json = toSerialize.ToJson().Compact();
+
+ Assert.That(json, Is.EqualTo("{'s':null,'m':null,'n':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_NullableRecord_FollowsConfiguration()
+ {
+ var toSerialize = new NullableMoneyRecord(null, null, 42);
+
+
+ string json = toSerialize.ToJson().Compact();
+
+ Assert.That(json, Is.EqualTo("{'s':null,'m':null,'n':42}".Jsonify()));
+ }
+
+ #endregion
+
+ #region deserialize
+
+ [Test]
+ public void Deserialize_CustomPolicyAndEnumFormat_FollowsCaseAndCurrencyFormat()
+ {
+ string json = "{'amount':NumberDecimal('14.3'),'currency':'xts'}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ Money actual = BsonSerializer.Deserialize(json);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomPolicyAndEnumFormat_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ string json = original.ToJson().Compact();
+
+ var deserialized = BsonSerializer.Deserialize(json);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_Record()
+ {
+ string json = "{'s':'s','m':{'amount':NumberDecimal('14.3'),'currency':'XTS'},'n':42}".Jsonify();
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var expected = new MoneyRecord("s", money, 42);
+
+ MoneyRecord actual = BsonSerializer.Deserialize(json);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_Container()
+ {
+ string json = "{'s':'s','m':{'amount':NumberDecimal('14.3'),'currency':'XTS'},'n':42}".Jsonify();
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var actual = BsonSerializer.Deserialize(json);
+
+ Assert.That(actual, Has.Property("S").EqualTo("s"));
+ Assert.That(actual, Has.Property("M").EqualTo(money));
+ Assert.That(actual, Has.Property("N").EqualTo(42));
+ }
+
+ [Test]
+ public void Deserialize_NullableRecord()
+ {
+ string json = "{'s':null,'m':null,'n':42}".Jsonify();
+ var expected = new NullableMoneyRecord(null, null, 42);
+
+ var actual = BsonSerializer.Deserialize(json);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ #endregion
+}
diff --git a/tests/NMoneys.Serialization.Tests/BSON/QuantitySerializerTester.cs b/tests/NMoneys.Serialization.Tests/BSON/QuantitySerializerTester.cs
new file mode 100644
index 0000000..93780c2
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/BSON/QuantitySerializerTester.cs
@@ -0,0 +1,131 @@
+using MongoDB.Bson;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+
+using NMoneys.Serialization.BSON;
+using NMoneys.Serialization.Tests.Support;
+using Testing.Commons.Serialization;
+
+namespace NMoneys.Serialization.Tests.BSON;
+
+[TestFixture]
+public class QuantitySerializerTester
+{
+
+ # region serialization
+
+ [Test]
+ public void Serialize_DefaultConvention_QuantityString()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantitySerializer();
+ string json = toSerialize.ToJson(serializer: subject);
+
+ Assert.That(json, Is.EqualTo("'XTS 14.3'".Jsonify()));
+ }
+
+ #endregion
+
+ #region deserialization
+
+ [Test]
+ public void Deserialize_QuantityString_PropsSet()
+ {
+ string json = "'XTS 14.3'".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantitySerializer();
+ Money deserialized = subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(json)));
+
+ Assert.That(deserialized, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantitySerializer();
+ var json = original.ToJson(serializer: subject);
+ Money deserialized = subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(json)));
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ #endregion
+
+ #region error handling
+
+ [Test]
+ public void Deserialize_MissingAmount_Exception()
+ {
+ string missingAmount = "'XTS'".Jsonify();
+ var subject = new QuantitySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(missingAmount))),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_NonNumericAmount_Exception()
+ {
+ string nonNumericAmount = "'XTS lol'".Jsonify();
+ var subject = new QuantitySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(nonNumericAmount))),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_MissingCurrency_Exception()
+ {
+ string missingCurrency = "'42'".Jsonify();
+ var subject = new QuantitySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(missingCurrency))),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_MismatchCurrencyFormat_DoesNotMatter()
+ {
+ string currencyFormatMismatch = "'963 42'".Jsonify();
+ var subject = new QuantitySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(currencyFormatMismatch))),
+ Throws.Nothing);
+ }
+
+ [Test]
+ public void Deserialize_FunkyCurrencyValueCasing_Exception()
+ {
+ string funkyCurrencyValue = "'XtS 42'".Jsonify();
+ var subject = new QuantitySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(funkyCurrencyValue))),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_UndefinedCurrency_Exception()
+ {
+ string undefinedCurrency = "'LOL 42'}".Jsonify();
+ var subject = new QuantitySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(undefinedCurrency))),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_PoorlyConstructedJson_Exception()
+ {
+ string missingObjectClose = "'XTS 42".Jsonify();
+ var subject = new QuantitySerializer();
+
+ Assert.That(()=> subject.Deserialize(BsonDeserializationContext.CreateRoot(new JsonReader(missingObjectClose))),
+ Throws.InstanceOf());
+ }
+
+ #endregion
+}
diff --git a/tests/NMoneys.Serialization.Tests/BSON/QuantityTester.cs b/tests/NMoneys.Serialization.Tests/BSON/QuantityTester.cs
new file mode 100644
index 0000000..6c9fdb5
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/BSON/QuantityTester.cs
@@ -0,0 +1,57 @@
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization;
+using NMoneys.Extensions;
+using NMoneys.Serialization.Tests.Support;
+using Testing.Commons.Serialization;
+
+namespace NMoneys.Serialization.Tests.BSON;
+
+[TestFixture]
+public class QuantityTester
+{
+ [Test]
+ public void CustomSerializeDto_NoNeedOfCustomSerializer()
+ {
+ var dto = new Dto { S = "str", M = new MonetaryQuantity(50m, CurrencyIsoCode.EUR) };
+ string json = dto.ToJson().Compact();
+ Assert.That(json, Is.EqualTo("{'S':'str','M':{'Amount':'50','Currency':978}}".Jsonify()));
+ }
+
+ [Test]
+ public void CustomSerializeDtr_NoNeedOfCustomSerializer()
+ {
+ var dtr = new Dtr("str", new MonetaryQuantity(50m, CurrencyIsoCode.EUR));
+ string json = dtr.ToJson().Compact();
+ Assert.That(json, Is.EqualTo("{'S':'str','M':{'Amount':'50','Currency':978}}".Jsonify()));
+ }
+
+ [Test]
+ public void CustomDeserializeDto_NoNeedOfCustomSerializer()
+ {
+ string json = "{'S':'str','M':{'Amount':'50','Currency':978}}";
+
+ Dto dto = BsonSerializer.Deserialize(json);
+
+ Assert.That(dto, Has.Property("S").EqualTo("str"));
+ Assert.That(dto?.M, Has.Property("Amount").EqualTo(50m).And
+ .Property("Currency").EqualTo(CurrencyIsoCode.EUR));
+
+ // convert to money for operations
+ Money m = (Money)dto!.M;
+ Assert.That(m, Is.EqualTo(50m.Eur()));
+ }
+
+ [Test]
+ public void CustomDeserializeDtr_NoNeedOfCustomSerializer()
+ {
+ string json = "{'S':'str','M':{'Amount':'50','Currency':978}}";
+ Dtr? dtr = BsonSerializer.Deserialize(json);
+ Assert.That(dtr, Has.Property("S").EqualTo("str"));
+ Assert.That(dtr?.M, Has.Property("Amount").EqualTo(50m).And
+ .Property("Currency").EqualTo(CurrencyIsoCode.EUR));
+
+ // convert to money for operations
+ Money m = (Money)dtr!.M;
+ Assert.That(m, Is.EqualTo(50m.Eur()));
+ }
+}
diff --git a/tests/NMoneys.Serialization.Tests/EFCore/JsonConverterTester.cs b/tests/NMoneys.Serialization.Tests/EFCore/JsonConverterTester.cs
new file mode 100644
index 0000000..b15197c
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/EFCore/JsonConverterTester.cs
@@ -0,0 +1,122 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.EntityFrameworkCore;
+using NMoneys.Serialization.EFCore;
+using NMoneys.Serialization.Tests.EFCore.Support;
+using NMoneys.Serialization.Tests.Support;
+using NMoneys.Serialization.Text_Json;
+using Testing.Commons.Serialization;
+
+using JsonConverter = NMoneys.Serialization.EFCore.JsonConverter;
+
+namespace NMoneys.Serialization.Tests.EFCore;
+
+[TestFixture]
+public class JsonConverterTester
+{
+ private FileInfo? _dbFile;
+
+ private JsonSerializerOptions JsonOptions { get; } = new()
+ {
+ Converters = { new MoneyConverter(), new JsonStringEnumConverter() },
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ [OneTimeSetUp]
+ public void SetupDb()
+ {
+ _dbFile = new FileInfo("JsonPersistence.sdf");
+ }
+
+ [OneTimeTearDown]
+ public void TearDownDb()
+ {
+ _dbFile?.Delete();
+ }
+
+ [Test]
+ public void SaveEntity_SavesAsQuantityValue()
+ {
+ using var context = new JsonDbContext(_dbFile!, new JsonConverter(JsonOptions), new MoneyComparer());
+ context.Database.EnsureCreated();
+
+ var m = new Money(43.75m, CurrencyIsoCode.XTS);
+ var id = new Guid(new string('1', 32));
+ var model = new MoneyModel
+ {
+ Id = id,
+ M = m,
+ N = 42
+ };
+ context.Models.Add(model);
+ context.SaveChanges();
+
+ FormattableString sql = $"SELECT M FROM Models WHERE ID = {id.ToString()}";
+ var queried = context.Database.SqlQuery(sql)
+ .ToArray()
+ .Single();
+ Assert.That(queried, Is.EqualTo("{'amount':43.75,'currency':'XTS'}".Jsonify()));
+ }
+
+ [Test]
+ public void SaveEntity_AsQuantity_CanRoundtrip()
+ {
+ using var context = new JsonDbContext(_dbFile!, new JsonConverter(JsonOptions), new MoneyComparer());
+ context.Database.EnsureCreated();
+
+ var m = new Money(43.75m, CurrencyIsoCode.XTS);
+ var id = new Guid(new string('2', 32));
+ var model = new MoneyModel
+ {
+ Id = id,
+ M = m,
+ N = 42
+ };
+ context.Models.Add(model);
+ context.SaveChanges();
+
+ MoneyModel queried = context.Models.Single(mm => mm.Id == id);
+ Assert.That(queried.M, Is.EqualTo(m));
+ }
+
+ [Test]
+ public void SaveNullableEntity_SavesAsNull()
+ {
+ using var context = new JsonDbContext(_dbFile!, new JsonConverter(JsonOptions), new MoneyComparer());
+ context.Database.EnsureCreated();
+
+ var id = new Guid(new string('3', 32));
+ var model = new NullableMoneyModel
+ {
+ Id = id,
+ N = 42
+ };
+ context.NullableModels.Add(model);
+ context.SaveChanges();
+
+ FormattableString sql = $"SELECT M FROM NullableModels WHERE Id = {id.ToString()}";
+ var queried = context.Database.SqlQuery(sql)
+ .ToArray()
+ .Single();
+ Assert.That(queried, Is.Null);
+ }
+
+ [Test]
+ public void SaveNullableEntity_AsQuantity_CanRoundtrip()
+ {
+ using var context = new JsonDbContext(_dbFile!, new JsonConverter(JsonOptions), new MoneyComparer());
+ context.Database.EnsureCreated();
+
+ var id = new Guid(new string('4', 32));
+ var model = new NullableMoneyModel
+ {
+ Id = id,
+ N = 42
+ };
+ context.NullableModels.Add(model);
+ context.SaveChanges();
+
+ NullableMoneyModel queried = context.NullableModels.Single(mm => mm.Id == id);
+ Assert.That(queried.M, Is.Null);
+ }
+}
diff --git a/tests/NMoneys.Serialization.Tests/EFCore/QuantityConverterTester.cs b/tests/NMoneys.Serialization.Tests/EFCore/QuantityConverterTester.cs
new file mode 100644
index 0000000..bc1f850
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/EFCore/QuantityConverterTester.cs
@@ -0,0 +1,109 @@
+using Microsoft.EntityFrameworkCore;
+using NMoneys.Serialization.EFCore;
+using NMoneys.Serialization.Tests.EFCore.Support;
+using NMoneys.Serialization.Tests.Support;
+
+namespace NMoneys.Serialization.Tests.EFCore;
+
+[TestFixture]
+public class QuantityConverterTester
+{
+ private FileInfo? _dbFile;
+ [OneTimeSetUp]
+ public void SetupDb()
+ {
+ _dbFile = new FileInfo("QuantityPersistence.sdf");
+ }
+
+ [OneTimeTearDown]
+ public void TearDownDb()
+ {
+ _dbFile?.Delete();
+ }
+
+ [Test]
+ public void SaveEntity_SavesAsQuantityValue()
+ {
+ using var context = new QuantityDbContext(_dbFile!, new QuantityConverter(), new MoneyComparer());
+ context.Database.EnsureCreated();
+
+ var m = new Money(43.75m, CurrencyIsoCode.XTS);
+ var id = new Guid(new string('1', 32));
+ var model = new MoneyModel
+ {
+ Id = id,
+ M = m,
+ N = 42
+ };
+ context.Models.Add(model);
+ context.SaveChanges();
+
+ FormattableString sql = $"SELECT M FROM Models WHERE Id = {id.ToString()}";
+ var queried = context.Database.SqlQuery(sql)
+ .ToArray()
+ .Single();
+ Assert.That(queried, Is.EqualTo("XTS 43.75"));
+ }
+
+ [Test]
+ public void SaveEntity_AsQuantity_CanRoundtrip()
+ {
+ using var context = new QuantityDbContext(_dbFile!, new QuantityConverter(), new MoneyComparer());
+ context.Database.EnsureCreated();
+
+ var m = new Money(43.75m, CurrencyIsoCode.XTS);
+ var id = new Guid(new string('2', 32));
+ var model = new MoneyModel
+ {
+ Id = id,
+ M = m,
+ N = 42
+ };
+ context.Models.Add(model);
+ context.SaveChanges();
+
+ MoneyModel queried = context.Models.Single(mm => mm.Id == id);
+ Assert.That(queried.M, Is.EqualTo(m));
+ }
+
+ [Test]
+ public void SaveNullableEntity_SavesAsNull()
+ {
+ using var context = new QuantityDbContext(_dbFile!, new QuantityConverter(), new MoneyComparer());
+ context.Database.EnsureCreated();
+
+ var id = new Guid(new string('3', 32));
+ var model = new NullableMoneyModel
+ {
+ Id = id,
+ N = 42
+ };
+ context.NullableModels.Add(model);
+ context.SaveChanges();
+
+ FormattableString sql = $"SELECT M FROM NullableModels WHERE Id = {id.ToString()}";
+ var queried = context.Database.SqlQuery(sql)
+ .ToArray()
+ .Single();
+ Assert.That(queried, Is.Null);
+ }
+
+ [Test]
+ public void SaveNullableEntity_AsQuantity_CanRoundtrip()
+ {
+ using var context = new QuantityDbContext(_dbFile!, new QuantityConverter(), new MoneyComparer());
+ context.Database.EnsureCreated();
+
+ var id = new Guid(new string('4', 32));
+ var model = new NullableMoneyModel
+ {
+ Id = id,
+ N = 42
+ };
+ context.NullableModels.Add(model);
+ context.SaveChanges();
+
+ NullableMoneyModel queried = context.NullableModels.Single(mm => mm.Id == id);
+ Assert.That(queried.M, Is.Null);
+ }
+}
diff --git a/tests/NMoneys.Serialization.Tests/EFCore/Support/JsonDbContext.cs b/tests/NMoneys.Serialization.Tests/EFCore/Support/JsonDbContext.cs
new file mode 100644
index 0000000..70dd151
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/EFCore/Support/JsonDbContext.cs
@@ -0,0 +1,43 @@
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using NMoneys.Serialization.EFCore;
+using NMoneys.Serialization.Tests.Support;
+
+namespace NMoneys.Serialization.Tests.EFCore.Support;
+
+public class JsonDbContext : DbContext
+{
+ private readonly ValueComparer _comparer;
+ private readonly string _connectionString;
+ private readonly ValueConverter _converter;
+ public JsonDbContext(FileInfo dbFile, JsonConverter converter, ValueComparer comparer)
+ {
+ _comparer = comparer;
+ _converter = converter;
+ var builder = new SqliteConnectionStringBuilder
+ {
+ DataSource = dbFile.Name
+ };
+ _connectionString = builder.ConnectionString;
+ }
+ public DbSet Models { get; set; }
+ public DbSet NullableModels { get; set; }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder options)
+ {
+ base.OnConfiguring(options);
+
+ options.UseSqlite(_connectionString);
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ builder.Entity().HasKey(e => e.Id);
+ builder.Entity().HasKey(e => e.Id);
+ builder.Entity().Property(e => e.M).HasConversion(_converter, _comparer);
+ builder.Entity().Property(e => e.M).HasConversion(_converter, _comparer);
+ base.OnModelCreating(builder);
+ }
+}
diff --git a/tests/NMoneys.Serialization.Tests/EFCore/Support/QuantityDbContext.cs b/tests/NMoneys.Serialization.Tests/EFCore/Support/QuantityDbContext.cs
new file mode 100644
index 0000000..684ce2f
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/EFCore/Support/QuantityDbContext.cs
@@ -0,0 +1,44 @@
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using NMoneys.Serialization.EFCore;
+using NMoneys.Serialization.Tests.Support;
+
+namespace NMoneys.Serialization.Tests.EFCore.Support;
+
+public class QuantityDbContext : DbContext
+{
+ private readonly ValueComparer _comparer;
+ private readonly string _connectionString;
+ private readonly ValueConverter _converter;
+ public QuantityDbContext(FileInfo dbFile, QuantityConverter converter, ValueComparer comparer)
+ {
+ _comparer = comparer;
+ _converter = converter;
+ var builder = new SqliteConnectionStringBuilder
+ {
+ DataSource = dbFile.Name
+ };
+ _connectionString = builder.ConnectionString;
+ }
+ public DbSet Models { get; set; }
+ public DbSet NullableModels { get; set; }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder options)
+ {
+ base.OnConfiguring(options);
+
+ options.UseSqlite(_connectionString);
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ builder.Entity().HasKey(e => e.Id);
+ builder.Entity().HasKey(e => e.Id);
+ builder.Entity().Property(e => e.M).HasConversion(_converter, _comparer);
+ builder.Entity().Property(e => e.M).HasConversion(_converter, _comparer);
+ base.OnModelCreating(builder);
+ }
+
+}
diff --git a/tests/NMoneys.Serialization.Tests/Json_NET/DescriptiveMoneyConverterTester.cs b/tests/NMoneys.Serialization.Tests/Json_NET/DescriptiveMoneyConverterTester.cs
new file mode 100644
index 0000000..4ed57c0
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Json_NET/DescriptiveMoneyConverterTester.cs
@@ -0,0 +1,9 @@
+namespace NMoneys.Serialization.Tests.Json_NET;
+
+// TODO: lots of tests for errors
+
+[TestFixture]
+public class DescriptiveMoneyConverterTester
+{
+
+}
diff --git a/tests/NMoneys.Serialization.Tests/Json_NET/MoneyConverterTester.cs b/tests/NMoneys.Serialization.Tests/Json_NET/MoneyConverterTester.cs
new file mode 100644
index 0000000..da351a3
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Json_NET/MoneyConverterTester.cs
@@ -0,0 +1,476 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Serialization;
+using NMoneys.Extensions;
+using Testing.Commons.Serialization;
+
+using NMoneys.Serialization.Tests.Support;
+using NMoneys.Serialization.Json_NET;
+
+namespace NMoneys.Serialization.Tests.Json_NET;
+
+[TestFixture]
+public class MoneyConverterTester
+{
+ [Test, Category("exploratory")]
+ public void OutOfTheBoxSerialization_BlowsUp()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ Assert.That(()=> JsonConvert.SerializeObject(toSerialize), Throws.InstanceOf());
+ }
+
+ # region serialization
+
+ [Test]
+ public void Serialize_DefaultResolver_PascalCasedAndNumericCurrency()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ string actual = JsonConvert.SerializeObject(toSerialize, subject);
+
+ Assert.That(actual, Is.EqualTo("{'Amount':14.3,'Currency':963}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_DefaultResolverWithForcedStringCurrency_PascalCasedAndStringCurrency()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ string actual = JsonConvert.SerializeObject(toSerialize, subject);
+
+ Assert.That(actual, Is.EqualTo("{'Amount':14.3,'Currency':'XTS'}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomResolverAndEnumFormat_FollowsCaseAndCurrencyFormat()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ string actual = JsonConvert.SerializeObject(toSerialize, settings);
+
+ Assert.That(actual, Is.EqualTo("{'amount':14.3,'currency':'XTS'}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomResolverAndForcedString_FollowsCaseAndOverridesCurrencyFormat()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { subject }
+ };
+ string actual = JsonConvert.SerializeObject(toSerialize, settings);
+
+ Assert.That(actual, Is.EqualTo("{'amount':14.3,'currency':'XTS'}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_Null_DefaultNullable()
+ {
+ Money? @null = default(Money?);
+
+ var subject = new MoneyConverter();
+
+ string actual = JsonConvert.SerializeObject(@null, subject);
+ Assert.That(actual, Is.EqualTo("null"));
+ }
+
+ [Test]
+ public void Serialize_DefaultResolverRecord_PascalCasedAndNumericCurrency()
+ {
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var toSerialize = new MoneyRecord("s", money, 42);
+
+ var subject = new MoneyConverter();
+ string actual = JsonConvert.SerializeObject(toSerialize, subject);
+
+ Assert.That(actual, Is.EqualTo("{'S':'s','M':{'Amount':14.3,'Currency':963},'N':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomContainer_FollowsConfiguration()
+ {
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var toSerialize = new MoneyContainer
+ {
+ S = "s",
+ M = money,
+ N = 42
+ };
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ string actual = JsonConvert.SerializeObject(toSerialize, settings);
+
+ Assert.That(actual, Is.EqualTo("{'s':'s','m':{'amount':14.3,'currency':'XTS'},'n':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_DefaultResolverNullableContainer_PascalCasedAndNumericCurrency()
+ {
+ var toSerialize = new NullableMoneyContainer
+ {
+ N = 42
+ };
+
+ var subject = new MoneyConverter();
+ string actual = JsonConvert.SerializeObject(toSerialize, subject);
+
+ Assert.That(actual, Is.EqualTo("{'S':null,'M':null,'N':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomNullableRecord_FollowsConfiguration()
+ {
+ var toSerialize = new NullableMoneyRecord(null, null, 42);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ string actual = JsonConvert.SerializeObject(toSerialize, settings);
+
+ Assert.That(actual, Is.EqualTo("{'s':null,'m':null,'n':42}".Jsonify()));
+ }
+
+ #endregion
+
+ #region deserialization
+
+ [Test]
+ public void Deserialize_DefaultResolver_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'Amount':14.3,'Currency':963}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ Money deserialized = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(deserialized, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_DefaultResolver_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ string json = JsonConvert.SerializeObject(original, subject);
+ var deserialized = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_DefaultResolverWithForcedStringCurrency_PascalCasedAndStringCurrency()
+ {
+ string json = "{'Amount':14.3,'Currency':'XTS'}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ Money deserialized = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(deserialized, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_DefaultResolverWithForcedStringCurrency_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ string json = JsonConvert.SerializeObject(original, subject);
+ var deserialized = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_CustomResolverAndEnumFormat_FollowsCaseAndCurrencyFormat()
+ {
+ string json = "{'amount':14.3,'currency':'XTS'}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ var actual = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomResolverAndEnumFormat_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ string json = JsonConvert.SerializeObject(original, settings);
+ var deserialized = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_CustomResolverAndForcedString_FollowsCaseAndOverridesCurrencyFormat()
+ {
+ string json = "{'amount':14.3,'currency':'XTS'}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { subject }
+ };
+ var actual = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomResolverAndForcedString_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { subject }
+ };
+ var json = JsonConvert.SerializeObject(original, settings);
+ var deserialized = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_Null_DefaultNullable()
+ {
+ var subject = new MoneyConverter();
+
+ var actual = JsonConvert.DeserializeObject("null", subject);
+ Assert.That(actual, Is.Null);
+ }
+
+ [Test]
+ public void Deserialize_DefaultResolverRecord_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'S':'s','M':{'Amount':14.3,'Currency':963},'N':42}".Jsonify();
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var expected = new MoneyRecord("s", money, 42);
+
+ var subject = new MoneyConverter();
+ MoneyRecord? actual = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomContainer_FollowsConfiguration()
+ {
+ string json = "{'s':'s','m':{'amount':14.3,'currency':'XTS'},'n':42}".Jsonify();
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ var actual = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(actual, Has.Property("S").EqualTo("s"));
+ Assert.That(actual, Has.Property("M").EqualTo(money));
+ Assert.That(actual, Has.Property("N").EqualTo(42));
+
+ }
+
+ [Test]
+ public void Deserialize_DefaultResolverNullableContainer_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'S':null,'M':null,'N':42}".Jsonify();
+
+ var subject = new MoneyConverter();
+ var actual = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(actual, Has.Property("S").Null);
+ Assert.That(actual, Has.Property("M").Null);
+ Assert.That(actual, Has.Property("N").EqualTo(42));
+ }
+
+ [Test]
+ public void Deserialize_CustomNullableRecord_FollowsConfiguration()
+ {
+ string json = "{'s':null,'m':null,'n':42}".Jsonify();
+ var expected = new NullableMoneyRecord(null, null, 42);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ var actual = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ #region error handling
+
+ [Test]
+ public void Deserialize_DefaultNotAnObject_Exception()
+ {
+ string notAJsonObject = "'str'".Jsonify();
+ var subject = new MoneyConverter();
+
+ Assert.That(()=> JsonConvert.DeserializeObject(notAJsonObject, subject),
+ Throws.InstanceOf()
+ .With.Message.Contains("token 'Object'")
+ .And.Message.Contains("'String'"));
+ }
+
+ [Test]
+ public void Deserialize_DefaultMissingAmount_Exception()
+ {
+ string missingAmount = "{}".Jsonify();
+ var subject = new MoneyConverter();
+
+ Assert.That(()=> JsonConvert.DeserializeObject(missingAmount, subject),
+ Throws.InstanceOf()
+ .With.Message.Contains("Missing property")
+ .And.Message.Contains("'Amount'"));
+ }
+
+ [Test]
+ public void Deserialize_DefaultNonNumericAmount_Exception()
+ {
+ string nonNumericAmount = "{'Amount':'lol'}".Jsonify();
+ var subject = new MoneyConverter();
+
+ Assert.That(()=> JsonConvert.DeserializeObject(nonNumericAmount, subject),
+ Throws.InstanceOf()
+ .With.Message.Contains("input string 'lol'"));
+ }
+
+ [Test]
+ public void Deserialize_CaseDoesNotMatchContract_DoesntMatter()
+ {
+ string caseMismatch = "{'Amount':1,'currency':963}".Jsonify();
+ var subject = new MoneyConverter();
+
+ Assert.That(()=> JsonConvert.DeserializeObject(caseMismatch, subject),
+ Throws.Nothing);
+ var deserialized = JsonConvert.DeserializeObject(caseMismatch, subject);
+ Assert.That(deserialized, Is.EqualTo(1m.Xts()));
+ }
+
+ [Test]
+ public void Deserialize_MissingCurrency_ExceptionIgnoringCasing()
+ {
+ string missingCurrency = "{'amount':1}".Jsonify();
+ var subject = new MoneyConverter();
+
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonConvert.DeserializeObject(missingCurrency, settings),
+ Throws.InstanceOf()
+ .With.Message.Contains("Missing property")
+ .And.Message.Contains("'Currency'"), "no camel-casing");
+ }
+
+ [Test]
+ public void Deserialize_MismatchCurrencyFormat_DoesNotMatter()
+ {
+ string currencyFormatMismatch = "{'amount':1,'currency':'XTS'}".Jsonify();
+ var subject = new MoneyConverter();
+
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonConvert.DeserializeObject(currencyFormatMismatch, settings),
+ Throws.Nothing);
+
+ Money deserialized = JsonConvert.DeserializeObject(currencyFormatMismatch, settings);
+ Assert.That(deserialized, Is.EqualTo(1m.Xts()));
+ }
+
+ [Test]
+ public void Deserialize_FunkyCurrencyValueCasing_DoesNotMatter()
+ {
+ string funkyCurrencyValue = "{'amount':1,'currency':'XtS'}".Jsonify();
+ var subject = new MoneyConverter();
+
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonConvert.DeserializeObject(funkyCurrencyValue, settings),
+ Throws.Nothing);
+
+ Money deserialized = JsonConvert.DeserializeObject(funkyCurrencyValue, settings);
+ Assert.That(deserialized, Is.EqualTo(1m.Xts()));
+ }
+
+ [Test]
+ public void Deserialize_UndefinedCurrency_Exception()
+ {
+ string undefinedCurrency = "{'amount':1,'currency':1}".Jsonify();
+ var subject = new MoneyConverter();
+
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonConvert.DeserializeObject(undefinedCurrency, settings),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_PoorlyConstructedJson_Exception()
+ {
+ string missingObjectClose = "{'amount':1,'currency':1".Jsonify();
+ var subject = new MoneyConverter();
+
+ Assert.That(()=> JsonConvert.DeserializeObject(missingObjectClose, subject),
+ Throws.InstanceOf());
+ }
+
+ #endregion
+
+ #endregion
+}
diff --git a/tests/NMoneys.Serialization.Tests/Json_NET/QuantityConverterTester.cs b/tests/NMoneys.Serialization.Tests/Json_NET/QuantityConverterTester.cs
new file mode 100644
index 0000000..323571f
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Json_NET/QuantityConverterTester.cs
@@ -0,0 +1,468 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Serialization;
+using NMoneys.Extensions;
+using Testing.Commons.Serialization;
+
+using NMoneys.Serialization.Tests.Support;
+using NMoneys.Serialization.Json_NET;
+
+namespace NMoneys.Serialization.Tests.Json_NET;
+
+[TestFixture]
+public class QuantityConverterTester
+{
+ # region serialization
+
+ [Test]
+ public void Serialize_DefaultResolver_PascalCasedAndNumericCurrency()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantityConverter();
+ string actual = JsonConvert.SerializeObject(toSerialize, subject);
+
+ Assert.That(actual, Is.EqualTo("{'Amount':14.3,'Currency':963}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_DefaultResolverWithForcedStringCurrency_PascalCasedAndStringCurrency()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ string actual = JsonConvert.SerializeObject(toSerialize, subject);
+
+ Assert.That(actual, Is.EqualTo("{'Amount':14.3,'Currency':'XTS'}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomResolverAndEnumFormat_FollowsCaseAndCurrencyFormat()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ string actual = JsonConvert.SerializeObject(toSerialize, settings);
+
+ Assert.That(actual, Is.EqualTo("{'amount':14.3,'currency':'XTS'}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomResolverAndForcedString_FollowsCaseAndOverridesCurrencyFormat()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { subject }
+ };
+ string actual = JsonConvert.SerializeObject(toSerialize, settings);
+
+ Assert.That(actual, Is.EqualTo("{'amount':14.3,'currency':'XTS'}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_Null_DefaultNullable()
+ {
+ Money? @null = default(Money?);
+
+ var subject = new MoneyConverter();
+
+ string actual = JsonConvert.SerializeObject(@null, subject);
+ Assert.That(actual, Is.EqualTo("null"));
+ }
+
+ [Test]
+ public void Serialize_DefaultResolverRecord_PascalCasedAndNumericCurrency()
+ {
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var toSerialize = new MoneyRecord("s", money, 42);
+
+ var subject = new MoneyConverter();
+ string actual = JsonConvert.SerializeObject(toSerialize, subject);
+
+ Assert.That(actual, Is.EqualTo("{'S':'s','M':{'Amount':14.3,'Currency':963},'N':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomContainer_FollowsConfiguration()
+ {
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var toSerialize = new MoneyContainer
+ {
+ S = "s",
+ M = money,
+ N = 42
+ };
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ string actual = JsonConvert.SerializeObject(toSerialize, settings);
+
+ Assert.That(actual, Is.EqualTo("{'s':'s','m':{'amount':14.3,'currency':'XTS'},'n':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_DefaultResolverNullableContainer_PascalCasedAndNumericCurrency()
+ {
+ var toSerialize = new NullableMoneyContainer
+ {
+ N = 42
+ };
+
+ var subject = new MoneyConverter();
+ string actual = JsonConvert.SerializeObject(toSerialize, subject);
+
+ Assert.That(actual, Is.EqualTo("{'S':null,'M':null,'N':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomNullableRecord_FollowsConfiguration()
+ {
+ var toSerialize = new NullableMoneyRecord(null, null, 42);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ string actual = JsonConvert.SerializeObject(toSerialize, settings);
+
+ Assert.That(actual, Is.EqualTo("{'s':null,'m':null,'n':42}".Jsonify()));
+ }
+
+ #endregion
+
+ #region deserialization
+
+ [Test]
+ public void Deserialize_DefaultResolver_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'Amount':14.3,'Currency':963}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ Money deserialized = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(deserialized, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_DefaultResolver_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ string json = JsonConvert.SerializeObject(original, subject);
+ var deserialized = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_DefaultResolverWithForcedStringCurrency_PascalCasedAndStringCurrency()
+ {
+ string json = "{'Amount':14.3,'Currency':'XTS'}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ Money deserialized = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(deserialized, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_DefaultResolverWithForcedStringCurrency_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ string json = JsonConvert.SerializeObject(original, subject);
+ var deserialized = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_CustomResolverAndEnumFormat_FollowsCaseAndCurrencyFormat()
+ {
+ string json = "{'amount':14.3,'currency':'XTS'}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ var actual = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomResolverAndEnumFormat_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ string json = JsonConvert.SerializeObject(original, settings);
+ var deserialized = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_CustomResolverAndForcedString_FollowsCaseAndOverridesCurrencyFormat()
+ {
+ string json = "{'amount':14.3,'currency':'XTS'}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { subject }
+ };
+ var actual = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomResolverAndForcedString_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter(true);
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { subject }
+ };
+ var json = JsonConvert.SerializeObject(original, settings);
+ var deserialized = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_Null_DefaultNullable()
+ {
+ var subject = new MoneyConverter();
+
+ var actual = JsonConvert.DeserializeObject("null", subject);
+ Assert.That(actual, Is.Null);
+ }
+
+ [Test]
+ public void Deserialize_DefaultResolverRecord_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'S':'s','M':{'Amount':14.3,'Currency':963},'N':42}".Jsonify();
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var expected = new MoneyRecord("s", money, 42);
+
+ var subject = new MoneyConverter();
+ MoneyRecord? actual = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomContainer_FollowsConfiguration()
+ {
+ string json = "{'s':'s','m':{'amount':14.3,'currency':'XTS'},'n':42}".Jsonify();
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ var actual = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(actual, Has.Property("S").EqualTo("s"));
+ Assert.That(actual, Has.Property("M").EqualTo(money));
+ Assert.That(actual, Has.Property("N").EqualTo(42));
+
+ }
+
+ [Test]
+ public void Deserialize_DefaultResolverNullableContainer_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'S':null,'M':null,'N':42}".Jsonify();
+
+ var subject = new MoneyConverter();
+ var actual = JsonConvert.DeserializeObject(json, subject);
+
+ Assert.That(actual, Has.Property("S").Null);
+ Assert.That(actual, Has.Property("M").Null);
+ Assert.That(actual, Has.Property("N").EqualTo(42));
+ }
+
+ [Test]
+ public void Deserialize_CustomNullableRecord_FollowsConfiguration()
+ {
+ string json = "{'s':null,'m':null,'n':42}".Jsonify();
+ var expected = new NullableMoneyRecord(null, null, 42);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = new JsonConverter[] { new StringEnumConverter(), subject }
+ };
+ var actual = JsonConvert.DeserializeObject(json, settings);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ #region error handling
+
+ [Test]
+ public void Deserialize_DefaultNotAnObject_Exception()
+ {
+ string notAJsonObject = "'str'".Jsonify();
+ var subject = new MoneyConverter();
+
+ Assert.That(()=> JsonConvert.DeserializeObject(notAJsonObject, subject),
+ Throws.InstanceOf()
+ .With.Message.Contains("token 'Object'")
+ .And.Message.Contains("'String'"));
+ }
+
+ [Test]
+ public void Deserialize_DefaultMissingAmount_Exception()
+ {
+ string missingAmount = "{}".Jsonify();
+ var subject = new MoneyConverter();
+
+ Assert.That(()=> JsonConvert.DeserializeObject(missingAmount, subject),
+ Throws.InstanceOf()
+ .With.Message.Contains("Missing property")
+ .And.Message.Contains("'Amount'"));
+ }
+
+ [Test]
+ public void Deserialize_DefaultNonNumericAmount_Exception()
+ {
+ string nonNumericAmount = "{'Amount':'lol'}".Jsonify();
+ var subject = new MoneyConverter();
+
+ Assert.That(()=> JsonConvert.DeserializeObject(nonNumericAmount, subject),
+ Throws.InstanceOf()
+ .With.Message.Contains("input string 'lol'"));
+ }
+
+ [Test]
+ public void Deserialize_CaseDoesNotMatchContract_DoesntMatter()
+ {
+ string caseMismatch = "{'Amount':1,'currency':963}".Jsonify();
+ var subject = new MoneyConverter();
+
+ Assert.That(()=> JsonConvert.DeserializeObject(caseMismatch, subject),
+ Throws.Nothing);
+ var deserialized = JsonConvert.DeserializeObject(caseMismatch, subject);
+ Assert.That(deserialized, Is.EqualTo(1m.Xts()));
+ }
+
+ [Test]
+ public void Deserialize_MissingCurrency_ExceptionIgnoringCasing()
+ {
+ string missingCurrency = "{'amount':1}".Jsonify();
+ var subject = new MoneyConverter();
+
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonConvert.DeserializeObject(missingCurrency, settings),
+ Throws.InstanceOf()
+ .With.Message.Contains("Missing property")
+ .And.Message.Contains("'Currency'"), "no camel-casing");
+ }
+
+ [Test]
+ public void Deserialize_MismatchCurrencyFormat_DoesNotMatter()
+ {
+ string currencyFormatMismatch = "{'amount':1,'currency':'XTS'}".Jsonify();
+ var subject = new MoneyConverter();
+
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonConvert.DeserializeObject(currencyFormatMismatch, settings),
+ Throws.Nothing);
+
+ Money deserialized = JsonConvert.DeserializeObject(currencyFormatMismatch, settings);
+ Assert.That(deserialized, Is.EqualTo(1m.Xts()));
+ }
+
+ [Test]
+ public void Deserialize_FunkyCurrencyValueCasing_DoesNotMatter()
+ {
+ string funkyCurrencyValue = "{'amount':1,'currency':'XtS'}".Jsonify();
+ var subject = new MoneyConverter();
+
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonConvert.DeserializeObject(funkyCurrencyValue, settings),
+ Throws.Nothing);
+
+ Money deserialized = JsonConvert.DeserializeObject(funkyCurrencyValue, settings);
+ Assert.That(deserialized, Is.EqualTo(1m.Xts()));
+ }
+
+ [Test]
+ public void Deserialize_UndefinedCurrency_Exception()
+ {
+ string undefinedCurrency = "{'amount':1,'currency':1}".Jsonify();
+ var subject = new MoneyConverter();
+
+ var settings = new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonConvert.DeserializeObject(undefinedCurrency, settings),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_PoorlyConstructedJson_Exception()
+ {
+ string missingObjectClose = "{'amount':1,'currency':1".Jsonify();
+ var subject = new MoneyConverter();
+
+ Assert.That(()=> JsonConvert.DeserializeObject(missingObjectClose, subject),
+ Throws.InstanceOf());
+ }
+
+ #endregion
+
+ #endregion
+}
diff --git a/tests/NMoneys.Serialization.Tests/Json_NET/QuantityTester.cs b/tests/NMoneys.Serialization.Tests/Json_NET/QuantityTester.cs
new file mode 100644
index 0000000..872b481
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Json_NET/QuantityTester.cs
@@ -0,0 +1,72 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Serialization;
+using NMoneys.Extensions;
+using NMoneys.Serialization.Tests.Support;
+using Testing.Commons.Serialization;
+
+namespace NMoneys.Serialization.Tests.Json_NET;
+
+[TestFixture]
+public class QuantityTester
+{
+ [Test]
+ public void CustomSerializeDto_NoNeedOfCustomSerializer()
+ {
+ var dto = new Dto { S = "str", M = new MonetaryQuantity(50m, CurrencyIsoCode.EUR) };
+ string json = JsonConvert.SerializeObject(dto, new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = {new StringEnumConverter() }
+ });
+ Assert.That(json, Is.EqualTo("{'s':'str','m':{'amount':50.0,'currency':'EUR'}}".Jsonify()));
+ }
+
+ [Test]
+ public void CustomSerializeDtr_NoNeedOfCustomSerializer()
+ {
+ var dtr = new Dtr("str", new MonetaryQuantity(50m, CurrencyIsoCode.EUR));
+ string json = JsonConvert.SerializeObject(dtr, new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = {new StringEnumConverter() }
+ });
+ Assert.That(json, Is.EqualTo("{'s':'str','m':{'amount':50.0,'currency':'EUR'}}".Jsonify()));
+ }
+
+ [Test]
+ public void CustomDeserializeDto_NoNeedOfCustomSerializer()
+ {
+ string json = "{'s':'str','m':{'amount':50,'currency':'EUR'}}";
+ Dto? dto = JsonConvert.DeserializeObject(json, new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = {new StringEnumConverter() }
+ });
+ Assert.That(dto, Has.Property("S").EqualTo("str"));
+ Assert.That(dto?.M, Has.Property("Amount").EqualTo(50m).And
+ .Property("Currency").EqualTo(CurrencyIsoCode.EUR));
+
+ // convert to money for operations
+ Money m = (Money)dto!.M;
+ Assert.That(m, Is.EqualTo(50m.Eur()));
+ }
+
+ [Test]
+ public void CustomDeserializeDtr_NoNeedOfCustomSerializer()
+ {
+ string json = "{'s':'str','m':{'amount':50,'currency':'EUR'}}";
+ Dtr? dtr = JsonConvert.DeserializeObject(json, new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
+ Converters = {new StringEnumConverter() }
+ });
+ Assert.That(dtr, Has.Property("S").EqualTo("str"));
+ Assert.That(dtr?.M, Has.Property("Amount").EqualTo(50m).And
+ .Property("Currency").EqualTo(CurrencyIsoCode.EUR));
+
+ // convert to money for operations
+ Money m = (Money)dtr!.M;
+ Assert.That(m, Is.EqualTo(50m.Eur()));
+ }
+}
diff --git a/tests/NMoneys.Serialization.Tests/NMoneys.Serialization.Tests.csproj b/tests/NMoneys.Serialization.Tests/NMoneys.Serialization.Tests.csproj
new file mode 100644
index 0000000..cc922bb
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/NMoneys.Serialization.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/NMoneys.Serialization.Tests/Support/TestExtensions.cs b/tests/NMoneys.Serialization.Tests/Support/TestExtensions.cs
new file mode 100644
index 0000000..82e6ee6
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Support/TestExtensions.cs
@@ -0,0 +1,15 @@
+namespace NMoneys.Serialization.Tests.Support;
+
+internal static class TestExtensions
+{
+ ///
+ /// "Compacts" the resulting JSON from Bson serialization.
+ ///
+ /// Removes spaces from a non-indented JSON.
+ ///
+ /// JSON without spaces.
+ public static string Compact(this string json)
+ {
+ return json.Replace(" ", string.Empty);
+ }
+}
diff --git a/tests/NMoneys.Serialization.Tests/Support/money_containers.cs b/tests/NMoneys.Serialization.Tests/Support/money_containers.cs
new file mode 100644
index 0000000..6c51e2d
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Support/money_containers.cs
@@ -0,0 +1,25 @@
+using MongoDB.Bson;
+
+namespace NMoneys.Serialization.Tests.Support;
+
+public record MoneyRecord(string S, Money M, int N);
+
+public record MoneyRecordDoc(ObjectId Id, string S, Money M, int N);
+
+public class MoneyContainer
+{
+ public string? S { get; set; }
+ public Money M { get; set; }
+
+ public int N { get; set; }
+}
+
+public class MoneyDoc : MoneyContainer
+{
+ public ObjectId Id { get; set; }
+}
+
+public class MoneyModel : MoneyContainer
+{
+ public Guid Id { get; set; }
+}
diff --git a/tests/NMoneys.Serialization.Tests/Support/nullable_money_containers.cs b/tests/NMoneys.Serialization.Tests/Support/nullable_money_containers.cs
new file mode 100644
index 0000000..74701f9
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Support/nullable_money_containers.cs
@@ -0,0 +1,25 @@
+using MongoDB.Bson;
+
+namespace NMoneys.Serialization.Tests.Support;
+
+public record NullableMoneyRecord(string? S, Money? M, int N);
+
+public record NullableMoneyRecordDoc(ObjectId Id, string? S, Money? M, int N);
+
+public class NullableMoneyContainer
+{
+ public string? S { get; set; }
+ public Money? M { get; set; }
+
+ public int N { get; set; }
+}
+
+public class NullableMoneyDoc : NullableMoneyContainer
+{
+ public ObjectId Id { get; set; }
+}
+
+public class NullableMoneyModel : NullableMoneyContainer
+{
+ public Guid Id { get; set; }
+}
diff --git a/tests/NMoneys.Serialization.Tests/Support/quantity_containers.cs b/tests/NMoneys.Serialization.Tests/Support/quantity_containers.cs
new file mode 100644
index 0000000..6d2c152
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Support/quantity_containers.cs
@@ -0,0 +1,12 @@
+// ReSharper disable PropertyCanBeMadeInitOnly.Global
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+namespace NMoneys.Serialization.Tests.Support;
+
+public class Dto
+{
+
+ public string S { get; set; }
+ public MonetaryQuantity M { get; set; }
+}
+
+public record Dtr(string S, MonetaryQuantity M);
diff --git a/tests/NMoneys.Serialization.Tests/Text_Json/MoneyConverterTester.cs b/tests/NMoneys.Serialization.Tests/Text_Json/MoneyConverterTester.cs
new file mode 100644
index 0000000..6ebd46b
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Text_Json/MoneyConverterTester.cs
@@ -0,0 +1,405 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using NMoneys.Extensions;
+using NMoneys.Serialization.Tests.Support;
+using NMoneys.Serialization.Text_Json;
+using Testing.Commons.Serialization;
+
+namespace NMoneys.Serialization.Tests.Text_Json;
+
+[TestFixture]
+public class MoneyConverterTester
+{
+ [Test, Category("exploratory")]
+ public void OutOfTheBoxSerialization_BlowsUp()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ Assert.That(() => JsonSerializer.Serialize(toSerialize), Throws.InstanceOf());
+ }
+
+ # region serialization
+
+ [Test]
+ public void Serialize_DefaultPolicy_PascalCasedAndNumericCurrency()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions
+ {
+ Converters = { subject }
+ };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("{'Amount':14.3,'Currency':963}".Jsonify()));
+ }
+
+
+ [Test]
+ public void Serialize_CustomPolicyAndEnumFormat_FollowsCaseAndCurrencyFormat()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), subject }
+ };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("{'amount':14.3,'currency':'xts'}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_Null_DefaultNullable()
+ {
+ Money? @null = default(Money?);
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ string actual = JsonSerializer.Serialize(@null, options);
+ Assert.That(actual, Is.EqualTo("null"));
+ }
+
+ [Test]
+ public void Serialize_DefaultOptionsRecord_PascalCasedAndNumericCurrency()
+ {
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var toSerialize = new MoneyRecord("s", money, 42);
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("{'S':'s','M':{'Amount':14.3,'Currency':963},'N':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomContainer_FollowsConfiguration()
+ {
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var toSerialize = new MoneyContainer
+ {
+ S = "s",
+ M = money,
+ N = 42
+ };
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(), subject }
+ };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("{'s':'s','m':{'amount':14.3,'currency':'XTS'},'n':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_DefaultOptionsNullableContainer_PascalCasedAndNumericCurrency()
+ {
+ var toSerialize = new NullableMoneyContainer
+ {
+ N = 42
+ };
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("{'S':null,'M':null,'N':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomNullableRecord_FollowsConfiguration()
+ {
+ var toSerialize = new NullableMoneyRecord(null, null, 42);
+ ;
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(), subject }
+ };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("{'s':null,'m':null,'n':42}".Jsonify()));
+ }
+
+ #endregion
+
+ #region deserialization
+
+ [Test]
+ public void Deserialize_DefaultOptions_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'Amount':14.3,'Currency':963}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ Money deserialized = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(deserialized, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_DefaultOptions_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ string json = JsonSerializer.Serialize(original, options);
+ var deserialized = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_CustomPolicyAndEnumFormat_FollowsCaseAndCurrencyFormat()
+ {
+ string json = "{'amount':14.3,'currency':'xts'}".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), subject }
+ };
+ var actual = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomPolicyAndEnumFormat_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), subject }
+ };
+ string json = JsonSerializer.Serialize(original, options);
+ var deserialized = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+
+ [Test]
+ public void Deserialize_Null_DefaultNullable()
+ {
+ var subject = new MoneyConverter();
+
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ var actual = JsonSerializer.Deserialize("null", options);
+ Assert.That(actual, Is.Null);
+ }
+
+ [Test]
+ public void Deserialize_DefaultOptionsRecord_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'S':'s','M':{'Amount':14.3,'Currency':963},'N':42}".Jsonify();
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var expected = new MoneyRecord("s", money, 42);
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ MoneyRecord? actual = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomContainer_FollowsConfiguration()
+ {
+ string json = "{'s':'s','m':{'amount':14.3,'currency':'XTS'},'n':42}".Jsonify();
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(), subject }
+ };
+ var actual = JsonSerializer.Deserialize(json, settings);
+
+ Assert.That(actual, Has.Property("S").EqualTo("s"));
+ Assert.That(actual, Has.Property("M").EqualTo(money));
+ Assert.That(actual, Has.Property("N").EqualTo(42));
+ }
+
+ [Test]
+ public void Deserialize_DefaultOptionsNullableContainer_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'S':null,'M':null,'N':42}".Jsonify();
+
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ var actual = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(actual, Has.Property("S").Null);
+ Assert.That(actual, Has.Property("M").Null);
+ Assert.That(actual, Has.Property("N").EqualTo(42));
+ }
+
+ [Test]
+ public void Deserialize_CustomNullableRecord_FollowsConfiguration()
+ {
+ string json = "{'s':null,'m':null,'n':42}".Jsonify();
+ var expected = new NullableMoneyRecord(null, null, 42);
+
+ var subject = new MoneyConverter();
+ var settings = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters ={ new JsonStringEnumConverter(), subject }
+ };
+ var actual = JsonSerializer.Deserialize(json, settings);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ #region error handling
+
+ [Test]
+ public void Deserialize_DefaultNotAnObject_Exception()
+ {
+ string notAJsonObject = "'str'".Jsonify();
+ var subject = new MoneyConverter();
+
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ Assert.That(()=> JsonSerializer.Deserialize(notAJsonObject, options),
+ Throws.InstanceOf()
+ .With.Message.Contains("must be of type")
+ .And.Message.Contains("'JsonObject'"));
+ }
+
+ [Test]
+ public void Deserialize_DefaultMissingAmount_Exception()
+ {
+ string missingAmount = "{}".Jsonify();
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+
+ Assert.That(()=> JsonSerializer.Deserialize(missingAmount, options),
+ Throws.InstanceOf()
+ .With.Message.Contains("Missing property")
+ .And.Message.Contains("'Amount'"));
+ }
+
+ [Test]
+ public void Deserialize_DefaultNonNumericAmount_Exception()
+ {
+ string nonNumericAmount = "{'Amount':'lol'}".Jsonify();
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+
+ Assert.That(()=> JsonSerializer.Deserialize(nonNumericAmount, options),
+ Throws.InstanceOf()
+ .With.Message.Contains("cannot be converted")
+ .And.Message.Contains("'String'")
+ .And.Message.Contains("'System.Decimal'"));
+ }
+
+ [Test]
+ public void Deserialize_CaseDoesNotMatchContract_DoesntMatter()
+ {
+ string caseMismatch = "{'Amount':1,'currency':963}".Jsonify();
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+
+ Assert.That(()=> JsonSerializer.Deserialize(caseMismatch, options),
+ Throws.Nothing);
+ var deserialized = JsonSerializer.Deserialize(caseMismatch, options);
+ Assert.That(deserialized, Is.EqualTo(1m.Xts()));
+ }
+
+ [Test]
+ public void Deserialize_MissingCurrency_ExceptionIgnoringCasing()
+ {
+ string missingCurrency = "{'amount':1}".Jsonify();
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonSerializer.Deserialize(missingCurrency, options),
+ Throws.InstanceOf()
+ .With.Message.Contains("Missing property")
+ .And.Message.Contains("'Currency'"), "no camel-casing");
+ }
+
+ [Test]
+ public void Deserialize_MismatchCurrencyFormat_Exception()
+ {
+ string currencyFormatMismatch = "{'amount':1,'currency':'XTS'}".Jsonify();
+ var subject = new MoneyConverter();
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonSerializer.Deserialize(currencyFormatMismatch, options),
+ Throws.InstanceOf()
+ .With.Message.Contains("could not be converted to")
+ .And.Message.Contains("NMoneys.CurrencyIsoCode"));
+ }
+
+ [Test]
+ public void Deserialize_FunkyCurrencyValueCasing_Exception()
+ {
+ string funkyCurrencyValue = "{'amount':1,'currency':'XtS'}".Jsonify();
+ var subject = new MoneyConverter();
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonSerializer.Deserialize(funkyCurrencyValue, options),
+ Throws.InstanceOf()
+ .With.Message.Contains("could not be converted to")
+ .And.Message.Contains("NMoneys.CurrencyIsoCode"));
+ }
+
+ [Test]
+ public void Deserialize_UndefinedCurrency_Exception()
+ {
+ string undefinedCurrency = "{'amount':1,'currency':1}".Jsonify();
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonSerializer.Deserialize(undefinedCurrency, options),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_PoorlyConstructedJson_Exception()
+ {
+ string missingObjectClose = "{'amount':1,'currency':1".Jsonify();
+ var subject = new MoneyConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+
+ Assert.That(()=> JsonSerializer.Deserialize(missingObjectClose, options),
+ Throws.InstanceOf());
+ }
+
+ #endregion
+
+ #endregion
+}
diff --git a/tests/NMoneys.Serialization.Tests/Text_Json/QuantityConverterTester.cs b/tests/NMoneys.Serialization.Tests/Text_Json/QuantityConverterTester.cs
new file mode 100644
index 0000000..5a31f5d
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Text_Json/QuantityConverterTester.cs
@@ -0,0 +1,388 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using NMoneys.Extensions;
+using NMoneys.Serialization.Tests.Support;
+using NMoneys.Serialization.Text_Json;
+using Testing.Commons.Serialization;
+
+namespace NMoneys.Serialization.Tests.Text_Json;
+
+[TestFixture]
+public class QuantityConverterTester
+{
+ # region serialization
+
+ [Test]
+ public void Serialize_DefaultPolicy_QuantityString()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions
+ {
+ Converters = { subject }
+ };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("'XTS 14.3'".Jsonify()));
+ }
+
+
+ [Test]
+ public void Serialize_CustomPolicyAndEnumFormat_QuantityString()
+ {
+ var toSerialize = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), subject }
+ };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("'XTS 14.3'".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_Null_DefaultNullable()
+ {
+ Money? @null = default(Money?);
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ string actual = JsonSerializer.Serialize(@null, options);
+ Assert.That(actual, Is.EqualTo("null"));
+ }
+
+ [Test]
+ public void Serialize_DefaultOptionsRecord_QuantityString()
+ {
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var toSerialize = new MoneyRecord("s", money, 42);
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("{'S':'s','M':'XTS 14.3','N':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomContainer_FollowsConfiguration()
+ {
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var toSerialize = new MoneyContainer
+ {
+ S = "s",
+ M = money,
+ N = 42
+ };
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(), subject }
+ };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("{'s':'s','m':'XTS 14.3','n':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_DefaultOptionsNullableContainer_PascalCasedAndNumericCurrency()
+ {
+ var toSerialize = new NullableMoneyContainer
+ {
+ N = 42
+ };
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("{'S':null,'M':null,'N':42}".Jsonify()));
+ }
+
+ [Test]
+ public void Serialize_CustomNullableRecord_FollowsConfiguration()
+ {
+ var toSerialize = new NullableMoneyRecord(null, null, 42);
+ ;
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(), subject }
+ };
+ string actual = JsonSerializer.Serialize(toSerialize, options);
+
+ Assert.That(actual, Is.EqualTo("{'s':null,'m':null,'n':42}".Jsonify()));
+ }
+
+ #endregion
+
+ #region deserialization
+
+ [Test]
+ public void Deserialize_DefaultOptions_QuantityParsedProps()
+ {
+ string json = "'XTS 14.3'".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ Money deserialized = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(deserialized, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_DefaultOptions_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ string json = JsonSerializer.Serialize(original, options);
+ var deserialized = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+ [Test]
+ public void Deserialize_CustomPolicyAndEnumFormat_QuantityParsedProps()
+ {
+ string json = "'XTS 14.3'".Jsonify();
+ var expected = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), subject }
+ };
+ var actual = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomPolicyAndEnumFormat_CanRoundtrip()
+ {
+ var original = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), subject }
+ };
+ string json = JsonSerializer.Serialize(original, options);
+ var deserialized = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(deserialized, Is.EqualTo(original));
+ }
+
+
+ [Test]
+ public void Deserialize_Null_DefaultNullable()
+ {
+ var subject = new QuantityConverter();
+
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ var actual = JsonSerializer.Deserialize("null", options);
+ Assert.That(actual, Is.Null);
+ }
+
+ [Test]
+ public void Deserialize_DefaultOptionsRecord_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'S':'s','M':'XTS 14.3','N':42}".Jsonify();
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+ var expected = new MoneyRecord("s", money, 42);
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ MoneyRecord? actual = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Deserialize_CustomContainer_FollowsConfiguration()
+ {
+ string json = "{'s':'s','m':'XTS 14.3','n':42}".Jsonify();
+ var money = new Money(14.3m, CurrencyIsoCode.XTS);
+
+ var subject = new QuantityConverter();
+ var settings = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter(), subject }
+ };
+ var actual = JsonSerializer.Deserialize(json, settings);
+
+ Assert.That(actual, Has.Property("S").EqualTo("s"));
+ Assert.That(actual, Has.Property("M").EqualTo(money));
+ Assert.That(actual, Has.Property("N").EqualTo(42));
+ }
+
+ [Test]
+ public void Deserialize_DefaultOptionsNullableContainer_PascalCasedAndNumericCurrency()
+ {
+ string json = "{'S':null,'M':null,'N':42}".Jsonify();
+
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ var actual = JsonSerializer.Deserialize(json, options);
+
+ Assert.That(actual, Has.Property("S").Null);
+ Assert.That(actual, Has.Property("M").Null);
+ Assert.That(actual, Has.Property("N").EqualTo(42));
+ }
+
+ [Test]
+ public void Deserialize_CustomNullableRecord_FollowsConfiguration()
+ {
+ string json = "{'s':null,'m':null,'n':42}".Jsonify();
+ var expected = new NullableMoneyRecord(null, null, 42);
+
+ var subject = new QuantityConverter();
+ var settings = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters ={ new JsonStringEnumConverter(), subject }
+ };
+ var actual = JsonSerializer.Deserialize(json, settings);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ #region error handling
+
+ [Test]
+ public void Deserialize_DefaultNotAString_Exception()
+ {
+ string notAJsonObject = "{}".Jsonify();
+ var subject = new QuantityConverter();
+
+ var options = new JsonSerializerOptions { Converters = { subject } };
+ Assert.That(()=> JsonSerializer.Deserialize(notAJsonObject, options),
+ Throws.InstanceOf()
+ .With.Message.Contains("must be of type")
+ .And.Message.Contains("'JsonValue'"));
+ }
+
+ [Test]
+ public void Deserialize_DefaultMissingAmount_Exception()
+ {
+ string missingAmount = "'XTS'".Jsonify();
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+
+ Assert.That(()=> JsonSerializer.Deserialize(missingAmount, options),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_DefaultNonNumericAmount_Exception()
+ {
+ string nonNumericAmount = "'XTS lol'}".Jsonify();
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+
+ Assert.That(()=> JsonSerializer.Deserialize(nonNumericAmount, options),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_LowercaseCurrency_Exception()
+ {
+ string caseMismatch = "'xts 1'".Jsonify();
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+
+ Assert.That(()=> JsonSerializer.Deserialize( caseMismatch, options),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_MissingCurrency_ExceptionIgnoringCasing()
+ {
+ string missingCurrency = "'1'".Jsonify();
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonSerializer.Deserialize(missingCurrency, options),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_MismatchCurrencyFormat_DoesNotMatter()
+ {
+ string currencyFormatMismatch = "'963 1'".Jsonify();
+ var subject = new QuantityConverter();
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonSerializer.Deserialize(currencyFormatMismatch, options),
+ Throws.Nothing);
+
+ Assert.That(JsonSerializer.Deserialize(currencyFormatMismatch, options), Is.EqualTo(1m.Xts()));
+ }
+
+ [Test]
+ public void Deserialize_FunkyCurrencyValueCasing_Exception()
+ {
+ string funkyCurrencyValue = "'XtS 1'".Jsonify();
+ var subject = new QuantityConverter();
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonSerializer.Deserialize(funkyCurrencyValue, options),
+ Throws.InstanceOf()
+ .With.Message.Contains("'XtS'"));
+ }
+
+ [Test]
+ public void Deserialize_UndefinedCurrency_Exception()
+ {
+ string undefinedCurrency = "'001 1'".Jsonify();
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { subject }
+ };
+ Assert.That(()=> JsonSerializer.Deserialize(undefinedCurrency, options),
+ Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Deserialize_PoorlyConstructedJson_Exception()
+ {
+ string missingObjectClose = "'XTS 1".Jsonify();
+ var subject = new QuantityConverter();
+ var options = new JsonSerializerOptions { Converters = { subject } };
+
+ Assert.That(()=> JsonSerializer.Deserialize(missingObjectClose, options),
+ Throws.InstanceOf());
+ }
+
+ #endregion
+
+ #endregion
+
+}
diff --git a/tests/NMoneys.Serialization.Tests/Text_Json/QuantityTester.cs b/tests/NMoneys.Serialization.Tests/Text_Json/QuantityTester.cs
new file mode 100644
index 0000000..2a8c42d
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Text_Json/QuantityTester.cs
@@ -0,0 +1,72 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using NMoneys.Extensions;
+using NMoneys.Serialization.Tests.Support;
+using Testing.Commons.Serialization;
+
+namespace NMoneys.Serialization.Tests.Text_Json;
+
+[TestFixture]
+public class QuantityTester
+{
+ [Test]
+ public void CustomSerializeDto_NoNeedOfCustomSerializer()
+ {
+ var dto = new Dto { S = "str", M = new MonetaryQuantity(50m, CurrencyIsoCode.EUR) };
+
+ string json = JsonSerializer.Serialize(dto, new JsonSerializerOptions()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter() }
+ });
+ Assert.That(json, Is.EqualTo("{'s':'str','m':{'amount':50,'currency':'EUR'}}".Jsonify()));
+ }
+
+ [Test]
+ public void CustomSerializeDtr_NoNeedOfCustomSerializer()
+ {
+ var dtr = new Dtr("str", new MonetaryQuantity(50m, CurrencyIsoCode.EUR));
+ string json = JsonSerializer.Serialize(dtr, new JsonSerializerOptions()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter() }
+ });
+ Assert.That(json, Is.EqualTo("{'s':'str','m':{'amount':50,'currency':'EUR'}}".Jsonify()));
+ }
+
+ [Test]
+ public void CustomDeserializeDto_NoNeedOfCustomSerializer()
+ {
+ string json = "{'s':'str','m':{'amount':50,'currency':'EUR'}}".Jsonify();
+ Dto? dto = JsonSerializer.Deserialize(json, new JsonSerializerOptions()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter() }
+ });
+ Assert.That(dto, Has.Property("S").EqualTo("str"));
+ Assert.That(dto?.M, Has.Property("Amount").EqualTo(50m).And
+ .Property("Currency").EqualTo(CurrencyIsoCode.EUR));
+
+ // convert to money for operations
+ Money m = (Money)dto!.M;
+ Assert.That(m, Is.EqualTo(50m.Eur()));
+ }
+
+ [Test]
+ public void CustomDeserializeDtr_NoNeedOfCustomSerializer()
+ {
+ string json = "{'s':'str','m':{'amount':50,'currency':'EUR'}}".Jsonify();
+ Dtr? dtr = JsonSerializer.Deserialize(json, new JsonSerializerOptions()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter() }
+ });
+ Assert.That(dtr, Has.Property("S").EqualTo("str"));
+ Assert.That(dtr?.M, Has.Property("Amount").EqualTo(50m).And
+ .Property("Currency").EqualTo(CurrencyIsoCode.EUR));
+
+ // convert to money for operations
+ Money m = (Money)dtr!.M;
+ Assert.That(m, Is.EqualTo(50m.Eur()));
+ }
+}
diff --git a/tests/NMoneys.Serialization.Tests/Usings.cs b/tests/NMoneys.Serialization.Tests/Usings.cs
new file mode 100644
index 0000000..cefced4
--- /dev/null
+++ b/tests/NMoneys.Serialization.Tests/Usings.cs
@@ -0,0 +1 @@
+global using NUnit.Framework;
\ No newline at end of file
diff --git a/tests/NMoneys.Tests/MonetaryQuantityTester.cs b/tests/NMoneys.Tests/MonetaryQuantityTester.cs
new file mode 100644
index 0000000..d4e4e02
--- /dev/null
+++ b/tests/NMoneys.Tests/MonetaryQuantityTester.cs
@@ -0,0 +1,93 @@
+using NMoneys.Extensions;
+
+namespace NMoneys.Tests;
+
+[TestFixture]
+public class MonetaryQuantityTester
+{
+ #region ctors
+
+ [Test]
+ public void DefaultCtor_ZeroAmountAndNoCurrency()
+ {
+ var subject = new MonetaryQuantity();
+
+ Assert.That(subject.Amount, Is.EqualTo(decimal.Zero));
+ Assert.That(subject.Currency, Is.EqualTo(CurrencyIsoCode.XXX));
+ }
+
+ [Test]
+ public void CompleteCtor_SetsAmountAndCurrency()
+ {
+ decimal amount = 42.375m;
+
+ var subject = new MonetaryQuantity(amount, CurrencyIsoCode.XTS);
+
+ Assert.That(subject.Amount, Is.EqualTo(amount));
+ Assert.That(subject.Currency, Is.EqualTo(CurrencyIsoCode.XTS));
+ }
+
+ #endregion
+
+ [Test]
+ public void ToString_CompactMonetaryRepresentation()
+ {
+ var subject = new MonetaryQuantity(-42.375m, CurrencyIsoCode.XTS);
+
+ Assert.That(subject.ToString(), Is.EqualTo("XTS -42.375"));
+ }
+
+ #region conversions
+
+ #region from money
+
+ [Test]
+ public void FromMoneyOperator_SetsAmountAndCurrency()
+ {
+ var subject = (MonetaryQuantity)52.75m.Xts();
+
+ Assert.That(subject.Amount, Is.EqualTo(52.75m));
+ Assert.That(subject.Currency, Is.EqualTo(CurrencyIsoCode.XTS));
+ }
+
+ [Test]
+ public void FromMoney_SetsAmountAndCurrency()
+ {
+ var subject = MonetaryQuantity.FromMoney(52.75m.Xts());
+
+ Assert.That(subject.Amount, Is.EqualTo(52.75m));
+ Assert.That(subject.Currency, Is.EqualTo(CurrencyIsoCode.XTS));
+ }
+
+ #endregion
+
+ #region to money
+
+ [Test]
+ public void ToMoneyOperator_SetsAmountAndCurrency()
+ {
+ var money = (Money)new MonetaryQuantity(52.75m, CurrencyIsoCode.XTS);
+
+ Assert.That(money, Is.EqualTo(52.75m.Xts()));
+ }
+
+ [Test]
+ public void StaticToMoney_SetsAmountAndCurrency()
+ {
+ var subject = new MonetaryQuantity(52.75m, CurrencyIsoCode.XTS);
+
+ Assert.That(MonetaryQuantity.ToMoney(subject), Is.EqualTo(52.75m.Xts()));
+ }
+
+ [Test]
+ public void InstanceToMoney_SetsAmountAndCurrency()
+ {
+ var subject = new MonetaryQuantity(52.75m, CurrencyIsoCode.XTS);
+
+ Assert.That(subject.ToMoney(), Is.EqualTo(52.75m.Xts()));
+ }
+
+ #endregion
+
+ #endregion
+}
diff --git a/tests/NMoneys.Tests/MonetaryRecordTester.cs b/tests/NMoneys.Tests/MonetaryRecordTester.cs
new file mode 100644
index 0000000..ec783c4
--- /dev/null
+++ b/tests/NMoneys.Tests/MonetaryRecordTester.cs
@@ -0,0 +1,92 @@
+using NMoneys.Extensions;
+
+namespace NMoneys.Tests;
+
+[TestFixture]
+public class MonetaryRecordTester
+{
+ #region ctors
+
+ [Test]
+ public void DefaultCtor_ZeroAmountAndNoCurrency()
+ {
+ var subject = new MonetaryRecord();
+
+ Assert.That(subject.Amount, Is.EqualTo(decimal.Zero));
+ Assert.That(subject.Currency, Is.EqualTo(CurrencyIsoCode.XXX));
+ }
+
+ [Test]
+ public void CompleteCtor_SetsAmountAndCurrency()
+ {
+ decimal amount = 42.375m;
+ var subject = new MonetaryRecord(amount, CurrencyIsoCode.XTS);
+
+ Assert.That(subject.Amount, Is.EqualTo(amount));
+ Assert.That(subject.Currency, Is.EqualTo(CurrencyIsoCode.XTS));
+ }
+
+ #endregion
+
+ [Test]
+ public void ToString_CompactMonetaryRepresentation()
+ {
+ var subject = new MonetaryRecord(-42.375m, CurrencyIsoCode.XTS);
+
+ Assert.That(subject.ToString(), Is.EqualTo("XTS -42.375"));
+ }
+
+ #region conversions
+
+ #region from money
+
+ [Test]
+ public void FromMoneyOperator_SetsAmountAndCurrency()
+ {
+ var subject = (MonetaryRecord)52.75m.Xts();
+
+ Assert.That(subject.Amount, Is.EqualTo(52.75m));
+ Assert.That(subject.Currency, Is.EqualTo(CurrencyIsoCode.XTS));
+ }
+
+ [Test]
+ public void FromMoney_SetsAmountAndCurrency()
+ {
+ var subject = MonetaryRecord.FromMoney(52.75m.Xts());
+
+ Assert.That(subject.Amount, Is.EqualTo(52.75m));
+ Assert.That(subject.Currency, Is.EqualTo(CurrencyIsoCode.XTS));
+ }
+
+ #endregion
+
+ #region to money
+
+ [Test]
+ public void ToMoneyOperator_SetsAmountAndCurrency()
+ {
+ var money = (Money)new MonetaryRecord(52.75m, CurrencyIsoCode.XTS);
+
+ Assert.That(money, Is.EqualTo(52.75m.Xts()));
+ }
+
+ [Test]
+ public void StaticToMoney_SetsAmountAndCurrency()
+ {
+ var subject = new MonetaryRecord(52.75m, CurrencyIsoCode.XTS);
+
+ Assert.That(MonetaryRecord.ToMoney(subject), Is.EqualTo(52.75m.Xts()));
+ }
+
+ [Test]
+ public void InstanceToMoney_SetsAmountAndCurrency()
+ {
+ var subject = new MonetaryRecord(52.75m, CurrencyIsoCode.XTS);
+
+ Assert.That(subject.ToMoney(), Is.EqualTo(52.75m.Xts()));
+ }
+
+ #endregion
+
+ #endregion
+}
diff --git a/tests/NMoneys.Tests/MoneyTester.Formatting.cs b/tests/NMoneys.Tests/MoneyTester.Formatting.cs
new file mode 100644
index 0000000..99395a4
--- /dev/null
+++ b/tests/NMoneys.Tests/MoneyTester.Formatting.cs
@@ -0,0 +1,34 @@
+using NMoneys.Extensions;
+
+namespace NMoneys.Tests;
+
+[TestFixture]
+public partial class MoneyTester
+{
+ #region AsQuantity
+
+ [Test]
+ public void AsQuantity_InvariantCurrency_SpaceSeparatedCapitalizedCurrencyCodeAndInvariantAmount()
+ {
+ string quantity = 13.45m.Xts().AsQuantity();
+ Assert.That(quantity, Is.EqualTo("XTS 13.45"));
+ }
+
+ [Test]
+ public void AsQuantity_NonInvariantCurrency_SpaceSeparatedCapitalizedCurrencyCodeAndInvariantAmount()
+ {
+ string quantity = (-13.45m).Eur().AsQuantity();
+ Assert.That(quantity, Is.EqualTo("EUR -13.45"));
+ }
+
+ [Test, TestCaseSource(nameof(quantities))]
+ public void AsQuantity_QuantityWrittenAsPerSpec(string expected, CurrencyIsoCode currency, decimal amount)
+ {
+ var money = new Money(amount, currency);
+ string actual = money.AsQuantity();
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ #endregion
+}
diff --git a/tests/NMoneys.Tests/MoneyTester.Parsing.cs b/tests/NMoneys.Tests/MoneyTester.Parsing.cs
index 06799fa..5be83bd 100644
--- a/tests/NMoneys.Tests/MoneyTester.Parsing.cs
+++ b/tests/NMoneys.Tests/MoneyTester.Parsing.cs
@@ -9,6 +9,63 @@ public partial class MoneyTester
{
#region Parse
+ #region quantity
+
+ private static TestCaseData[] quantities = new[]
+ {
+ new TestCaseData("XTS 50", CurrencyIsoCode.XTS, 50m)
+ .SetName("positive integral"),
+ new TestCaseData("XXX -50", CurrencyIsoCode.XXX, -50m)
+ .SetName("negative integral"),
+ new TestCaseData("GBP 5.75", CurrencyIsoCode.GBP, 5.75m)
+ .SetName("positive fractional"),
+ new TestCaseData("EUR -5.765", CurrencyIsoCode.EUR, -5.765m)
+ .SetName("negative fractional"),
+ new TestCaseData("AUD 1.000000000000000001", CurrencyIsoCode.AUD, 1.000000000000000001m)
+ .SetName("many decimals")
+ };
+
+ [Test, TestCaseSource(nameof(quantities))]
+ public void Parse_Quantity_MoneyCreatedAsPerSpec(string quantity, CurrencyIsoCode currency, decimal amount)
+ {
+ Money parsed = Money.Parse(quantity);
+ Assert.That(parsed, Is.EqualTo(new Money(amount, currency)));
+ }
+
+ [Test]
+ public void Parse_ExponentialQuantity_MoneyParsed()
+ {
+ Money parsed = Money.Parse("USD 1E6");
+ // cannot be written by .AsQuantity(), but can be parse
+ Assert.That(parsed, Is.EqualTo(1000000m.Usd()));
+ }
+
+ [Test]
+ public void Parse_ThousandsQuantity_Exception()
+ {
+ Assert.That(() => Money.Parse("XXX 1,000.1"), Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Parse_TrailingSignQuantity_Exception()
+ {
+ Assert.That(() => Money.Parse("USD 1-"), Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Parse_UndefinedCurrency_Exception()
+ {
+ Assert.That(() => Money.Parse("LOL 1"), Throws.InstanceOf());
+ }
+
+ [Test]
+ public void Parse_MissingAmount_Exception()
+ {
+ Assert.That(() => Money.Parse("USD"), Throws.InstanceOf());
+ }
+
+ #endregion
+
[TestCaseSource(nameof(positiveAmounts))]
public void Parse_CurrencyStyle_PositiveAmount_MoneyParsed(string positiveAmount, Currency currency, decimal amount)
{
@@ -134,106 +191,157 @@ public void Parse_WithStyle_IncorrectFormat_Exception()
#region TryParse
- [TestCaseSource(nameof(positiveAmounts))]
- public void TryParse_CurrencyStyle_PositiveAmount_MoneyParsed(string positiveAmount, Currency currency, decimal amount)
- {
- Assert.That(Money.TryParse(positiveAmount, currency, out Money? parsed), Is.True);
- Assert.That(parsed, Iz.MoneyWith(amount, currency));
- }
-
- [TestCaseSource(nameof(negativeAmounts))]
- public void TryParse_CurrencyStyle_NegativeAmount_MoneyParsed(string negativeAmount, Currency currency, decimal amount)
- {
- Assert.That(Money.TryParse(negativeAmount, currency, out Money? parsed), Is.True);
- Assert.That(parsed, Iz.MoneyWith(amount, currency));
- }
-
- [Test]
- public void TryParse_CurrencyStyle_MoreSignificantDecimalDigits_PrecisionMaintained()
- {
- Assert.That(Money.TryParse("$1.5678", Currency.Usd, out Money? parsed), Is.True);
- Assert.That(parsed, Iz.MoneyWith(1.5678m, Currency.Usd));
- }
-
- [Test]
- public void TryParse_CurrencyStyle_CurrenciesWithNoDecimals_MaintainsAmountButNoRepresented()
- {
- Money.TryParse("¥1.49", Currency.Jpy, out Money? moreThanOneYen);
- Assert.That(moreThanOneYen, Iz.MoneyWith(1.49m, Currency.Jpy));
- Assert.That(moreThanOneYen.ToString(), Is.EqualTo("¥1"));
-
- Money.TryParse("¥1.5", Currency.Jpy, out Money? lessThanTwoYen);
- Assert.That(lessThanTwoYen, Iz.MoneyWith(1.5m, Currency.Jpy));
- Assert.That(lessThanTwoYen.ToString(), Is.EqualTo("¥2"));
- }
-
- [TestCaseSource(nameof(flexibleParsing))]
- public void TryParse_CurrencyStyle_IsAsFlexibleAsParsingDecimals(string flexibleParse, Currency currency, decimal amount)
- {
- Assert.That(Money.TryParse(flexibleParse, currency, out Money? parsed), Is.True);
- Assert.That(parsed, Iz.MoneyWith(amount, currency));
- }
-
- [Test]
- public void TryParse_Default_IncorrectFormat_False()
- {
- Assert.That(Money.TryParse("not a Number", Currency.None, out Money? notParsed), Is.False);
- Assert.That(notParsed, Is.Null);
- Assert.That(Money.TryParse("1--", Currency.None, out notParsed), Is.False);
- Assert.That(notParsed, Is.Null);
- }
-
- [Test]
- public void TryParse_WithStyle_AllowsIgnoringCurrencySymbols()
- {
- Assert.That(Money.TryParse("3", NumberStyles.Number, Currency.Nok, out Money? parsed), Is.True);
- Assert.That(parsed, Iz.MoneyWith(3m, Currency.Nok));
- }
-
- [Test]
- public void TryParse_WithStyle_AllowsHavingStandardSignedAmounts()
- {
- // in usd, negative amounts go between parenthesis, we can override that behavior
- Assert.That(Money.TryParse("-73", NumberStyles.Number, Currency.Usd, out Money? parsed), Is.True);
- Assert.That(parsed, Iz.MoneyWith(-73m, Currency.Usd));
- }
-
- [Test]
- public void TryParse_WithStyle_IncorrectFormat_False()
- {
- Assert.That(Money.TryParse("¤1.4", NumberStyles.Integer, Currency.None, out Money? notParsed), Is.False);
- Assert.That(notParsed, Is.Null);
- Assert.That(Money.TryParse("1e-1", NumberStyles.Currency, Currency.None, out Money? notParsedEither), Is.False);
- Assert.That(notParsedEither, Is.Null);
- }
-
- [Test, Combinatorial]
- public void TryParse_CanParseAllWrittenCurrencies(
- [ValueSource(nameof(nonFractionalAmounts))]decimal amount,
- [ValueSource(nameof(allCurrencies))]Currency currency)
- {
- Money beforeWritting = new Money(amount, currency);
- Money.TryParse(beforeWritting.ToString(), currency, out Money? afterParsing);
-
- Assert.That(beforeWritting, Is.EqualTo(afterParsing));
- }
-
- #region Issue 28. Support detachment of parsing logic from currency
-
- [Test]
- public void TryParse_ParsingDetachedFromCurrency_FullControlOverParsing()
- {
- // currency does the parsing and the money construction: dot is not a decimal separator
- Money.TryParse("€1000.00", Currency.Eur, out Money? parsed);
- Assert.That(parsed, Is.EqualTo(100000m.Eur()));
-
- // NumberFormatInfo used for parsing, currency for building the money instance
- NumberFormatInfo parser = CultureInfo.GetCultureInfo("en-GB").NumberFormat;
- Money.TryParse("€1000.00", NumberStyles.Currency, parser, Currency.Eur, out parsed);
- Assert.That(parsed, Is.EqualTo(1000m.Eur()));
- }
-
- #endregion
-
- #endregion
+ #region quantity
+
+ [Test, TestCaseSource(nameof(quantities))]
+ public void TryParse_Quantity_MoneyCreatedAsPerSpec(string quantity, CurrencyIsoCode currency, decimal amount)
+ {
+ Assert.That(Money.TryParse(quantity, out Money? parsed), Is.True);
+ Assert.That(parsed, Is.EqualTo(new Money(amount, currency)));
+ }
+
+ [Test]
+ public void TryParse_ExponentialQuantity_MoneyParsed()
+ {
+ // cannot be written by .AsQuantity(), but can be parsed
+ Assert.That(Money.TryParse("USD 1E6", out Money? parsed), Is.True);
+ Assert.That(parsed, Is.EqualTo(1000000m.Usd()));
+ }
+
+ [Test]
+ public void TryParse_ThousandsQuantity_False()
+ {
+ Assert.That(Money.TryParse("XXX 1,000.1", out Money? parsed), Is.False);
+ Assert.That(parsed, Is.Null);
+ }
+
+ [Test]
+ public void TryParse_TrailingSignQuantity_False()
+ {
+ Assert.That(Money.TryParse("USD 1-", out Money? parsed), Is.False);
+ Assert.That(parsed, Is.Null);
+ }
+
+ [Test]
+ public void TryParse_UndefinedCurrency_Exception()
+ {
+ Assert.That(Money.TryParse("LOL 1", out Money? parsed), Is.False);
+ Assert.That(parsed, Is.Null);
+ }
+
+ [Test]
+ public void Parse_MissingAmount_False()
+ {
+ Assert.That(Money.TryParse("USD", out Money? parsed), Is.False);
+ Assert.That(parsed, Is.Null);
+ }
+
+ #endregion
+
+ [TestCaseSource(nameof(positiveAmounts))]
+ public void TryParse_CurrencyStyle_PositiveAmount_MoneyParsed(string positiveAmount, Currency currency,
+ decimal amount)
+ {
+ Assert.That(Money.TryParse(positiveAmount, currency, out Money? parsed), Is.True);
+ Assert.That(parsed, Iz.MoneyWith(amount, currency));
+ }
+
+ [TestCaseSource(nameof(negativeAmounts))]
+ public void TryParse_CurrencyStyle_NegativeAmount_MoneyParsed(string negativeAmount, Currency currency,
+ decimal amount)
+ {
+ Assert.That(Money.TryParse(negativeAmount, currency, out Money? parsed), Is.True);
+ Assert.That(parsed, Iz.MoneyWith(amount, currency));
+ }
+
+ [Test]
+ public void TryParse_CurrencyStyle_MoreSignificantDecimalDigits_PrecisionMaintained()
+ {
+ Assert.That(Money.TryParse("$1.5678", Currency.Usd, out Money? parsed), Is.True);
+ Assert.That(parsed, Iz.MoneyWith(1.5678m, Currency.Usd));
+ }
+
+ [Test]
+ public void TryParse_CurrencyStyle_CurrenciesWithNoDecimals_MaintainsAmountButNoRepresented()
+ {
+ Money.TryParse("¥1.49", Currency.Jpy, out Money? moreThanOneYen);
+ Assert.That(moreThanOneYen, Iz.MoneyWith(1.49m, Currency.Jpy));
+ Assert.That(moreThanOneYen.ToString(), Is.EqualTo("¥1"));
+
+ Money.TryParse("¥1.5", Currency.Jpy, out Money? lessThanTwoYen);
+ Assert.That(lessThanTwoYen, Iz.MoneyWith(1.5m, Currency.Jpy));
+ Assert.That(lessThanTwoYen.ToString(), Is.EqualTo("¥2"));
+ }
+
+ [TestCaseSource(nameof(flexibleParsing))]
+ public void TryParse_CurrencyStyle_IsAsFlexibleAsParsingDecimals(string flexibleParse, Currency currency,
+ decimal amount)
+ {
+ Assert.That(Money.TryParse(flexibleParse, currency, out Money? parsed), Is.True);
+ Assert.That(parsed, Iz.MoneyWith(amount, currency));
+ }
+
+ [Test]
+ public void TryParse_Default_IncorrectFormat_False()
+ {
+ Assert.That(Money.TryParse("not a Number", Currency.None, out Money? notParsed), Is.False);
+ Assert.That(notParsed, Is.Null);
+ Assert.That(Money.TryParse("1--", Currency.None, out notParsed), Is.False);
+ Assert.That(notParsed, Is.Null);
+ }
+
+ [Test]
+ public void TryParse_WithStyle_AllowsIgnoringCurrencySymbols()
+ {
+ Assert.That(Money.TryParse("3", NumberStyles.Number, Currency.Nok, out Money? parsed), Is.True);
+ Assert.That(parsed, Iz.MoneyWith(3m, Currency.Nok));
+ }
+
+ [Test]
+ public void TryParse_WithStyle_AllowsHavingStandardSignedAmounts()
+ {
+ // in usd, negative amounts go between parenthesis, we can override that behavior
+ Assert.That(Money.TryParse("-73", NumberStyles.Number, Currency.Usd, out Money? parsed), Is.True);
+ Assert.That(parsed, Iz.MoneyWith(-73m, Currency.Usd));
+ }
+
+ [Test]
+ public void TryParse_WithStyle_IncorrectFormat_False()
+ {
+ Assert.That(Money.TryParse("¤1.4", NumberStyles.Integer, Currency.None, out Money? notParsed), Is.False);
+ Assert.That(notParsed, Is.Null);
+ Assert.That(Money.TryParse("1e-1", NumberStyles.Currency, Currency.None, out Money? notParsedEither), Is.False);
+ Assert.That(notParsedEither, Is.Null);
+ }
+
+ [Test, Combinatorial]
+ public void TryParse_CanParseAllWrittenCurrencies(
+ [ValueSource(nameof(nonFractionalAmounts))]
+ decimal amount,
+ [ValueSource(nameof(allCurrencies))] Currency currency)
+ {
+ Money beforeWritting = new Money(amount, currency);
+ Money.TryParse(beforeWritting.ToString(), currency, out Money? afterParsing);
+
+ Assert.That(beforeWritting, Is.EqualTo(afterParsing));
+ }
+
+ #region Issue 28. Support detachment of parsing logic from currency
+
+ [Test]
+ public void TryParse_ParsingDetachedFromCurrency_FullControlOverParsing()
+ {
+ // currency does the parsing and the money construction: dot is not a decimal separator
+ Money.TryParse("€1000.00", Currency.Eur, out Money? parsed);
+ Assert.That(parsed, Is.EqualTo(100000m.Eur()));
+
+ // NumberFormatInfo used for parsing, currency for building the money instance
+ NumberFormatInfo parser = CultureInfo.GetCultureInfo("en-GB").NumberFormat;
+ Money.TryParse("€1000.00", NumberStyles.Currency, parser, Currency.Eur, out parsed);
+ Assert.That(parsed, Is.EqualTo(1000m.Eur()));
+ }
+
+ #endregion
+
+ #endregion
}