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

Added DataContract annotations to all Quantities {Value, Unit} #907

Closed
wants to merge 8 commits into from

Conversation

lipchev
Copy link
Collaborator

@lipchev lipchev commented Mar 8, 2021

..also

Finally here is some background info on how you can setup data contract surrogates as a service behavior (pretty much the same way you fix the nasty date format, only they've removed the property, along with the whole interface after net48).

- added tests covering the default DataContractSerializer (xml) & DataContractJsonSerializer (json)
- added DataContractSurrogates (n/a in .netcore) that can be used in the same way as the JsonConverter (i.e. offer an alternative json representation):
a) GenericQuantityDataContractSurrogate: {"1.20", "InformationUnit.Exabyte"}  // handles the double/decimals automatically
b) ExtendedQuantityDataContractSurrogate: {"1.2", "InformationUnit.Exabyte", ""1.20", "decimal"}  // same format as the one used with the UnitsNetIQuantityJsonConverter
c) BasicQuantityContractSurrogate: {"1.2", "InformationUnit.Exabyte"}  // same format as the one used with the UnitsNetJsonConverter (obsolete)
d) QuantityWithAbbreviationContractSurrogate: {"1", "kg", "Mass"} // can be used in conjunction with a much wider range of clients (also handles the double/decimal variations)
- added tests (mostly shared) for each of these
- added two-way compatibilty tests with both UnitsNetJsonConverter & UnitsNetIQuantityJsonConverter (in their respective test-projects- only applicable to net48, which I had to include)
@lipchev
Copy link
Collaborator Author

lipchev commented Mar 9, 2021

Right, so I did a couple of tests and turns out that adding a second framework to any of the tests projects causes the build to freeze (tested on the main repository).
I guess I'll move the Surrogate tests to a new dedicated project (say, UnitsNet.Serialization.DataContract.Tests). And then another one for the compatibility tests- *DataContract.Compatability.JsonNet.Tests- both targeting net48 (or earlier if we want)..
Unless of course you have some other suggestion- in truth the data contract surrogates may arguably be outside the scope of the project altogether- but I would argue that leaving them out could induce many people to simply opt in for the simpler solution- that is just use the default DataContractJsonSerializer's schema, which would work- but would be incompatible with the one produced by the JsonNet nuget.

- moved all Serialization.DataContract.Compatibility.Json tests to another new project (net48)
- added missing tests for the BasicQuantityContractSurrogate
@codecov
Copy link

codecov bot commented Mar 9, 2021

Codecov Report

Merging #907 (d871f69) into master (3941f35) will increase coverage by 82.7%.
The diff coverage is 0.0%.

❗ Current head d871f69 differs from pull request most recent head a7e0bc6. Consider uploading reports for the commit a7e0bc6 to get more accurate results
Impacted file tree graph

@@            Coverage Diff            @@
##           master    #907      +/-   ##
=========================================
+ Coverage        0   82.7%   +82.7%     
=========================================
  Files           0     294     +294     
  Lines           0   44039   +44039     
=========================================
+ Hits            0   36403   +36403     
- Misses          0    7636    +7636     
Impacted Files Coverage Δ
...nitsNet/GeneratedCode/Quantities/Acceleration.g.cs 82.6% <ø> (ø)
...et/GeneratedCode/Quantities/AmountOfSubstance.g.cs 82.9% <ø> (ø)
...tsNet/GeneratedCode/Quantities/AmplitudeRatio.g.cs 77.8% <ø> (ø)
UnitsNet/GeneratedCode/Quantities/Angle.g.cs 82.1% <ø> (ø)
...tsNet/GeneratedCode/Quantities/ApparentEnergy.g.cs 77.3% <ø> (ø)
...itsNet/GeneratedCode/Quantities/ApparentPower.g.cs 77.8% <ø> (ø)
UnitsNet/GeneratedCode/Quantities/Area.g.cs 84.5% <ø> (ø)
UnitsNet/GeneratedCode/Quantities/AreaDensity.g.cs 75.0% <ø> (ø)
.../GeneratedCode/Quantities/AreaMomentOfInertia.g.cs 79.5% <ø> (ø)
UnitsNet/GeneratedCode/Quantities/BitRate.g.cs 85.5% <ø> (ø)
... and 393 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3941f35...a7e0bc6. Read the comment docs.

