From 2e8995223e9ee8b50dae2621c53185533c6a9b8d Mon Sep 17 00:00:00 2001 From: Marnix Kraus Date: Tue, 8 Jun 2021 10:38:56 +0200 Subject: [PATCH 1/2] Add possibility to register custom types to the JsonConverter The Activator is used to instantiate objects, instead of calling Quantity.From. The assumption is that there will be a constructor of new T(double value, TUnit unit). --- .../UnitsNetBaseJsonConverter.cs | 82 +++++++++++++++---- .../CustomQuantities/HowMuchTests.cs | 25 ++++++ UnitsNet.Tests/UnitsNet.Tests.csproj | 1 + 3 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 UnitsNet.Tests/CustomQuantities/HowMuchTests.cs diff --git a/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs b/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs index 9ead71de7e..1bd400ee4a 100644 --- a/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs +++ b/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs @@ -2,6 +2,7 @@ // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using JetBrains.Annotations; @@ -17,6 +18,28 @@ namespace UnitsNet.Serialization.JsonNet /// The type being converted. Should either be or public abstract class UnitsNetBaseJsonConverter : JsonConverter { + private List<(Type Quantity, Type Unit)> _registeredTypes = new(); + + /// + /// Register custom types so that the converter can instantiate these quantities. + /// Instead of calling , the will be used to instantiate the object. + /// It is therefore assumed that the constructor of is specified with new T(double value, typeof() unit). + /// + public void RegisterCustomType(Type quantity, Type unit) + { + if (!typeof(T).IsAssignableFrom(quantity)) + { + throw new ArgumentException($"The type {quantity} is not a {nameof(T)}"); + } + + if (!typeof(Enum).IsAssignableFrom(unit)) + { + throw new ArgumentException($"The type {unit} is not a {nameof(Enum)}"); + } + + _registeredTypes.Add((quantity, unit)); + } + /// /// Reads the "Unit" and "Value" properties from a JSON string /// @@ -79,6 +102,12 @@ protected IQuantity ConvertValueUnit(ValueUnit valueUnit) } var unit = GetUnit(valueUnit.Unit); + Type registeredType = GetRegisteredType(valueUnit.Unit); + + if (registeredType is not null) + { + return (IQuantity)Activator.CreateInstance(registeredType, valueUnit.Value, unit); + } return valueUnit switch { @@ -87,7 +116,40 @@ protected IQuantity ConvertValueUnit(ValueUnit valueUnit) }; } - private static Enum GetUnit(string unit) + private Type GetRegisteredType(string unit) + { + (var unitEnumTypeName, var unitEnumValue) = SplitUnitString(unit); + return _registeredTypes.Find(t => t.Unit.Name == unitEnumTypeName).Quantity; + } + + private Enum GetUnit(string unit) + { + (var unitEnumTypeName, var unitEnumValue) = SplitUnitString(unit); + + // First try to find the name in the list of registered types. + var unitEnumType = _registeredTypes.Find(t => t.Unit.Name == unitEnumTypeName).Unit; + + if (unitEnumType is null) + { + // "UnitsNet.Units.MassUnit,UnitsNet" + var unitEnumTypeAssemblyQualifiedName = "UnitsNet.Units." + unitEnumTypeName + ",UnitsNet"; + + // -- see http://stackoverflow.com/a/6465096/1256096 for details + unitEnumType = Type.GetType(unitEnumTypeAssemblyQualifiedName); + + if (unitEnumType is null) + { + var ex = new UnitsNetException("Unable to find enum type."); + ex.Data["type"] = unitEnumTypeAssemblyQualifiedName; + throw ex; + } + } + + var unitValue = (Enum) Enum.Parse(unitEnumType, unitEnumValue); // Ex: MassUnit.Kilogram + return unitValue; + } + + private static (string EnumName, string EnumValue) SplitUnitString(string unit) { var unitParts = unit.Split('.'); @@ -99,23 +161,7 @@ private static Enum GetUnit(string unit) } // "MassUnit.Kilogram" => "MassUnit" and "Kilogram" - var unitEnumTypeName = unitParts[0]; - var unitEnumValue = unitParts[1]; - - // "UnitsNet.Units.MassUnit,UnitsNet" - var unitEnumTypeAssemblyQualifiedName = "UnitsNet.Units." + unitEnumTypeName + ",UnitsNet"; - - // -- see http://stackoverflow.com/a/6465096/1256096 for details - var unitEnumType = Type.GetType(unitEnumTypeAssemblyQualifiedName); - if (unitEnumType == null) - { - var ex = new UnitsNetException("Unable to find enum type."); - ex.Data["type"] = unitEnumTypeAssemblyQualifiedName; - throw ex; - } - - var unitValue = (Enum) Enum.Parse(unitEnumType, unitEnumValue); // Ex: MassUnit.Kilogram - return unitValue; + return (unitParts[0], unitParts[1]); } /// diff --git a/UnitsNet.Tests/CustomQuantities/HowMuchTests.cs b/UnitsNet.Tests/CustomQuantities/HowMuchTests.cs new file mode 100644 index 0000000000..aa43df1015 --- /dev/null +++ b/UnitsNet.Tests/CustomQuantities/HowMuchTests.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using UnitsNet.Serialization.JsonNet; +using Xunit; + +namespace UnitsNet.Tests.CustomQuantities +{ + public class HowMuchTests + { + [Fact] + public static void SerializeAndDeserializeCreatesSameObjectForIQuantity() + { + var jsonSerializerSettings = new JsonSerializerSettings { Formatting = Formatting.Indented }; + var quantityConverter = new UnitsNetIQuantityJsonConverter(); + quantityConverter.RegisterCustomType(typeof(HowMuch), typeof(HowMuchUnit)); + jsonSerializerSettings.Converters.Add(quantityConverter); + + var quantity = new HowMuch(12.34, HowMuchUnit.ATon); + + var serializedQuantity = JsonConvert.SerializeObject(quantity, jsonSerializerSettings); + + var deserializedQuantity = JsonConvert.DeserializeObject(serializedQuantity, jsonSerializerSettings); + Assert.Equal(quantity, deserializedQuantity); + } + } +} diff --git a/UnitsNet.Tests/UnitsNet.Tests.csproj b/UnitsNet.Tests/UnitsNet.Tests.csproj index 11e56a9173..6b780f33b6 100644 --- a/UnitsNet.Tests/UnitsNet.Tests.csproj +++ b/UnitsNet.Tests/UnitsNet.Tests.csproj @@ -32,6 +32,7 @@ + From b7214b49baaa38e638a36e0fbfea6175ba3e5d64 Mon Sep 17 00:00:00 2001 From: Marnix Kraus Date: Mon, 5 Jul 2021 08:43:45 +0200 Subject: [PATCH 2/2] Fix review comments Use ConcurrentDictionary Move test to Serialization --- .../CustomQuantities/HowMuchTests.cs | 0 ...nitsNet.Serialization.JsonNet.Tests.csproj | 1 + .../UnitsNetBaseJsonConverter.cs | 28 +++++++++++-------- UnitsNet.Tests/UnitsNet.Tests.csproj | 1 - 4 files changed, 18 insertions(+), 12 deletions(-) rename {UnitsNet.Tests => UnitsNet.Serialization.JsonNet.Tests}/CustomQuantities/HowMuchTests.cs (100%) diff --git a/UnitsNet.Tests/CustomQuantities/HowMuchTests.cs b/UnitsNet.Serialization.JsonNet.Tests/CustomQuantities/HowMuchTests.cs similarity index 100% rename from UnitsNet.Tests/CustomQuantities/HowMuchTests.cs rename to UnitsNet.Serialization.JsonNet.Tests/CustomQuantities/HowMuchTests.cs diff --git a/UnitsNet.Serialization.JsonNet.Tests/UnitsNet.Serialization.JsonNet.Tests.csproj b/UnitsNet.Serialization.JsonNet.Tests/UnitsNet.Serialization.JsonNet.Tests.csproj index 50c6292c2a..05eb397a50 100644 --- a/UnitsNet.Serialization.JsonNet.Tests/UnitsNet.Serialization.JsonNet.Tests.csproj +++ b/UnitsNet.Serialization.JsonNet.Tests/UnitsNet.Serialization.JsonNet.Tests.csproj @@ -23,6 +23,7 @@ + diff --git a/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs b/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs index 1bd400ee4a..f73416c2ea 100644 --- a/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs +++ b/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs @@ -2,7 +2,7 @@ // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Globalization; using System.Linq; using JetBrains.Annotations; @@ -18,18 +18,19 @@ namespace UnitsNet.Serialization.JsonNet /// The type being converted. Should either be or public abstract class UnitsNetBaseJsonConverter : JsonConverter { - private List<(Type Quantity, Type Unit)> _registeredTypes = new(); + private ConcurrentDictionary _registeredTypes = new(); /// /// Register custom types so that the converter can instantiate these quantities. /// Instead of calling , the will be used to instantiate the object. /// It is therefore assumed that the constructor of is specified with new T(double value, typeof() unit). + /// Registering the same multiple times, it will overwrite the one registered. /// public void RegisterCustomType(Type quantity, Type unit) { if (!typeof(T).IsAssignableFrom(quantity)) { - throw new ArgumentException($"The type {quantity} is not a {nameof(T)}"); + throw new ArgumentException($"The type {quantity} is not a {typeof(T)}"); } if (!typeof(Enum).IsAssignableFrom(unit)) @@ -37,7 +38,7 @@ public void RegisterCustomType(Type quantity, Type unit) throw new ArgumentException($"The type {unit} is not a {nameof(Enum)}"); } - _registeredTypes.Add((quantity, unit)); + _registeredTypes[unit.Name] = (quantity, unit); } /// @@ -102,11 +103,11 @@ protected IQuantity ConvertValueUnit(ValueUnit valueUnit) } var unit = GetUnit(valueUnit.Unit); - Type registeredType = GetRegisteredType(valueUnit.Unit); + var registeredQuantity = GetRegisteredType(valueUnit.Unit).Quantity; - if (registeredType is not null) + if (registeredQuantity is not null) { - return (IQuantity)Activator.CreateInstance(registeredType, valueUnit.Value, unit); + return (IQuantity)Activator.CreateInstance(registeredQuantity, valueUnit.Value, unit); } return valueUnit switch @@ -116,10 +117,15 @@ protected IQuantity ConvertValueUnit(ValueUnit valueUnit) }; } - private Type GetRegisteredType(string unit) + private (Type Quantity, Type Unit) GetRegisteredType(string unit) { - (var unitEnumTypeName, var unitEnumValue) = SplitUnitString(unit); - return _registeredTypes.Find(t => t.Unit.Name == unitEnumTypeName).Quantity; + (var unitEnumTypeName, var _) = SplitUnitString(unit); + if (_registeredTypes.TryGetValue(unitEnumTypeName, out var registeredType)) + { + return registeredType; + } + + return (null, null); } private Enum GetUnit(string unit) @@ -127,7 +133,7 @@ private Enum GetUnit(string unit) (var unitEnumTypeName, var unitEnumValue) = SplitUnitString(unit); // First try to find the name in the list of registered types. - var unitEnumType = _registeredTypes.Find(t => t.Unit.Name == unitEnumTypeName).Unit; + var unitEnumType = GetRegisteredType(unit).Unit; if (unitEnumType is null) { diff --git a/UnitsNet.Tests/UnitsNet.Tests.csproj b/UnitsNet.Tests/UnitsNet.Tests.csproj index 6b780f33b6..11e56a9173 100644 --- a/UnitsNet.Tests/UnitsNet.Tests.csproj +++ b/UnitsNet.Tests/UnitsNet.Tests.csproj @@ -32,7 +32,6 @@ -