From 749ef6dbbd741dd8e94ea231678bed0cda10c20a Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp <5693174+tmilnthorp@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:57:51 -0400 Subject: [PATCH 1/2] Keep the netstandard2.0 public API available on .NET A library compiled against the netstandard2.0 assembly can run in an app that loads one of the .NET assemblies, where several public members were missing and calls failed with MissingMethodException: - IQuantityInfo and IQuantityInstanceInfo were internal on .NET - IQuantityOfType.QuantityInfo did not exist on .NET - The Temperature tolerance Equals overloads in AffineQuantityExtensions only existed on netstandard2.0 These now exist on all target frameworks. IQuantityOfType.QuantityInfo is obsolete on .NET like IQuantity.QuantityInfo, and is implemented once by IQuantity. Enable package validation to catch such differences in CI. The remaining suppressions are the static abstract members and generic math constraints, which require .NET 7 or later. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012XKhDsyHDc5BHrmibxScqG --- UnitsNet.Tests/CustomCode/IQuantityTests.cs | 17 +++++ UnitsNet/CompatibilitySuppressions.xml | 66 +++++++++++++++++++ .../Extensions/AffineQuantityExtensions.cs | 4 +- UnitsNet/IQuantity.cs | 11 +++- UnitsNet/IQuantityInfo.cs | 14 +--- UnitsNet/UnitsNet.csproj | 2 + 6 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 UnitsNet/CompatibilitySuppressions.xml diff --git a/UnitsNet.Tests/CustomCode/IQuantityTests.cs b/UnitsNet.Tests/CustomCode/IQuantityTests.cs index eb1a376d96..df207f33ee 100644 --- a/UnitsNet.Tests/CustomCode/IQuantityTests.cs +++ b/UnitsNet.Tests/CustomCode/IQuantityTests.cs @@ -2,6 +2,7 @@ // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System.Numerics; +using UnitsNet.Tests.CustomQuantities; namespace UnitsNet.Tests; @@ -265,6 +266,22 @@ public void GetQuantityInfo_Typed_ReturnsRegisteredQuantityInfo() Assert.Same(Mass.Info, info); } + [Fact] + public void QuantityInfo_IQuantityOfTypeReference_ReturnsQuantityInfo() + { + // Part of the netstandard2.0 API, so it must also exist on .NET for libraries compiled against netstandard2.0. + Assert.Same(Mass.Info, GetQuantityInfo(new Mass(1.0, MassUnit.Kilogram))); + Assert.Same(HowMuch.Info, GetQuantityInfo(new HowMuch(1.0, HowMuchUnit.Some))); + + static IQuantityInstanceInfo GetQuantityInfo(TQuantity quantity) + where TQuantity : IQuantityOfType + { +#pragma warning disable CS0618 // Type or member is obsolete + return quantity.QuantityInfo; +#pragma warning restore CS0618 + } + } + #if NET [Fact] public void StaticAbstract_Info_ReturnsSameAsTypedInfo() diff --git a/UnitsNet/CompatibilitySuppressions.xml b/UnitsNet/CompatibilitySuppressions.xml new file mode 100644 index 0000000000..eaa41d3ede --- /dev/null +++ b/UnitsNet/CompatibilitySuppressions.xml @@ -0,0 +1,66 @@ + + + + + + CP0006 + M:UnitsNet.IQuantity`2.From(UnitsNet.QuantityValue,`1) + lib/netstandard2.0/UnitsNet.dll + lib/net8.0/UnitsNet.dll + + + CP0006 + M:UnitsNet.IQuantityOfType`1.Create(UnitsNet.QuantityValue,UnitsNet.UnitKey) + lib/netstandard2.0/UnitsNet.dll + lib/net8.0/UnitsNet.dll + + + CP0006 + P:UnitsNet.IAffineQuantity`2.Zero + lib/netstandard2.0/UnitsNet.dll + lib/net8.0/UnitsNet.dll + + + CP0006 + P:UnitsNet.ILinearQuantity`1.Zero + lib/netstandard2.0/UnitsNet.dll + lib/net8.0/UnitsNet.dll + + + CP0006 + P:UnitsNet.ILogarithmicQuantity`1.Zero + lib/netstandard2.0/UnitsNet.dll + lib/net8.0/UnitsNet.dll + + + CP0006 + P:UnitsNet.IQuantity`2.Info + lib/netstandard2.0/UnitsNet.dll + lib/net8.0/UnitsNet.dll + + + CP0006 + P:UnitsNet.IQuantityOfType`1.Info + lib/netstandard2.0/UnitsNet.dll + lib/net8.0/UnitsNet.dll + + + CP0021 + T:UnitsNet.IAffineQuantity`2``1:T:System.Numerics.IAdditiveIdentity{`1,`1} + lib/netstandard2.0/UnitsNet.dll + lib/net8.0/UnitsNet.dll + + + CP0021 + T:UnitsNet.IAffineQuantity`3``2:T:System.Numerics.IAdditiveIdentity{`2,`2} + lib/netstandard2.0/UnitsNet.dll + lib/net8.0/UnitsNet.dll + + \ No newline at end of file diff --git a/UnitsNet/Extensions/AffineQuantityExtensions.cs b/UnitsNet/Extensions/AffineQuantityExtensions.cs index 3eaebd6495..fb6cba5a5a 100644 --- a/UnitsNet/Extensions/AffineQuantityExtensions.cs +++ b/UnitsNet/Extensions/AffineQuantityExtensions.cs @@ -66,7 +66,8 @@ private static bool EqualsAbsolute(this TQuantity quantity, TOffset difference = quantity - other; return QuantityValue.Abs(difference.Value) <= tolerance.GetValue(difference.UnitKey); } -#else +#endif + /// /// /// Compare equality to given a for the maximum allowed +/- @@ -109,7 +110,6 @@ public static bool Equals(this Temperature quantity, IQuantity? other, Temperatu { return other is Temperature otherInstance && quantity.Equals(otherInstance, tolerance); } -#endif /// /// Calculates the average of a collection of values. diff --git a/UnitsNet/IQuantity.cs b/UnitsNet/IQuantity.cs index 4150697698..8b3dca3434 100644 --- a/UnitsNet/IQuantity.cs +++ b/UnitsNet/IQuantity.cs @@ -109,10 +109,13 @@ public interface IQuantityOfType : IQuantity /// The unit of the quantity. /// An instance of the quantity with the specified value and unit. public static abstract TQuantity Create(QuantityValue value, UnitKey unit); -#else +#endif + /// - new IQuantityInstanceInfo QuantityInfo { get; } +#if NET + [Obsolete("Kept for back-compat with netstandard2.0. On .NET 5+, use the static TSelf.Info property or the GetQuantityInfo() extension method.")] #endif + new IQuantityInstanceInfo QuantityInfo { get; } } /// @@ -146,6 +149,10 @@ public interface IQuantity : IQuantityOfType, IQuantity static QuantityInfo IQuantityOfType.Info => TSelf.Info; +#pragma warning disable CS0618 // Type or member is obsolete + IQuantityInstanceInfo IQuantityOfType.QuantityInfo => TSelf.Info; +#pragma warning restore CS0618 + #pragma warning disable CS0618 // Type or member is obsolete QuantityInfo IQuantity.QuantityInfo { diff --git a/UnitsNet/IQuantityInfo.cs b/UnitsNet/IQuantityInfo.cs index d5d566982b..39265f7bf1 100644 --- a/UnitsNet/IQuantityInfo.cs +++ b/UnitsNet/IQuantityInfo.cs @@ -14,12 +14,7 @@ namespace UnitsNet; /// /// Typically you obtain this by looking it up via . /// -#if NETSTANDARD2_0 -public -#else -internal -#endif -interface IQuantityInfo +public interface IQuantityInfo { /// /// Quantity name, such as "Length" or "Mass". @@ -111,12 +106,7 @@ interface IQuantityInfo /// This is a specialization of that is used (internally) for constraining certain /// methods, without having to include the unit type as additional generic parameter. /// -#if NETSTANDARD2_0 -public -#else -internal -#endif -interface IQuantityInstanceInfo : IQuantityInfo +public interface IQuantityInstanceInfo : IQuantityInfo where TQuantity : IQuantity { /// diff --git a/UnitsNet/UnitsNet.csproj b/UnitsNet/UnitsNet.csproj index 00bfb47f1e..5238a39018 100644 --- a/UnitsNet/UnitsNet.csproj +++ b/UnitsNet/UnitsNet.csproj @@ -17,6 +17,8 @@ unit units quantity quantities measurement si metric imperial abbreviation abbreviations convert conversion parse immutable README.md true + + true From b05e9c714efcee5686eccda46765b7bb50499fcf Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp <5693174+tmilnthorp@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:52:40 -0400 Subject: [PATCH 2/2] Test the generic affine Equals overloads on .NET On .NET, calls with Temperature arguments now bind to the Temperature overloads, which left the generic overloads untested. Call them from a generic context to cover them again. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012XKhDsyHDc5BHrmibxScqG --- .../AffineQuantityExtensionsTest.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/UnitsNet.Tests/AffineQuantityExtensionsTest.cs b/UnitsNet.Tests/AffineQuantityExtensionsTest.cs index 9c8a091454..503a988759 100644 --- a/UnitsNet.Tests/AffineQuantityExtensionsTest.cs +++ b/UnitsNet.Tests/AffineQuantityExtensionsTest.cs @@ -1,6 +1,7 @@ // 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.Numerics; using UnitsNet.Tests.CustomQuantities; namespace UnitsNet.Tests; @@ -109,6 +110,49 @@ public void Equals_TQuantity_WithNullOther_ReturnsFalse() Assert.False(result); } + + [Theory] + [InlineData(25.0, 25.0, 0.1, true)] // Equal values + [InlineData(25.0, 25.1, 0.1, true)] // Within tolerance + [InlineData(25.0, 25.2, 0.1, false)] // Outside tolerance + [InlineData(25.0, 25.0, 0.0, true)] // Zero tolerance, equal values + [InlineData(25.0, 25.1, 0.0, false)] // Zero tolerance, different values + public void Equals_GenericAffineQuantity_ComparesWithinTolerance(double value1, double value2, double toleranceValue, bool expected) + { + var temperature1 = Temperature.FromDegreesCelsius(value1); + var temperature2 = Temperature.FromDegreesCelsius(value2); + var tolerance = TemperatureDelta.FromDegreesCelsius(toleranceValue); + + Assert.Equal(expected, GenericEquals(temperature1, temperature2, tolerance)); + Assert.Equal(expected, GenericEqualsIQuantity(temperature1, temperature2, tolerance)); + } + + [Fact] + public void Equals_GenericAffineQuantity_ThrowsArgumentOutOfRangeException_ForNegativeTolerance() + { + var temperature1 = Temperature.FromDegreesCelsius(25.0); + var temperature2 = Temperature.FromDegreesCelsius(25.0); + var negativeTolerance = TemperatureDelta.FromDegreesCelsius(-0.1); + + Assert.Throws(() => GenericEquals(temperature1, temperature2, negativeTolerance)); + Assert.Throws(() => GenericEqualsIQuantity(temperature1, temperature2, negativeTolerance)); + } + + // Calling Equals in a generic context binds to the generic overloads; with concrete Temperature arguments + // the compiler prefers the Temperature-specific overloads instead. + private static bool GenericEquals(TQuantity quantity, TQuantity other, TOffset tolerance) + where TQuantity : IAffineQuantity, ISubtractionOperators + where TOffset : IQuantityOfType, IAdditiveIdentity + { + return quantity.Equals(other, tolerance); + } + + private static bool GenericEqualsIQuantity(TQuantity quantity, IQuantity other, TOffset tolerance) + where TQuantity : IAffineQuantity, ISubtractionOperators + where TOffset : IQuantityOfType, IAdditiveIdentity + { + return quantity.Equals(other, tolerance); + } #endif [Fact]