…alization of double/decimal lists by using the IEnumerable interface
@stale
Copy link

stale bot commented Jun 2, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Jun 2, 2021
@lipchev lipchev removed the wontfix label Jun 2, 2021
@lipchev
Copy link
Collaborator Author

lipchev commented Jun 2, 2021

Hi,
This has been up for a while- and while there aren't any stoppers for me in here- I think at least the DataContract annotations would be nice-to-have (unless I'm mistaken - the previously mentioned issues regarding the between-version compatibility have been addressed). I think those should be plug-and-play for your standard WCF communication (not requiring interoperability with json.net).

As for the Surrogates - I really don't like how they introduced all the conditionals (which I failed to properly configure on the project level) - but I had no idea they were removed in core... Anyway- since they don't add any dependencies - I figured I'd keep them, allowing me to have the json-serializers-compatibility-tests (as a kind of a baseline contract for whatever other serialization library support we add- like System.Text.Json).

If you want- I can remove the Surrogates / Compatibility Tests (leaving only the 'default' DataContract xml/json serialization/deserialization tests).

Honestly, my goal was to make another PR after this one introducing a json.net Converter using the abbreviation-based schema (which I personally prefer, and regardless of preference, would be forced to use soon):

d) QuantityWithAbbreviationContractSurrogate: {"1", "kg", "Mass"}

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.

I think this is solid work 👏

Although WCF data contracts are no longer part of .NET future, there are tons of projects still on .NET 4 and on WCF stack for sure. I'm positive to include this for those targeting net40 of UnitsNet nuget.

Just some minor observations. I only skimmed the implementation and tests, but it looks really good to me. I trust that you who actually will be consuming this will be in a good position to verify it works as intended and it's easy to consume.

One thing, the README serialization section should be updated to reflect the new data contract support and the surrogates we provide out of the box. You might want to link to a wiki page if it gets too detailed to include on the front page.

Update:
Oh, and yeah, did you merge in latest master yet? Test projects have been upgraded to net5.0 recently.

/// The quantity type identifier.
/// </summary>
[DataMember(Order = 3)]
public TQuantity QuantityType { get; protected set; }
Copy link
Owner

Choose a reason for hiding this comment

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

Codecov points out some of these data contract members may be lacking test coverage. If that is correct, I think we should add some basic serialization tests for these.

@stale
Copy link

stale bot commented Aug 3, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Aug 3, 2021
@stale stale bot closed this Aug 10, 2021
@lipchev lipchev reopened this Sep 11, 2021
@stale stale bot removed the wontfix label Sep 11, 2021
@lipchev
Copy link
Collaborator Author

lipchev commented Sep 12, 2021

Ok here's a cheat-sheet of all the serialization schemas currently supported: the last 3 I have stashed locally as I wasn't sure which one(s) to select. Note that both json serializers can handle quoted numbers (so do System.Text), and in fact- this appears to be the only way for handling decimals with JsonNet (and possibly for System.Text as well). No out-of-the-box support in protobuf either :)

