diff --git a/UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs b/UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs index 2e88dc6d9e..f2c8cb949a 100644 --- a/UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs +++ b/UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs @@ -230,13 +230,13 @@ private class TestConverter : UnitsNetBaseJsonConverter public (string Unit, double Value) Test_ConvertDoubleIQuantity(IQuantity value) { - var result = ConvertIQuantity(value); + var result = BaseConverter.ConvertIQuantity(value, CreateValueUnit, CreateExtendedValueUnit); return (result.Unit, result.Value); } public (string Unit, decimal Value) Test_ConvertDecimalIQuantity(IQuantity value) { - var result = ConvertIQuantity(value); + var result = BaseConverter.ConvertIQuantity(value, CreateValueUnit, CreateExtendedValueUnit); if (result is ExtendedValueUnit {ValueType: "decimal"} decimalResult) { return (result.Unit, decimal.Parse(decimalResult.ValueString, CultureInfo.InvariantCulture)); @@ -253,7 +253,8 @@ public IQuantity Test_ConvertDecimalValueUnit(string unit, decimal value) => Tes }); public IQuantity Test_ConvertValueUnit() => Test_ConvertValueUnit(null); - private IQuantity Test_ConvertValueUnit(ValueUnit valueUnit) => ConvertValueUnit(valueUnit); + + private IQuantity Test_ConvertValueUnit(ValueUnit valueUnit) => BaseConverter.ConvertValueUnit(valueUnit); public JsonSerializer Test_CreateLocalSerializer(JsonSerializer serializer) => CreateLocalSerializer(serializer, this); diff --git a/UnitsNet.Serialization.JsonNet/UnitsNet.Serialization.JsonNet.csproj b/UnitsNet.Serialization.JsonNet/UnitsNet.Serialization.JsonNet.csproj index 9e9c8ac759..a06d9ed27e 100644 --- a/UnitsNet.Serialization.JsonNet/UnitsNet.Serialization.JsonNet.csproj +++ b/UnitsNet.Serialization.JsonNet/UnitsNet.Serialization.JsonNet.csproj @@ -49,12 +49,13 @@ + - + diff --git a/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs b/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs index f73416c2ea..bf884004cd 100644 --- a/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs +++ b/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs @@ -2,8 +2,6 @@ // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; -using System.Collections.Concurrent; -using System.Globalization; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; @@ -18,28 +16,27 @@ namespace UnitsNet.Serialization.JsonNet /// The type being converted. Should either be or public abstract class UnitsNetBaseJsonConverter : JsonConverter { - 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. + /// Base converter functionality /// + protected readonly QuantityConverter BaseConverter = new(); + + /// public void RegisterCustomType(Type quantity, Type unit) { - if (!typeof(T).IsAssignableFrom(quantity)) - { - throw new ArgumentException($"The type {quantity} is not a {typeof(T)}"); - } + BaseConverter.RegisterCustomType(quantity, unit); + } - if (!typeof(Enum).IsAssignableFrom(unit)) - { - throw new ArgumentException($"The type {unit} is not a {nameof(Enum)}"); - } + /// + /// Factory method to create a + /// + protected static ValueUnit CreateValueUnit(string unit, double value) => new ValueUnit { Unit = unit, Value = value }; - _registeredTypes[unit.Name] = (quantity, unit); - } + /// + /// Factory method to create a + /// + protected static ExtendedValueUnit CreateExtendedValueUnit(string unit, double value, string valueString, string valueType) + => new ExtendedValueUnit { Unit = unit, Value = value, ValueString = valueString, ValueType = valueType}; /// /// Reads the "Unit" and "Value" properties from a JSON string @@ -89,110 +86,6 @@ protected ValueUnit ReadValueUnit(JToken jsonToken) }; } - /// - /// Convert a to an - /// - /// The value unit to convert - /// Thrown when an invalid Unit has been provided - /// An IQuantity - protected IQuantity ConvertValueUnit(ValueUnit valueUnit) - { - if (string.IsNullOrWhiteSpace(valueUnit?.Unit)) - { - return null; - } - - var unit = GetUnit(valueUnit.Unit); - var registeredQuantity = GetRegisteredType(valueUnit.Unit).Quantity; - - if (registeredQuantity is not null) - { - return (IQuantity)Activator.CreateInstance(registeredQuantity, valueUnit.Value, unit); - } - - return valueUnit switch - { - ExtendedValueUnit {ValueType: "decimal"} extendedValueUnit => Quantity.From(decimal.Parse(extendedValueUnit.ValueString, CultureInfo.InvariantCulture), unit), - _ => Quantity.From(valueUnit.Value, unit) - }; - } - - private (Type Quantity, Type Unit) GetRegisteredType(string unit) - { - (var unitEnumTypeName, var _) = SplitUnitString(unit); - if (_registeredTypes.TryGetValue(unitEnumTypeName, out var registeredType)) - { - return registeredType; - } - - return (null, null); - } - - 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 = GetRegisteredType(unit).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('.'); - - if (unitParts.Length != 2) - { - var ex = new UnitsNetException($"\"{unit}\" is not a valid unit."); - ex.Data["type"] = unit; - throw ex; - } - - // "MassUnit.Kilogram" => "MassUnit" and "Kilogram" - return (unitParts[0], unitParts[1]); - } - - /// - /// Convert an to a - /// - /// The quantity to convert - /// A serializable object. - protected ValueUnit ConvertIQuantity(IQuantity quantity) - { - quantity = quantity ?? throw new ArgumentNullException(nameof(quantity)); - - if (quantity is IDecimalQuantity d) - { - return new ExtendedValueUnit - { - Unit = $"{quantity.QuantityInfo.UnitType.Name}.{quantity.Unit}", - Value = quantity.Value, - ValueString = d.Value.ToString(CultureInfo.InvariantCulture), - ValueType = "decimal" - }; - } - - return new ValueUnit {Value = quantity.Value, Unit = $"{quantity.QuantityInfo.UnitType.Name}.{quantity.Unit}"}; - } - /// /// Create a copy of a serializer, retaining any settings but leaving out a converter to prevent loops /// @@ -240,44 +133,26 @@ protected JsonSerializer CreateLocalSerializer(JsonSerializer serializer, JsonCo return localSerializer; } - /// - /// A structure used to serialize/deserialize Units.NET unit instances. - /// - protected class ValueUnit + /// + protected class ValueUnit: IValueUnit { - /// - /// The unit of the value. - /// - /// MassUnit.Pound - /// InformationUnit.Kilobyte + /// [JsonProperty(Order = 1)] public string Unit { get; [UsedImplicitly] set; } - /// - /// The value. - /// + /// [JsonProperty(Order = 2)] public double Value { get; [UsedImplicitly] set; } } - /// - /// A structure used to serialize/deserialize non-double Units.NET unit instances. - /// - /// - /// This type was added for lossless serialization of quantities with values. - /// The type distinguishes between 100 and 100.00 but Json.NET does not, therefore we serialize decimal values as string. - /// - protected sealed class ExtendedValueUnit : ValueUnit + /// + protected sealed class ExtendedValueUnit : ValueUnit, IExtendedValueUnit { - /// - /// The value as a string. - /// + /// [JsonProperty(Order = 3)] public string ValueString { get; [UsedImplicitly] set; } - /// - /// The type of the value, e.g. "decimal". - /// + /// [JsonProperty(Order = 4)] public string ValueType { get; [UsedImplicitly] set; } } diff --git a/UnitsNet.Serialization.JsonNet/UnitsNetIComparableJsonConverter.cs b/UnitsNet.Serialization.JsonNet/UnitsNetIComparableJsonConverter.cs index 66a3b57415..9982383f49 100644 --- a/UnitsNet.Serialization.JsonNet/UnitsNetIComparableJsonConverter.cs +++ b/UnitsNet.Serialization.JsonNet/UnitsNetIComparableJsonConverter.cs @@ -76,7 +76,7 @@ public override IComparable ReadJson([NotNull] JsonReader reader, Type objectTyp return token.ToObject(localSerializer); } - return ConvertValueUnit(valueUnit) as IComparable; + return BaseConverter.ConvertValueUnit(valueUnit) as IComparable; } } } diff --git a/UnitsNet.Serialization.JsonNet/UnitsNetIQuantityJsonConverter.cs b/UnitsNet.Serialization.JsonNet/UnitsNetIQuantityJsonConverter.cs index 4f258040f3..8946c6cd47 100644 --- a/UnitsNet.Serialization.JsonNet/UnitsNetIQuantityJsonConverter.cs +++ b/UnitsNet.Serialization.JsonNet/UnitsNetIQuantityJsonConverter.cs @@ -32,11 +32,13 @@ public override void WriteJson([NotNull] JsonWriter writer, IQuantity value, [No return; } - var valueUnit = ConvertIQuantity(value); + var valueUnit = (ValueUnit) BaseConverter.ConvertIQuantity(value, CreateValueUnit, CreateExtendedValueUnit); serializer.Serialize(writer, valueUnit); } + + /// /// Reads the JSON representation of the object. /// @@ -64,7 +66,7 @@ public override IQuantity ReadJson([NotNull] JsonReader reader, Type objectType, var valueUnit = ReadValueUnit(token); - return ConvertValueUnit(valueUnit); + return BaseConverter.ConvertValueUnit(valueUnit); } } } diff --git a/UnitsNet.Serialization.SystemTextJson.Tests/ChuckerTest.cs b/UnitsNet.Serialization.SystemTextJson.Tests/ChuckerTest.cs new file mode 100644 index 0000000000..f624274360 --- /dev/null +++ b/UnitsNet.Serialization.SystemTextJson.Tests/ChuckerTest.cs @@ -0,0 +1,53 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using UnitsNet.Serialization.SystemTextJson.Tests.Infrastructure; +using Xunit; + +namespace UnitsNet.Serialization.SystemTextJson.Tests +{ + public class ChuckerTest : UnitsNetJsonBaseTest + { + [Fact] + public void CellularDataUsage_CanDeserialize() + { + var expected = new CellularDataUsage( + Remaining: Information.FromGibibits(16), + Incremental: null, + Total: Information.FromKibibits(32)); + + var json = SerializeObject(expected); + + var actual = DeserializeObject(json); + + Assert.Equal(expected.Remaining, actual.Remaining); + Assert.Equal(expected.Incremental, actual.Incremental); + Assert.Equal(expected.Total, actual.Total); + + + } + + [Fact] + public void UnitsNet905_CanDeserialize() + { + var expected = new UnitsNet905 + { + DataUsage = UnitsNet.Information.FromGibibytes(15) + }; + + var json = SerializeObject(expected); + + var actual = DeserializeObject(json); + + Assert.Equal(expected.DataUsage, actual.DataUsage); + } + } + + public class UnitsNet905 + { + public Information DataUsage { get; set; } + } + + public record CellularDataUsage(Information Remaining, Information? Incremental, Information Total); + +} diff --git a/UnitsNet.Serialization.SystemTextJson.Tests/CustomQuantities/HowMuchTests.cs b/UnitsNet.Serialization.SystemTextJson.Tests/CustomQuantities/HowMuchTests.cs new file mode 100644 index 0000000000..b2f1afd27d --- /dev/null +++ b/UnitsNet.Serialization.SystemTextJson.Tests/CustomQuantities/HowMuchTests.cs @@ -0,0 +1,26 @@ +using System.Text.Json; +using UnitsNet.Serialization.SystemTextJson.Tests.Infrastructure; +using UnitsNet.Tests.CustomQuantities; +using Xunit; + +namespace UnitsNet.Serialization.SystemTextJson.Tests.CustomQuantities +{ + public class HowMuchTests + { + [Fact] + public static void SerializeAndDeserializeCreatesSameObjectForIQuantity() + { + var jsonSerializerSettings = new JsonSerializerOptions() { WriteIndented = true }; + var quantityConverterFactory = new UnitsNetIQuantityJsonConverterFactory(); + quantityConverterFactory.RegisterCustomType(typeof(HowMuch), typeof(HowMuchUnit)); + jsonSerializerSettings.Converters.Add(quantityConverterFactory); + + var quantity = new HowMuch(12.34, HowMuchUnit.ATon); + + var serializedQuantity = JsonSerializer.Serialize(quantity, jsonSerializerSettings); + + var deserializedQuantity = JsonSerializer.Deserialize(serializedQuantity, jsonSerializerSettings); + Assert.Equal(quantity, deserializedQuantity); + } + } +} diff --git a/UnitsNet.Serialization.SystemTextJson.Tests/Infrastructure/UnitsNetJsonBaseTest.cs b/UnitsNet.Serialization.SystemTextJson.Tests/Infrastructure/UnitsNetJsonBaseTest.cs new file mode 100644 index 0000000000..22731240e1 --- /dev/null +++ b/UnitsNet.Serialization.SystemTextJson.Tests/Infrastructure/UnitsNetJsonBaseTest.cs @@ -0,0 +1,28 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using System.Text.Json; + +namespace UnitsNet.Serialization.SystemTextJson.Tests.Infrastructure +{ + public abstract class UnitsNetJsonBaseTest + { + private readonly JsonSerializerOptions _jsonSerializerSettings; + + protected UnitsNetJsonBaseTest() + { + _jsonSerializerSettings = new JsonSerializerOptions { WriteIndented = true }; + _jsonSerializerSettings.Converters.Add(new UnitsNetIQuantityJsonConverterFactory()); + } + + protected string SerializeObject(object obj) + { + return JsonSerializer.Serialize(obj, _jsonSerializerSettings).Replace("\r\n", "\n"); + } + + protected T DeserializeObject(string json) + { + return JsonSerializer.Deserialize(json, _jsonSerializerSettings); + } + } +} diff --git a/UnitsNet.Serialization.SystemTextJson.Tests/UnitsNet.Serialization.SystemTextJson.Tests.csproj b/UnitsNet.Serialization.SystemTextJson.Tests/UnitsNet.Serialization.SystemTextJson.Tests.csproj new file mode 100644 index 0000000000..d1b9c5d7da --- /dev/null +++ b/UnitsNet.Serialization.SystemTextJson.Tests/UnitsNet.Serialization.SystemTextJson.Tests.csproj @@ -0,0 +1,29 @@ + + + + net5.0 + UnitsNet.Serialization.SystemTextJson.Tests + latest + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/UnitsNet.Serialization.SystemTextJson.Tests/UnitsNetIQuantityJsonConverterTest.cs b/UnitsNet.Serialization.SystemTextJson.Tests/UnitsNetIQuantityJsonConverterTest.cs new file mode 100644 index 0000000000..692c74fe1c --- /dev/null +++ b/UnitsNet.Serialization.SystemTextJson.Tests/UnitsNetIQuantityJsonConverterTest.cs @@ -0,0 +1,104 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using System.IO; +using System.Text; +using System.Text.Json; +using Xunit; + +namespace UnitsNet.Serialization.SystemTextJson.Tests +{ + public sealed class UnitsNetIQuantityJsonConverterTest + { + private readonly UnitsNetIQuantityJsonConverter _sut; + + public UnitsNetIQuantityJsonConverterTest() + { + _sut = new UnitsNetIQuantityJsonConverter(); + } + + [Fact] + public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_NULL_value() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + _sut.Write(writer, null, new JsonSerializerOptions()); + + var result = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("", result); + } + + [Fact] + public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_double_quantity() + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + _sut.Write(writer, Length.FromMeters(10.2365D), new JsonSerializerOptions()); + + var result = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("{\"Unit\":\"LengthUnit.Meter\",\"Value\":10.2365}", result); + } + + [Theory] + [InlineData(10.2365, 10.2365, "10.2365")] + [InlineData(10, 10, "10")] + public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_decimal_quantity(decimal value, double expectedValue, string expectedValueString) + { + using MemoryStream stream = new(); + using Utf8JsonWriter writer = new(stream); + + var powerValue = Power.FromWatts(value); + _sut.Write(writer, powerValue, new JsonSerializerOptions()); + + var result = Encoding.UTF8.GetString(stream.ToArray()); + + var resultElem = JsonDocument.Parse(result).RootElement; + Assert.Equal("PowerUnit.Watt", resultElem.GetProperty("Unit").GetString()); + Assert.Equal(expectedValue, resultElem.GetProperty("Value").GetDouble()); + Assert.Equal(expectedValueString, resultElem.GetProperty("ValueString").GetString()); + Assert.Equal("decimal", resultElem.GetProperty("ValueType").GetString()); + } + + [Fact] + public void UnitsNetIQuantityJsonConverter_ReadJson_handles_NULL_values_correctly() + { + var json = "null"; + + var jsonData = Encoding.UTF8.GetBytes(json); + var jsonReader = new Utf8JsonReader(jsonData); + var result = _sut.Read(ref jsonReader, typeof(IQuantity), new JsonSerializerOptions()); + + Assert.Null(result); + } + + [Fact] + public void UnitsNetIQuantityJsonConverter_Read_works_as_expected() + { + var json = "{ \"Unit\": \"PowerUnit.Watt\", \"Value\": 10.3654}"; + + var jsonData = Encoding.UTF8.GetBytes(json); + var jsonReader = new Utf8JsonReader(jsonData); + var result = _sut.Read(ref jsonReader, typeof(IQuantity), new JsonSerializerOptions()); + + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(10.3654D, ((Power)result).Watts); + } + + [Fact] + public void UnitsNetIQuantityJsonConverter_Read_extended_works_as_expected() + { + var json = "{\"ValueString\": \"10.2365\",\"ValueType\": \"decimal\",\"Unit\": \"PowerUnit.Watt\",\"Value\": 10.237}"; + + var jsonData = Encoding.UTF8.GetBytes(json); + var jsonReader = new Utf8JsonReader(jsonData); + var result = _sut.Read(ref jsonReader, typeof(IQuantity), new JsonSerializerOptions()); + + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(10.2365M, ((Power)result).Value); + } + } +} diff --git a/UnitsNet.Serialization.SystemTextJson/UnitsNet.Serialization.SystemTextJson.csproj b/UnitsNet.Serialization.SystemTextJson/UnitsNet.Serialization.SystemTextJson.csproj new file mode 100644 index 0000000000..8fa923cebd --- /dev/null +++ b/UnitsNet.Serialization.SystemTextJson/UnitsNet.Serialization.SystemTextJson.csproj @@ -0,0 +1,63 @@ + + + + UnitsNet.Serialization.SystemTextJson + 4.4.0 + Andreas Gullberg Larsen + Units.NET Serialization with Json.NET + A helper library for serializing and deserializing types in Units.NET using Json.NET. + Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). + true + https://github.com/angularsen/UnitsNet + logo-32.png + https://raw.githubusercontent.com/angularsen/UnitsNet/ce85185429be345d77eb2ce09c99d59cc9ab8aed/Docs/Images/logo-32.png + https://github.com/angularsen/UnitsNet + MIT + false + unit units measurement json Json.NET Newtonsoft serialize deserialize serialization deserialization + Upgrade JSON.NET to 12.0.3. Support arrays. + + + + + 4.0.0.0 + latest + UnitsNet.Serialization.SystemTextJson + netstandard2.0 + + + + + true + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + ../UnitsNet.snk + false + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UnitsNet.Serialization.SystemTextJson/UnitsNetIQuantityJsonConverter.cs b/UnitsNet.Serialization.SystemTextJson/UnitsNetIQuantityJsonConverter.cs new file mode 100644 index 0000000000..ccedbba277 --- /dev/null +++ b/UnitsNet.Serialization.SystemTextJson/UnitsNetIQuantityJsonConverter.cs @@ -0,0 +1,148 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace UnitsNet.Serialization.SystemTextJson +{ + /// + /// System.Text.Json converter for a specific IQuantity subtypes. + /// Normally, should be used from client code. + /// + public class UnitsNetIQuantityJsonConverter : JsonConverter where TQuantity : IQuantity + { + private readonly QuantityConverter _baseConverter; + + /// + /// Instantiates this converter with an optional base converter + /// + /// + public UnitsNetIQuantityJsonConverter(QuantityConverter baseConverter = null) + { + _baseConverter = baseConverter ?? new QuantityConverter(); + } + + /// + public void RegisterCustomType(Type quantity, Type unit) + { + _baseConverter.RegisterCustomType(quantity, unit); + } + + /// + public override TQuantity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var token = JsonDocument.ParseValue(ref reader).RootElement; + + if (reader.TokenType == JsonTokenType.Null) + { + return default(TQuantity); + } + + var valueUnit = ReadValueUnit(token); + + return (TQuantity)_baseConverter.ConvertValueUnit(valueUnit); + } + + /// + public override void Write(Utf8JsonWriter writer, TQuantity value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + // convert to object so that Serialize correctly used ExtendedValueUnit if necessary + var valueUnit = (object) _baseConverter.ConvertIQuantity(value, CreateValueUnit, CreateExtendedValueUnit); + JsonSerializer.Serialize(writer, valueUnit, options); + } + + /// + public override bool CanConvert(Type typeToConvert) => typeof(TQuantity).IsAssignableFrom(typeToConvert); + + /// + /// Factory method to create a + /// + private static ValueUnit CreateValueUnit(string unit, double value) => new ValueUnit { Unit = unit, Value = value }; + + /// + /// Factory method to create a + /// + private static ExtendedValueUnit CreateExtendedValueUnit(string unit, double value, string valueString, string valueType) + => new ExtendedValueUnit { Unit = unit, Value = value, ValueString = valueString, ValueType = valueType}; + + private ValueUnit ReadValueUnit(JsonElement serializedQuantity) + { + var unit = serializedQuantity.GetPropertyOrNull(nameof(ValueUnit.Unit)); + var value = serializedQuantity.GetPropertyOrNull(nameof(ValueUnit.Value)); + var valueType = serializedQuantity.GetPropertyOrNull(nameof(ExtendedValueUnit.ValueType)); + var valueString = serializedQuantity.GetPropertyOrNull(nameof(ExtendedValueUnit.ValueString)); + + if (unit == null || value == null) + { + return null; + } + + if (valueType == null) + { + if (value.Value.ValueKind != JsonValueKind.Number) + { + return null; + } + + return new ValueUnit {Unit = unit.Value.GetString(), Value = value.Value.GetDouble()}; + } + + if (valueType.Value.ValueKind != JsonValueKind.String) + { + return null; + } + + return new ExtendedValueUnit + { + Unit = unit.Value.GetString(), + Value = value.Value.GetDouble(), + ValueType = valueType.Value.GetString(), + ValueString = valueString?.GetString() + }; + } + + /// + protected class ValueUnit: IValueUnit + { + /// + [JsonPropertyName(nameof(Unit))] + public string Unit { get; set; } + + /// + [JsonPropertyName(nameof(Value))] + public double Value { get; set; } + } + + /// + protected sealed class ExtendedValueUnit : ValueUnit, IExtendedValueUnit + { + /// + [JsonPropertyName(nameof(ValueString))] + public string ValueString { get; set; } + + /// + [JsonPropertyName(nameof(ValueType))] + public string ValueType { get; set; } + } + } + + static class GetPropertyOrNullExtension + { + public static JsonElement? GetPropertyOrNull(this JsonElement me, string propertyName) + { + if (me.TryGetProperty(propertyName, out var result)) + { + return result; + } + else + { + return null; + } + } + } +} diff --git a/UnitsNet.Serialization.SystemTextJson/UnitsNetIQuantityJsonConverterFactory.cs b/UnitsNet.Serialization.SystemTextJson/UnitsNetIQuantityJsonConverterFactory.cs new file mode 100644 index 0000000000..70a9107abb --- /dev/null +++ b/UnitsNet.Serialization.SystemTextJson/UnitsNetIQuantityJsonConverterFactory.cs @@ -0,0 +1,45 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace UnitsNet.Serialization.SystemTextJson +{ + /// + /// System.Text.Json converter factory for IQuantity subtypes (e.g. all units in UnitsNet) + /// Use this converter to serialize and deserialize UnitsNet types to and from JSON + /// + /// Supports polymorphic deserialization of IQuantity subtypes + public class UnitsNetIQuantityJsonConverterFactory : JsonConverterFactory + { + /// + /// Base converter functionality + /// + private readonly QuantityConverter _baseConverter = new(); + + /// + public void RegisterCustomType(Type quantity, Type unit) + { + _baseConverter.RegisterCustomType(quantity, unit); + } + + /// + public override bool CanConvert(Type typeToConvert) + { + return typeof(IQuantity).IsAssignableFrom(typeToConvert); + } + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var converterType = typeof(UnitsNetIQuantityJsonConverter<>).MakeGenericType(typeToConvert); + // passing baseConverter to concrete converter instance so that is can deserialize to a custom unit + // if typeToConvert is one of the registered custom type quantities + var converter = (JsonConverter)Activator.CreateInstance(converterType, _baseConverter); + + return converter; + } + } +} diff --git a/UnitsNet.Serialization/IValueUnit.cs b/UnitsNet.Serialization/IValueUnit.cs new file mode 100644 index 0000000000..e512988440 --- /dev/null +++ b/UnitsNet.Serialization/IValueUnit.cs @@ -0,0 +1,43 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +namespace UnitsNet.Serialization +{ + /// + /// A structure used to serialize/deserialize Units.NET unit instances. + /// + public interface IValueUnit + { + /// + /// The unit of the value. + /// + /// MassUnit.Pound + /// InformationUnit.Kilobyte + public string Unit { get; } + + /// + /// The value. + /// + public double Value { get; } + } + + /// + /// A structure used to serialize/deserialize non-double Units.NET unit instances. + /// + /// + /// This type was added for lossless serialization of quantities with values. + /// The type distinguishes between 100 and 100.00 but Json.NET does not, therefore we serialize decimal values as string. + /// + public interface IExtendedValueUnit: IValueUnit + { + /// + /// The value as a string. + /// + public string ValueString { get; } + + /// + /// The type of the value, e.g. "decimal". + /// + public string ValueType { get; } + } +} diff --git a/UnitsNet.Serialization/QuantityConverter.cs b/UnitsNet.Serialization/QuantityConverter.cs new file mode 100644 index 0000000000..e19aef5f0d --- /dev/null +++ b/UnitsNet.Serialization/QuantityConverter.cs @@ -0,0 +1,149 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using System; +using System.Collections.Concurrent; +using System.Globalization; + +namespace UnitsNet.Serialization +{ + /// + /// Common converter functionality that can be used from both Json.Net and System.Text.Json converters + /// + public class QuantityConverter + { + private readonly 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(IQuantity).IsAssignableFrom(quantity)) + { + throw new ArgumentException($"The type {quantity} is not a {typeof(IQuantity)}"); + } + + if (!typeof(Enum).IsAssignableFrom(unit)) + { + throw new ArgumentException($"The type {unit} is not a {nameof(Enum)}"); + } + + _registeredTypes[unit.Name] = (quantity, unit); + } + + /// + /// Signature of function that creates an . + /// + public delegate IValueUnit CreateValueUnitFunc(string unit, double value); + + /// + /// Signature of function that creates an . + /// + public delegate IExtendedValueUnit CreateExtendedValueUnitFunc(string unit, double value, string valueString, string valueType); + + /// + /// Copied from UnitsNet.Serialization.JsonNet UnitsNetBaseJsonConverter to be used with System.Text.Json + /// + public IValueUnit ConvertIQuantity(IQuantity quantity, CreateValueUnitFunc createValueUnit, CreateExtendedValueUnitFunc createExtendedValueUnit) + { + quantity = quantity ?? throw new ArgumentNullException(nameof(quantity)); + + if (quantity is IDecimalQuantity d) + { + return createExtendedValueUnit( + unit: $"{quantity.QuantityInfo.UnitType.Name}.{quantity.Unit}", + value: quantity.Value, + valueString: d.Value.ToString(CultureInfo.InvariantCulture), + valueType: "decimal"); + } + + return createValueUnit(value: quantity.Value, unit: $"{quantity.QuantityInfo.UnitType.Name}.{quantity.Unit}"); + } + + /// + /// Convert a to a + /// + /// The value unit to convert + /// Thrown when an invalid Unit has been provided + /// An IQuantity + public IQuantity ConvertValueUnit(IValueUnit valueUnit) + { + if (string.IsNullOrWhiteSpace(valueUnit?.Unit)) + { + return default(IQuantity); + } + + var unit = GetUnit(valueUnit.Unit); + var registeredQuantity = GetRegisteredType(valueUnit.Unit).Quantity; + + if (registeredQuantity is not null) + { + return (IQuantity)Activator.CreateInstance(registeredQuantity, valueUnit.Value, unit); + } + + return valueUnit switch + { + IExtendedValueUnit {ValueType: "decimal"} extendedValueUnit => Quantity.From(decimal.Parse(extendedValueUnit.ValueString, CultureInfo.InvariantCulture), unit), + _ => Quantity.From(valueUnit.Value, unit) + }; + } + + + private (Type Quantity, Type Unit) GetRegisteredType(string unit) + { + (var unitEnumTypeName, var _) = SplitUnitString(unit); + if (_registeredTypes.TryGetValue(unitEnumTypeName, out var registeredType)) + { + return registeredType; + } + + return (null, null); + } + + private Enum GetUnit(string unit) + { + (var unitEnumTypeName, var unitEnumValue) = SplitUnitString(unit); + + // First try to find the name in the list of registered types. + Type unitEnumType = GetRegisteredType(unit).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 (string EnumName, string EnumValue) SplitUnitString(string unit) + { + var unitParts = unit.Split('.'); + + if (unitParts.Length != 2) + { + var ex = new UnitsNetException($"\"{unit}\" is not a valid unit."); + ex.Data["type"] = unit; + throw ex; + } + + // "MassUnit.Kilogram" => "MassUnit" and "Kilogram" + return (unitParts[0], unitParts[1]); + } + } +} diff --git a/UnitsNet.Serialization/UnitsNet.Serialization.csproj b/UnitsNet.Serialization/UnitsNet.Serialization.csproj new file mode 100644 index 0000000000..838d76b7a8 --- /dev/null +++ b/UnitsNet.Serialization/UnitsNet.Serialization.csproj @@ -0,0 +1,59 @@ + + + + UnitsNet.Serialization + 4.4.0 + Andreas Gullberg Larsen + Units.NET Serialization with Json.NET + A helper library for serializing and deserializing types in Units.NET using Json.NET. + Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). + true + https://github.com/angularsen/UnitsNet + logo-32.png + https://raw.githubusercontent.com/angularsen/UnitsNet/ce85185429be345d77eb2ce09c99d59cc9ab8aed/Docs/Images/logo-32.png + https://github.com/angularsen/UnitsNet + MIT + false + unit units measurement json Json.NET Newtonsoft serialize deserialize serialization deserialization + Upgrade JSON.NET to 12.0.3. Support arrays. + + + + + 4.0.0.0 + latest + UnitsNet.Serialization + netstandard2.0;net40 + + + + + true + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + ../UnitsNet.snk + false + true + + + + + + + + + + + + + + + + + + + diff --git a/UnitsNet.sln b/UnitsNet.sln index 4e089aca8d..77b8da40f6 100644 --- a/UnitsNet.sln +++ b/UnitsNet.sln @@ -44,6 +44,12 @@ ProjectSection(SolutionItems) = preProject UnitsNet.snk = UnitsNet.snk EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitsNet.Serialization", "UnitsNet.Serialization\UnitsNet.Serialization.csproj", "{9800D74F-5A10-4E36-89E3-4C9EE112413F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitsNet.Serialization.SystemTextJson", "UnitsNet.Serialization.SystemTextJson\UnitsNet.Serialization.SystemTextJson.csproj", "{95341193-0C5F-4AEA-9705-801872D6F19A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitsNet.Serialization.SystemTextJson.Tests", "UnitsNet.Serialization.SystemTextJson.Tests\UnitsNet.Serialization.SystemTextJson.Tests.csproj", "{F313EC26-748F-440E-8F7E-698C4DA45953}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +92,18 @@ Global {B4996AF5-9A8B-481A-9018-EC7F5B1605FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {B4996AF5-9A8B-481A-9018-EC7F5B1605FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {B4996AF5-9A8B-481A-9018-EC7F5B1605FF}.Release|Any CPU.Build.0 = Release|Any CPU + {9800D74F-5A10-4E36-89E3-4C9EE112413F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9800D74F-5A10-4E36-89E3-4C9EE112413F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9800D74F-5A10-4E36-89E3-4C9EE112413F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9800D74F-5A10-4E36-89E3-4C9EE112413F}.Release|Any CPU.Build.0 = Release|Any CPU + {95341193-0C5F-4AEA-9705-801872D6F19A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95341193-0C5F-4AEA-9705-801872D6F19A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95341193-0C5F-4AEA-9705-801872D6F19A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95341193-0C5F-4AEA-9705-801872D6F19A}.Release|Any CPU.Build.0 = Release|Any CPU + {F313EC26-748F-440E-8F7E-698C4DA45953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F313EC26-748F-440E-8F7E-698C4DA45953}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F313EC26-748F-440E-8F7E-698C4DA45953}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F313EC26-748F-440E-8F7E-698C4DA45953}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE