Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<UnitsNetException>(() => 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<UnitsNetException>(() => 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()
{
Expand Down Expand Up @@ -226,5 +260,32 @@ private class TestConverter : UnitsNetBaseJsonConverter<string>
return (result.Unit, result.Value);
}
}

private class TestQuantityConverter : UnitsNetBaseJsonConverter<IQuantity>
{
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();
}
}
}
33 changes: 29 additions & 4 deletions UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ namespace UnitsNet.Serialization.JsonNet
#endif
public abstract class UnitsNetBaseJsonConverter<T> : NullableQuantityConverter<T>
{
private const string RegisteredQuantityInstantiationFailedErrorCode = "JsonNetRegisteredQuantityInstantiationFailed";

private readonly ConcurrentDictionary<string, (Type Quantity, Type Unit)> _registeredTypes = new();

/// <summary>
/// Register custom types so that the converter can instantiate these quantities.
/// Instead of calling <see cref="Quantity.From(double,UnitKey)"/>, the <see cref="Activator"/> will be used to instantiate the object.
/// It is therefore assumed that the constructor of <paramref name="quantity"/> is specified with <c>new T(double value, typeof(<paramref name="unit"/>) unit)</c>.
/// Instead of calling <see cref="Quantity.From(double,UnitKey)"/>, <see cref="Activator.CreateInstance(Type, object?[])"/> will be used to instantiate the object.
/// It is therefore assumed that <paramref name="quantity"/> has a public constructor that accepts <c>(double value, <paramref name="unit"/> unit)</c>.
/// Registering the same <paramref name="unit"/> multiple times, it will overwrite the one registered.
/// </summary>
public void RegisterCustomType(Type quantity, Type unit)
Expand Down Expand Up @@ -92,16 +94,39 @@ 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)!;
try
{
IQuantity? instance = (IQuantity?)Activator.CreateInstance(registeredQuantity, valueUnit.Value, unit);
return instance ?? throw CreateRegisteredQuantityInstantiationException(registeredQuantity, registeredType.Unit ?? unit.GetType(), unit);
}
catch (Exception ex) when (!IsRegisteredQuantityInstantiationException(ex))
{
throw CreateRegisteredQuantityInstantiationException(registeredQuantity, registeredType.Unit ?? unit.GetType(), 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);
Expand Down
5 changes: 5 additions & 0 deletions UnitsNet/Exceptions/UnitsNetException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ namespace UnitsNet
/// </summary>
public class UnitsNetException : Exception
{
/// <summary>
/// Key used in <see cref="Exception.Data" /> for stable UnitsNet error codes.
/// </summary>
public const string ErrorCodeDataKey = "errorCode";

/// <inheritdoc />
public UnitsNetException()
{
Expand Down
Loading