Name Dependency Schema Description Decimal Output Decimal Input Arrays IEnumerable Tuples 1.2 mg 1.20 mW
DefaultDataContractJsonSerializer None XML The default WCF serializer Supported Supported Supported Supported Supported <Mass xmlns="http://schemas.datacontract.org/2004/07/UnitsNet"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Value>1.2</Value>
<Unit>Milligram</Unit>
</Mass>
<Power xmlns="http://schemas.datacontract.org/2004/07/UnitsNet"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Value>1.20</Value>
<Unit>Milliwatt</Unit>
</Power>
DataContractJsonSerializer None {Number, Integer} The default WCF json serializer Supported Supported Supported Supported Supported {"Value":1.2,"Unit":16} {"Value":1.20,"Unit":20}
BasicQuantityContractSurrogate .Net Framework {Double, Enum} The default DataContractJsonSerializer configured with the BasicQuantityContractSurrogate: double precision with string-based unit serialization (e.g. MassUnit.Milligram) Double Precision Double Precision Supported Supported Supported {"Value":1.2,"Unit":"MassUnit.Milligram"} {"Value":1.2,"Unit":"PowerUnit.Milliwatt"}
GenericQuantityDataContractSurrogate .Net Framework {Number, Enum} The default DataContractJsonSerializer configured with the GenericQuantityDataContractSurrogate: double/decimal precision with string-based unit serialization (e.g. MassUnit.Milligram) Supported Supported Supported Supported Supported {"Value":1.2,"Unit":"MassUnit.Milligram"} {"Value":1.20,"Unit":"PowerUnit.Milliwatt"}
ExtendedQuantityDataContractSurrogate .Net Framework {Double, Enum, Decimal, Type} The default DataContractJsonSerializer configured with the ExtendedQuantityDataContractSurrogate: custom (e.g. decimal) values types stored as quoted numbers (two additional fields) Supported Supported Supported Supported Supported {"Value":1.2,"Unit":"MassUnit.Milligram"} {"Value":1.2,"Unit":"PowerUnit.Milliwatt","ValueString":"1.20","ValueType":"decimal"}
QuantityWithAbbreviationContractSurrogate .Net Framework {Number, Abbreviation, Type} The default DataContractJsonSerializer configured with the QuantityWithAbbreviationContractSurrogate: double/decimal values with quantity type and unit abbreviation Supported Supported Supported Supported Supported {"Value":1.2,"Unit":"mg","QuantityType":"Mass"} {"Value":1.20,"Unit":"mW","QuantityType":"Power"}
UnitsNetIQuantityJsonConverter JsonNet {Double, Enum, Decimal, Type} The currently recommended UnitsNet.JsonNet converter: decimals supported using the extended schema Supported Supported Supported Supported Not Supported {"Unit":"MassUnit.Milligram","Value":1.2} {"Unit":"PowerUnit.Milliwatt","Value":1.2,"ValueString":"1.20","ValueType":"decimal"}
UnitsNetJsonConverter JsonNet {Double, Enum} The obsolete UnitsNet.JsonConverter Double Precision Double Precision Not Supported Not Supported Not Supported {Unit":"MassUnit.Milligram","Value":1.2} {Unit":"PowerUnit.Milliwatt","Value":1.2}
GenericQuantityJsonConverter JsonNet {Number, Enum} Base on the generic QuantityValueContract: double/decimal precision with string-based unit serialization (e.g. MassUnit.Milligram) Supported Double Precision Supported Supported Not Supported {"Value":1.2,"Unit":"MassUnit.Milligram"} {"Value":1.20,"Unit":"PowerUnit.Milliwatt"}
QuantityWithAbbreviationJsonConverter JsonNet {Double, Abbreviation, Type} Double values with quantity type and unit abbreviation Double Precision Double Precision Supported Supported Not Supported {"Value":1.2,"Unit":"mg","QuantityType":"Mass"} {"Value":1.2,"Unit":"mW","QuantityType":"Power"}
PreciseQuantityWithAbbreviationJsonConverter(quoted:false) JsonNet {Number, Abbreviation, Type} Serialization preserves the correct precision; unquoted values converted to double during deserialization Supported Double Precision Supported Supported Not Supported {"Value":1.2,"Unit":"mg","QuantityType":"Mass"} {"Value":1.20,"Unit":"mW","QuantityType":"Power"}
PreciseQuantityWithAbbreviationJsonConverter(quoted:true) JsonNet {Number, Abbreviation, Type} Decimal values serialized as string (quoted numbers are supported by both JsonNet & the DataContractJsonSerializer) Supported Supported Supported Supported Not Supported {"Value":1.2,"Unit":"mg","QuantityType":"Mass"} {"Value":"1.20","Unit":"mW","QuantityType":"Power"}

@angularsen
Copy link
Owner

That's one impressive list @lipchev 😅

If decimals must be represented as strings, then I suppose it is simpler and more consistent to serialize all quantity values regardless of number type.

Beyond that, let me know when this is getting ready to review or if you are waiting for feedback on anything specific 👍

@lipchev
Copy link
Collaborator Author

lipchev commented Sep 15, 2021

If decimals must be represented as strings, then I suppose it is simpler and more consistent to serialize all quantity values regardless of number type.

I did a quick test today with the System.Text serializer and it looks like it is correctly handling the unquote decimal precision. That means that JsonNet is the only one that actually requires the use of strings (and the additional properties of the ExtendedContract).
I would therefore suggest that we recommend the use of the following two schemas:

  1. For use with applications referencing UnitsNet: QuantityValueContract= {"Value":1.20,"Unit":"PowerUnit.Milliwatt"}
  2. For use within a broader environment: QuantityWithAbbreviationContract = {"Value":1.20,"Unit":"mW","QuantityType":"Power"}

These are fully supported by the GenericQuantityDataContractSurrogate and QuantityWithAbbreviationContractSurrogate, and will be natively supported the (future) System.Text converter.
In both cases quoted strings are accepted as input - but the output is always a Number: e.g. 1.20.

For JsonNet users there are two scenarios w.r.t. the value type:

  1. The precision (number of zeros) associated with the decimal is not important- use the GenericQuantityJsonConverter (using the generic QuantityValueContract<double | decimal, string>) or the version currently named PreciseQuantityWithAbbreviationJsonConverter(quoted:false)
  2. Preserving the exact precision during de-serialization is important (you've probably got some custom quantity implementation, as most UnitsNet operations are double-based): you can use the UnitsNetIQuantityJsonConverter (the current implementation using the extended format), PreciseQuantityJsonConverter (serialized using the QuantityValueContract<double | string, string>) or the PreciseQuantityWithAbbreviationJsonConverter(quoted:true)

Unless there is some significant difference in performance between the UnitsNetIQuantityJsonConverter and the (not yet implemented version) of the PreciseQuantityJsonConverter, I think I would favor the latter.

Finally, for people only interested in the IQuantity (double) interface it is trivial to provide slightly faster (and cleaner) implementations: e.g. BasicQuantityContractSurrogate & QuantityWithAbbreviationJsonConverter that completely ignore the IDecimalQuantity interface, but I'm not sure if it is actually worth the extra bytes (unless of course, you think bytes are fun).

So the main question is - HowMany converter-versions do we provide?

Bonus question: what do you think about "QuantityType" as property name- maybe we should use something shorter?

@angularsen
Copy link
Owner

angularsen commented Sep 15, 2021

First, I need to understand something. Is this PR primarily about supporting WCF xml + json?

System.text.json and json.net is discussed here also.

There is #966, won't that cover system.text.json?
Is the idea here to also support system.text.json via data contracts?
If so, should we still go ahead with custom json converters in #966?

I think I would favor the latter
Finally, for people only interested in the IQuantity (double) interface it is trivial to provide slightly faster (and cleaner) implementations:

Yes! The fewest number of options that we absolutely need to provide, please.

@lipchev
Copy link
Collaborator Author

lipchev commented Sep 15, 2021

We are going about it backwards: from the existing schemas (implementations of JsonNet) that are out there we've got the corresponding DataContracts (1:1 mapping of a particular schema). Those can then be used by any of the serializers/converters (e.g. ValueUnit is the QuantityValueContract. Same for the ExtendedValueUnit and ExtendedQuantityValueContract. #966 is also based on the ExtendedQuantityValue[Contract]. Those can both be refactored using the common (generic) contract- but that is not the point of this PR.

The point was to add the [DataContract] annotations and to provide reference schema(s), implemented (as an example) by the DataContractSurrogates. At the same time, I was making a point about having both an internal and external schema (our Delphi application already has a table with standard units and conversions, but knows nothing about MassUnit.Milligram).

The problem is then further divided into two parts by the (JsonNet's) existing ExtendedQuantityValue schema- which I argue should be deprecated in favor of the GenericQuantityDataContract schema (with the unquoted number- example implementation with GenericQuantityDataContractSurrogate).

While this PR should be good to go as it (not sure I finally managed to make codecov happy- but I think things are otherwise covered), I started work on the JsonNet-based implementations of the QuantityWithAbbreviationSchema (QuantityWithAbbreviationJsonConverter and PreciseQuantityWithAbbreviationJsonConverter)- trying out the capabilities of the different serializers (producing the cheat-sheet).

The two implementations above are ready- and mostly covered, but I haven't pushed them as they add to the existing JsonConverter/Tests projects. If you would rather have it all in one- I can push them as well (giving the PR some broader name)...

@lipchev
Copy link
Collaborator Author

lipchev commented Sep 15, 2021

If so, should we still go ahead with custom json converters in #966?

That is my question as well- we can easily have both the Generic & Extended schemas implemented- but should we and which one would you recommend?

@angularsen angularsen self-requested a review September 17, 2021 21:38
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="nanoFramework.CoreLibrary" version="1.10.4-preview.11" targetFramework="netnanoframework10" />
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think this PR should have to touch these nanoframework files?
Make sure you have merged in latest origin/master and then maybe revert these.

Copy link
Owner

@angularsen angularsen Sep 17, 2021

Choose a reason for hiding this comment

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

That is my question as well- we can easily have both the Generic & Extended schemas implemented- but should we and which one would you recommend?

I don't like the extended schema. If some quantities need quoted numbers, then I think it would be simpler to just quote all numbers.

I have to admit I am struggling to grasp all the contracts, and surrogates and converters here and in #966.
That table alone is mind-boggling to put it mildly 😅 so bear with me if I am not quite getting it.

How about these two contracts:

  • Shape A: Quoted number + full enum name
  • Shape B: Quoted number + unit abbreviation

It could then look like this:

  • WCF and its DataContractSerializer (xml) or DataContractJsonSerializer (json)
    • Shape A: GenericQuantityDataContractSurrogate
    • Shape B: QuantityWithAbbreviationContractSurrogate
  • Json.NET and its JsonConverter
    • Shape A: GenericQuantityJsonConverter (but with quoted numbers)
    • Shape B: QuantityWithAbbreviationJsonConverter (but with quoted numbers)
  • System.Text.Json and its JsonSerializer
  • Protobuf and other data contract compatible serializers
    • I assume similar converters as above must be created for each? That pesky decimal seems to be the main problem.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Those got auto-incremented on the last commit (after running the build script)- I didn't follow much of the discussion around the nano-framework, so I thought this was intentional.

Copy link
Owner

Choose a reason for hiding this comment

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

Yes, sorry, that was a flaw in the setup earlier. It should have been fixed by now.

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've updated the summary:

  1. From what I gather there should be no backwards support for the Basic & Extended schemas (will remove the corresponding Surrogates).
  2. All values should be quoted: will update the contracts & tests
  3. This would break the compatibility tests- so those should be removed as well
  4. Should I add the new JsonNet converters? System.Text?
  5. If (4) then should I keep the DC <-> JsonNet compatibility tests? Maybe change them something about them? I first wanted to make it work with strings and quantities- but the format is never exactly the same : there is a difference in serializing slashes- like in "mg/l", so I went with the serialize-with-one -deserialize-with-another workflow..
  6. Nano packages should be reverted

Copy link
Owner

@angularsen angularsen Sep 17, 2021

Choose a reason for hiding this comment

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

  1. I think breaking backwards compatibility for new contracts/shapes is fine.
  2. Yes
  3. Yes
  4. json.net and system.text converters can be added in separate PRs, if it makes sense to scope this PR can to only the data contracts. You decide. Also I don't know PR 966 well enough, if you should reuse that work or not.
  5. If I understand correctly, Shape A and Shape B should have compatible JSON formats regardless of DC, JsonNet or SystemText being used. If so, then yes, compatibility tests sounds useful to keep.
  6. Yes

@lipchev lipchev deleted the data-contract-annotations branch October 1, 2021 23:25
@lipchev
Copy link
Collaborator Author

lipchev commented Oct 1, 2021

I'm going to start this over: this time without the DataContractSurrogates - there is simply too much overhead (with the dependency to .NET Framework) - and I can't imagine anybody is ever going to upgrade to that particular setup.
Otherwise, I've been testing the [DataContract]s along with the basic abbreviation schema (unquoted) for the past few weeks and so far it's going pretty smoothly. A ton of auto-mapping code went away.

@angularsen
Copy link
Owner

@lipchev Ok, sounds good to me. It did feel a bit overwhelming with all different ways to serialize, and I believe surrogates and WCF is largely a thing of the past. I can imagine a very small minority would be interested in that.

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