diff --git a/UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs b/UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs index b07bf62272..1d7a292464 100644 --- a/UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs +++ b/UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; +using UnitsNet.Tests.CustomQuantities; using Xunit; namespace UnitsNet.Serialization.JsonNet.Tests @@ -67,6 +68,39 @@ public void UnitsNetBaseJsonConverter_ConvertValueUnit_throws_UnitsNetException_ Assert.Equal("PowerUnit Watt", result.Data["type"]); } + [Fact] + public void UnitsNetBaseJsonConverter_ConvertValueUnit_throws_UnitsNetException_when_registered_quantity_cannot_be_instantiated() + { + var sut = new TestQuantityConverter(); + sut.RegisterCustomType(typeof(IQuantity), typeof(HowMuchUnit)); + + var result = Assert.Throws(() => sut.Test_ConvertValueUnit("HowMuchUnit.Some", 10.2365)); + + Assert.Contains("Unable to instantiate registered quantity type", result.Message); + Assert.Equal("JsonNetRegisteredQuantityInstantiationFailed", result.Data[UnitsNetException.ErrorCodeDataKey]); + Assert.Equal(typeof(IQuantity), result.Data["quantityType"]); + Assert.Equal(typeof(HowMuchUnit), result.Data["unitType"]); + Assert.Equal(HowMuchUnit.Some, result.Data["unit"]); + Assert.NotNull(result.InnerException); + } + + [Fact] + public void UnitsNetBaseJsonConverter_ConvertValueUnit_wraps_exception_when_registered_quantity_constructor_throws() + { + var sut = new TestQuantityConverter(); + sut.RegisterCustomType(typeof(ThrowingQuantity), typeof(HowMuchUnit)); + + var result = Assert.Throws(() => sut.Test_ConvertValueUnit("HowMuchUnit.Some", 10.2365)); + + Assert.Contains("Unable to instantiate registered quantity type", result.Message); + Assert.Equal("JsonNetRegisteredQuantityInstantiationFailed", result.Data[UnitsNetException.ErrorCodeDataKey]); + Assert.Equal(typeof(ThrowingQuantity), result.Data["quantityType"]); + Assert.Equal(typeof(HowMuchUnit), result.Data["unitType"]); + Assert.Equal(HowMuchUnit.Some, result.Data["unit"]); + Assert.NotNull(result.InnerException); + Assert.Contains("Thrown from registered quantity constructor.", result.InnerException.ToString()); + } + [Fact] public void UnitsNetBaseJsonConverter_CreateLocalSerializer_works_as_expected() { @@ -226,5 +260,32 @@ private class TestConverter : UnitsNetBaseJsonConverter return (result.Unit, result.Value); } } + + private class TestQuantityConverter : UnitsNetBaseJsonConverter + { + public override bool CanRead => false; + public override bool CanWrite => false; + public override void WriteJson(JsonWriter writer, IQuantity value, JsonSerializer serializer) => throw new NotImplementedException(); + public override IQuantity ReadJson(JsonReader reader, Type objectType, IQuantity existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException(); + + public IQuantity Test_ConvertValueUnit(string unit, double value) => ConvertValueUnit(new ValueUnit {Unit = unit, Value = value}); + } + + private class ThrowingQuantity : IQuantity + { + public ThrowingQuantity(double value, HowMuchUnit unit) + { + throw new UnitsNetException("Thrown from registered quantity constructor."); + } + + public QuantityInfo QuantityInfo => throw new NotImplementedException(); + public Enum Unit => throw new NotImplementedException(); + public double Value => throw new NotImplementedException(); + public UnitKey UnitKey => throw new NotImplementedException(); + public double As(Enum unit) => throw new NotImplementedException(); + public double As(UnitKey unitKey) => throw new NotImplementedException(); + public IQuantity ToUnit(Enum unit) => throw new NotImplementedException(); + public string ToString(string format, IFormatProvider formatProvider) => throw new NotImplementedException(); + } } } diff --git a/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs b/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs index d4f6053579..b9b5506791 100644 --- a/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs +++ b/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs @@ -21,12 +21,14 @@ namespace UnitsNet.Serialization.JsonNet #endif public abstract class UnitsNetBaseJsonConverter : NullableQuantityConverter { + private const string RegisteredQuantityInstantiationFailedErrorCode = "JsonNetRegisteredQuantityInstantiationFailed"; + 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). + /// Instead of calling , will be used to instantiate the object. + /// It is therefore assumed that has a public constructor that accepts (double value, unit). /// Registering the same multiple times, it will overwrite the one registered. /// public void RegisterCustomType(Type quantity, Type unit) @@ -82,7 +84,7 @@ public void RegisterCustomType(Type quantity, Type unit) /// Convert a to an /// /// The value unit to convert - /// Thrown when an invalid Unit has been provided + /// Thrown when an invalid unit has been provided, or when a registered custom quantity type cannot be instantiated. /// An IQuantity protected IQuantity ConvertValueUnit(ValueUnit valueUnit) { @@ -92,16 +94,41 @@ protected IQuantity ConvertValueUnit(ValueUnit valueUnit) } var unit = GetUnit(valueUnit.Unit); - var registeredQuantity = GetRegisteredType(valueUnit.Unit).Quantity; + var registeredType = GetRegisteredType(valueUnit.Unit); + var registeredQuantity = registeredType.Quantity; if (registeredQuantity is not null) { - return (IQuantity)Activator.CreateInstance(registeredQuantity, valueUnit.Value, unit)!; + // RegisterCustomType stores quantity/unit pairs, so a registered quantity has a registered unit. + Type registeredUnit = registeredType.Unit!; + try + { + IQuantity? instance = (IQuantity?)Activator.CreateInstance(registeredQuantity, valueUnit.Value, unit); + return instance ?? throw CreateRegisteredQuantityInstantiationException(registeredQuantity, registeredUnit, unit); + } + catch (Exception ex) when (!IsRegisteredQuantityInstantiationException(ex)) + { + throw CreateRegisteredQuantityInstantiationException(registeredQuantity, registeredUnit, unit, ex); + } } return Quantity.From(valueUnit.Value, unit); } + private static bool IsRegisteredQuantityInstantiationException(Exception ex) => + ex is UnitsNetException && Equals(ex.Data[UnitsNetException.ErrorCodeDataKey], RegisteredQuantityInstantiationFailedErrorCode); + + private static UnitsNetException CreateRegisteredQuantityInstantiationException(Type quantityType, Type unitType, Enum unit, Exception? innerException = null) + { + string message = $"Unable to instantiate registered quantity type \"{quantityType.FullName}\" for unit \"{unitType.FullName}.{unit}\". The converter expected a non-null quantity instance from a public constructor accepting (double value, {unitType.Name} unit)."; + var ex = innerException is null ? new UnitsNetException(message) : new UnitsNetException(message, innerException); + ex.Data[UnitsNetException.ErrorCodeDataKey] = RegisteredQuantityInstantiationFailedErrorCode; + ex.Data["quantityType"] = quantityType; + ex.Data["unitType"] = unitType; + ex.Data["unit"] = unit; + return ex; + } + private (Type? Quantity, Type? Unit) GetRegisteredType(string unit) { var (unitEnumTypeName, _) = SplitUnitString(unit); diff --git a/UnitsNet/Exceptions/UnitsNetException.cs b/UnitsNet/Exceptions/UnitsNetException.cs index a517b700c2..31236cb9ca 100644 --- a/UnitsNet/Exceptions/UnitsNetException.cs +++ b/UnitsNet/Exceptions/UnitsNetException.cs @@ -10,6 +10,11 @@ namespace UnitsNet /// public class UnitsNetException : Exception { + /// + /// Key used in for stable UnitsNet error codes. + /// + public const string ErrorCodeDataKey = "errorCode"; + /// public UnitsNetException() {