Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add [DataContract] and [DataMember] annotations #972

Merged
merged 3 commits into from
Oct 17, 2021

Conversation

lipchev
Copy link
Collaborator

@lipchev lipchev commented Oct 2, 2021

  • added to all quantities and their respective fields (Unit, Value)
  • added a set of serialization tests (common to all serializers)
  • added tests for the DataContractSerializer (xml) and DataContractJsonSerializer (default settings)

Related to #907

…s) to all quantities

- added a set of serialization tests (common to all serializers)
- added tests for the DataContractSerializer (xml) and DataContractJsonSerializer (default settings)
.. and comments
@lipchev lipchev requested a review from angularsen October 11, 2021 21:46
@angularsen
Copy link
Owner

Will try to get to this soon.

Copy link
Owner

@angularsen angularsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial questions

public void DecimalQuantity_SerializedWithDecimalValueValueAndIntegerUnit()
{
var quantity = new Information(1.20m, InformationUnit.Exabyte);
var expectedJson = "{\"Value\":1.20,\"Unit\":4}";
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep forgetting, but wasn't there some good reason to use quoted numbers when serializing?

At first I thought json could not represent double.MaxValue or decimal.MaxValue, but it seems just fine representing both of these in a round-trip.

I tried a quick example in linqpad:

void Main()
{
	//decimal smallestDecimal = (decimal) (1 / Math.Pow(10, 28));
	//double smallestDouble = double.Epsilon;
	//var foo = new Foo(smallestDouble, smallestDecimal);

	var foo = new Foo(double.MaxValue, decimal.MaxValue);
	var systemTextJson = System.Text.Json.JsonSerializer.Serialize(foo).Dump("system.text.json");
	var newtonsoftJson = JsonConvert.SerializeObject(foo).Dump("newtonsoft");
	
	var systemTextFoo =  System.Text.Json.JsonSerializer.Deserialize<Foo>(systemTextJson).Dump("systemTextDeser");
	var newtonsoftFoo = JsonConvert.DeserializeObject<Foo>(newtonsoftJson).Dump("newtonsoftDeser");
	systemTextFoo.Equals(foo).Dump("systemTextDeser.Equals(original)");
	newtonsoftFoo.Equals(foo).Dump("newtonsoftDeser.Equals(original)");	
}

public record Foo(double MyDouble, decimal MyDecimal);

Result:
image

Similarly, if I instead of MaxValue chose the smallest number of each, it still roundtrips just fine for both Newtonsoft and System.Text.Json.

	decimal smallestDecimal = (decimal) (1 / Math.Pow(10, 28));
	double smallestDouble = double.Epsilon;
	var foo = new Foo(smallestDouble, smallestDecimal);

image

So. I can't really remember why quoted strings were used to begin with? Is it maybe not necessary after all? Do you remember?

This is the one that introduced it, but I don't really see a description of the problem it solved:
#868

Serialize decimal values as string to keep number of decimal places

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this relates to the scale being lost while de-serializing using Newtonsoft - you could see it in the debugger (or in you example of ****0335.0 - that's a double right there). At the same time - this doesn't happen during serialization - the exact scale is preserved.

After doing the initial round of tests- I actually stumbled upon an option that might be able to resolve this issue- but haven't had time to actually figure out what it does: FloatParseHandling.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also an option for the special floating point numbers, e.g. NaN, PositiveInfinity and NegativeInfinity - however given that none of those are really supported by UnitsNet (the Guard aggressively protecting against it) I don't think this would have been a reason to use the string representation for all numbers.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just trying to understand better, so here is another example, now using our json.net serializer and schema:

void Main()
{
	decimal smallestDecimal = (decimal) (1 / Math.Pow(10, 28));
	
	var foo = new Foo(
		LargestDecimal:  new BitRate(decimal.MaxValue, BitRateUnit.BitPerSecond),
		SmallestDecimal: new BitRate(smallestDecimal, BitRateUnit.BitPerSecond),		
		LargestDouble:   new Length(double.MaxValue, LengthUnit.Meter),
		SmallestDouble:  new Length(double.Epsilon, LengthUnit.Meter)
	).Dump("foo");

	var newtonJson = JsonConvert.SerializeObject(foo, Newtonsoft.Json.Formatting.Indented, new UnitsNetIQuantityJsonConverter()).Dump("newtonJson");
	//System.Text.Json.JsonSerializer.Serialize(foo).Dump("systemJson"); // throws cyclical exception
	
	var newtonFoo = JsonConvert.DeserializeObject<Foo>(newtonJson, new UnitsNetIQuantityJsonConverter()).Dump("newtonFoo");
	newtonFoo.Equals(foo).Dump("newtonFoo.Equals(foo)");
}

public record Foo(IQuantity LargestDecimal, IQuantity SmallestDecimal, IQuantity LargestDouble, IQuantity SmallestDouble);

image

{
  "LargestDecimal": {
    "Unit": "BitRateUnit.BitPerSecond",
    "Value": 7.922816251426434E+28,
    "ValueString": "79228162514264337593543950335",
    "ValueType": "decimal"
  },
  "SmallestDecimal": {
    "Unit": "BitRateUnit.BitPerSecond",
    "Value": 1.0000000000000001E-28,
    "ValueString": "0.0000000000000000000000000001",
    "ValueType": "decimal"
  },
  "LargestDouble": {
    "Unit": "LengthUnit.Meter",
    "Value": 1.7976931348623157E+308
  },
  "SmallestDouble": {
    "Unit": "LengthUnit.Meter",
    "Value": 5E-324
  }
}

You wrote:

I think this relates to the scale being lost while de-serializing using Newtonsoft [...] 0335.0 - that's a double right there

I think you are right, but only if we were actually trying to deserialize back into the wrong number type:

  • double.MaxValue serialized to JSON and deserialized to a decimal property would probably result in OverflowException
  • decimal.MaxValue serialized to JSON and deserialized to a double property, would lose precision.

However, this shouldn't be a problem when serializing decimal and double quantities if our converters do their job.

What I am beginning to realize is that the problem is not JSON itself, nor is it SystemTextJson or NewtonsoftJson. It is simply that when deserializing, we need to know whether to deserialize to double or to decimal. And we do have this information encoded in our JSON schema. So that leads me to think that the ValueString property in our schema is really redundant. ValueType should be sufficient, shouldn't it?

If I am not entirely missing something, then bringing this line of thought back to this PR; should the data contract have a ValueType property to support deserializing decimal values without losing precision?
And, should we deprecate the ValueString property in our json.net schema and bump the major version with a breaking change to remove that redundant property?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion- the ValueType is redundant - it can easily be determined from the Unit (I'm checking the corresponding QuantityInfo.Zero).

Unfortunately, in the case of Newtonsoft- knowing the target doesn't help. Here's a fiddle for you to consider:
JToken.Load(new JsonTextReader(new StringReader("{\"DecimalValue\":1.20}"))).Value<decimal>("DecimalValue").Dump()

Dumping object(Decimal)
1.2

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All our converters use JToken.Load, but that might be because everybody was copying over from the previous implementation..
The documentation isn't much help either- the first example uses JToken for the WriteJson (without implementing a corresponding ReadJson !?).
The second one is just as useless (I'm not even sure what it is supposed to demonstrate) .

After browsing through the source code - it seems like the implementation of DeserializeObject avoids calling JToken/JObject.Load: it's going through a while(reader.Read()) loop, parsing each token it encounters (e.g. TokenType.StartObject -> TokenType.PropertyName ... -> TokenType.EndObject).

Following that logic, I managed to deserialize my DecimalValue- it isn't pretty, but I think we can manage..

Copy link
Owner

@angularsen angularsen Oct 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so to recap.

  • It is possible do deprecate ValueType and ValueString in our existing json.net schema, the converters should be able to do without since we can look up the number type from QuantityInfo.
  • This PR's data contract is already good, but, more changes are needed to support full precision of decimal?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR doesn't affect the existing code in any way... It's just about the DataContract and DataMember annotations- and the tests for the corresponding DataContract serializers (xml and json - but without any of the surrogate definitions)..
I sensed you were still in the spirit of our discussion in #907 but the point of starting over was to take it one step at a time. I was of course, going to propose an implementation for a JsonConverter but in another PR.
Other than that - yes you are correct, I think we can manage without the extra properties - and yes, my current implementation (not part of this PR) only supported decimals using the "quoted" numbers schema- and needs to be re-written (once again).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should however, take a close look at the tests- as they would probably be shared between all serializers (DC, Newtonsoft, System.Text and whatever else comes after..).

Copy link
Owner

@angularsen angularsen Oct 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I am being slow to grasp all the things, I keep forgetting details as time passes between each time I visit this topic. I think I'm once again getting the mental map decent and this PR looks ready to merge to me.

Just for my own summary:

The tests are excellent, really thorough 👏

@angularsen angularsen changed the title Added the [DataContract] and [DataMember] annotations Add [DataContract] and [DataMember] annotations Oct 17, 2021
@angularsen angularsen merged commit acae44e into angularsen:master Oct 17, 2021
@angularsen
Copy link
Owner

@angularsen angularsen mentioned this pull request Nov 29, 2022
angularsen added a commit that referenced this pull request Nov 29, 2022
Fixes #180 

Merging the v5 release branch.

It is still in alpha, but it is functional, nugets are published and there are not many planned breaking changes left.
By merging, all efforts moving forward are targeting v5 and this reduces friction:

- No more merge conflicts trying to forward port all changes to v5, instead cherry pick new units and fixes to v4 until v5 is fully stable.
- Contributors are having trouble building v4 locally due to `net40`, `net47` and Windows Runtime Component targets.


## 💥 Breaking changes
Default number format should be CultureInfo.CurrentCulture, not CurrentUICulture (#795)
Use CurrentCulture rather than CurrentUICulture (#986)
Return `QuantityValue` in `IQuantity` properties instead of `double` (#1074)
Return `decimal` in properties of `Power`, `BitRate` and `Information` quantities (#1074)
Fix singular name VolumeFlow.MillionUsGallonsPerDay

## 🔥 Removed 
Remove targets: net40, net47, Windows Runtime Component.
Remove `Undefined` enum value for all unit enum types
Remove QuantityType enum
Remove IQuantity.Units and .UnitNames
Remove IQuantity.ToString() overloads
Remove IEquatable<T> and equality operators/methods 
Remove GlobalConfiguration
Remove obsolete and deprecated code.
Remove Molarity ctor and operator overloads
Remove MinValue, MaxValue per quantity due to ambiguity
Remove string format specifiers: "v", "s"
json: Remove UnitsNetJsonConverter

## ✨ New
QuantityValue: Implement IEquality, IComparable, IFormattable
QuantityValue: 16 bytes instead of 40 bytes (#1084)
Add `[DataContract]` annotations (#972)

## ♻️ Improvements
Upgrade CodeGen, tests and sample apps to net6.0.

## 📝 JSON unit definition schema changes
Rename `BaseType` to `ValueType`, for values "double" and "decimal".
Rename `XmlDoc` to `XmlDocSummary`.

## TODO
Add back `IEquatable<T>`, but implement as strict equality with tuple of quantity name + unit + value.
#1017 (comment)

## Postponed for later
#1067
@lipchev lipchev deleted the data-contract-annotations branch December 16, 2024 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants