From 8a9d7312c4d44a32674a04778bab417dfa235ab5 Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Fri, 1 May 2026 20:58:16 +0200 Subject: [PATCH 1/2] Revert IQuantity.UnitInfo DIM in favor of GetUnitInfo() extension on all TFMs Reverts the default interface members added in #1649 and instead exposes GetUnitInfo() / GetUnitInfo() as extension methods on all target frameworks (previously gated behind #if !NET). Rationale (per @lipchev review feedback on #1649): - DIM bloats the IQuantity contract with members that just delegate to QuantityInfo[UnitKey]; aim is a leaner interface. - A custom IQuantity implementor overriding UnitInfo would silently diverge from internal lookups (UnitConverter, UnitAbbreviationsCache etc.) which always go through QuantityInfo, not the new property. - Calling the DIM through the IQuantity interface on a struct quantity causes interface dispatch and boxing per call. The extension method has none of these issues and gives the same ergonomic call-site (quantity.GetUnitInfo()) on every TFM. Co-Authored-By: Claude Opus 4.7 (1M context) --- UnitsNet.Tests/CustomCode/IQuantityTests.cs | 16 ++++++++-------- UnitsNet/Extensions/QuantityExtensions.cs | 10 ---------- UnitsNet/IQuantity.cs | 16 ---------------- 3 files changed, 8 insertions(+), 34 deletions(-) diff --git a/UnitsNet.Tests/CustomCode/IQuantityTests.cs b/UnitsNet.Tests/CustomCode/IQuantityTests.cs index c6011b07c9..bcf57d6b12 100644 --- a/UnitsNet.Tests/CustomCode/IQuantityTests.cs +++ b/UnitsNet.Tests/CustomCode/IQuantityTests.cs @@ -60,44 +60,44 @@ public void ToUnit_UnitSystem_ThrowsArgumentNullExceptionIfNull() } [Fact] - public void UnitInfo_ReturnsUnitInfoForQuantityUnit() + public void GetUnitInfo_ReturnsUnitInfoForQuantityUnit() { var length = new Length(3.0, LengthUnit.Centimeter); IQuantity quantity = length; - UnitInfo unitInfo = quantity.UnitInfo; + UnitInfo unitInfo = quantity.GetUnitInfo(); Assert.Equal(nameof(LengthUnit.Centimeter), unitInfo.Name); Assert.Equal(quantity.UnitKey, unitInfo.UnitKey); } [Fact] - public void UnitInfo_Zero_ReturnsBaseUnitInfo() + public void GetUnitInfo_Zero_ReturnsBaseUnitInfo() { IQuantity quantity = Length.Info.Zero; - UnitInfo unitInfo = quantity.UnitInfo; + UnitInfo unitInfo = quantity.GetUnitInfo(); Assert.Equal(Length.Info.BaseUnitInfo.UnitKey, unitInfo.UnitKey); } [Fact] - public void UnitInfo_TypedQuantity_ReturnsTypedUnitInfo() + public void GetUnitInfo_TypedQuantity_ReturnsTypedUnitInfo() { IQuantity quantity = new Length(3.0, LengthUnit.Centimeter); - UnitInfo unitInfo = quantity.UnitInfo; + UnitInfo unitInfo = quantity.GetUnitInfo(); Assert.Equal(LengthUnit.Centimeter, unitInfo.Value); Assert.Equal(nameof(LengthUnit.Centimeter), unitInfo.Name); } [Fact] - public void UnitInfo_MatchesUnit() + public void GetUnitInfo_MatchesUnit() { Assert.All(Quantity.Infos.Select(x => x.Zero), quantity => { - Assert.Equal(quantity.Unit, quantity.UnitInfo.Value); + Assert.Equal(quantity.Unit, quantity.GetUnitInfo().Value); }); } diff --git a/UnitsNet/Extensions/QuantityExtensions.cs b/UnitsNet/Extensions/QuantityExtensions.cs index 6a4e258f7f..c63cd4c626 100644 --- a/UnitsNet/Extensions/QuantityExtensions.cs +++ b/UnitsNet/Extensions/QuantityExtensions.cs @@ -11,16 +11,11 @@ namespace UnitsNet; /// public static class QuantityExtensions { -#if !NET /// /// Gets the for the unit this quantity was constructed with. /// /// The quantity. /// The for the quantity's unit. - /// - /// On .NET 5+ targets, this is available as a default interface member property - /// IQuantity.UnitInfo instead. - /// public static UnitInfo GetUnitInfo(this IQuantity quantity) { return quantity.QuantityInfo[quantity.UnitKey]; @@ -32,16 +27,11 @@ public static UnitInfo GetUnitInfo(this IQuantity quantity) /// The unit enum type. /// The quantity. /// The for the quantity's unit. - /// - /// On .NET 5+ targets, this is available as a default interface member property - /// IQuantity<TUnitType>.UnitInfo instead. - /// public static UnitInfo GetUnitInfo(this IQuantity quantity) where TUnit : struct, Enum { return quantity.QuantityInfo[quantity.Unit]; } -#endif /// /// This should be using UnitConverter.Default.ConvertValue(quantity, toUnit) diff --git a/UnitsNet/IQuantity.cs b/UnitsNet/IQuantity.cs index bb49466d68..149b3f9dd0 100644 --- a/UnitsNet/IQuantity.cs +++ b/UnitsNet/IQuantity.cs @@ -58,17 +58,6 @@ public interface IQuantity : IFormattable /// as it avoids the boxing that would normally occur when casting the enum to . /// UnitKey UnitKey { get; } - -#if NET - /// - /// Gets the for the unit this quantity was constructed with. - /// - /// - /// On targets that do not support default interface members (e.g. netstandard2.0), - /// use the GetUnitInfo() extension method from instead. - /// - UnitInfo UnitInfo => QuantityInfo[UnitKey]; -#endif } /// @@ -105,13 +94,8 @@ public interface IQuantity : IQuantity #if NET - /// - new UnitInfo UnitInfo => QuantityInfo[Unit]; - #region Implementation of IQuantity - UnitInfo IQuantity.UnitInfo => UnitInfo; - QuantityInfo IQuantity.QuantityInfo { get => QuantityInfo; From 61d581002305401b593cacd5c25b7fd930b8e235 Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Sat, 2 May 2026 17:43:25 +0200 Subject: [PATCH 2/2] Make typed GetUnitInfo overload return UnitInfo Replaces the IQuantity -> UnitInfo overload with IQuantity -> UnitInfo, so the typed extension returns the most specific UnitInfo shape when the caller has a concretely-typed quantity (e.g. Length). Behavior: - Length q; q.GetUnitInfo() -> UnitInfo - IQuantity q; q.GetUnitInfo() -> UnitInfo (non-generic fallback) - IQuantity q; q.GetUnitInfo() -> UnitInfo (non-generic fallback) The IQuantity reference case loses its typed UnitInfo return, but that scenario is uncommon compared to the concretely-typed receiver case which now returns the richer UnitInfo. Co-Authored-By: Claude Opus 4.7 (1M context) --- UnitsNet.Tests/CustomCode/IQuantityTests.cs | 21 ++++++++++++++++++--- UnitsNet/Extensions/QuantityExtensions.cs | 20 +++++++++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/UnitsNet.Tests/CustomCode/IQuantityTests.cs b/UnitsNet.Tests/CustomCode/IQuantityTests.cs index bcf57d6b12..2aee75a27d 100644 --- a/UnitsNet.Tests/CustomCode/IQuantityTests.cs +++ b/UnitsNet.Tests/CustomCode/IQuantityTests.cs @@ -82,16 +82,31 @@ public void GetUnitInfo_Zero_ReturnsBaseUnitInfo() } [Fact] - public void GetUnitInfo_TypedQuantity_ReturnsTypedUnitInfo() + public void GetUnitInfo_ConcreteQuantity_ReturnsFullyTypedUnitInfo() { - IQuantity quantity = new Length(3.0, LengthUnit.Centimeter); + var quantity = new Length(3.0, LengthUnit.Centimeter); - UnitInfo unitInfo = quantity.GetUnitInfo(); + // Overload resolution picks GetUnitInfo for the concrete struct receiver, + // returning the most specific UnitInfo. + UnitInfo unitInfo = quantity.GetUnitInfo(); Assert.Equal(LengthUnit.Centimeter, unitInfo.Value); Assert.Equal(nameof(LengthUnit.Centimeter), unitInfo.Name); } + [Fact] + public void GetUnitInfo_TypedQuantityReference_FallsBackToNonGeneric() + { + IQuantity quantity = new Length(3.0, LengthUnit.Centimeter); + + // The IQuantity reference does not satisfy the IQuantity constraint + // (TSelf would be IQuantity), so resolution falls back to GetUnitInfo(IQuantity). + UnitInfo unitInfo = quantity.GetUnitInfo(); + + Assert.Equal(LengthUnit.Centimeter, ((UnitInfo)unitInfo).Value); + Assert.Equal(nameof(LengthUnit.Centimeter), unitInfo.Name); + } + [Fact] public void GetUnitInfo_MatchesUnit() { diff --git a/UnitsNet/Extensions/QuantityExtensions.cs b/UnitsNet/Extensions/QuantityExtensions.cs index c63cd4c626..229907569d 100644 --- a/UnitsNet/Extensions/QuantityExtensions.cs +++ b/UnitsNet/Extensions/QuantityExtensions.cs @@ -14,6 +14,12 @@ public static class QuantityExtensions /// /// Gets the for the unit this quantity was constructed with. /// + /// + /// Picked by overload resolution for callers that only have an reference. + /// Concretely-typed callers (e.g. a Mass receiver) bind to the + /// overload and get the + /// more specific return. + /// /// The quantity. /// The for the quantity's unit. public static UnitInfo GetUnitInfo(this IQuantity quantity) @@ -22,12 +28,20 @@ public static UnitInfo GetUnitInfo(this IQuantity quantity) } /// - /// Gets the for the unit this quantity was constructed with. + /// Gets the for the unit this quantity was constructed with. /// + /// + /// Picked by overload resolution for concretely-typed receivers (e.g. Mass) where C# can + /// infer both and from the receiver's + /// implementation. Callers with only an + /// reference fall back to the non-generic overload. + /// + /// The quantity type. /// The unit enum type. /// The quantity. - /// The for the quantity's unit. - public static UnitInfo GetUnitInfo(this IQuantity quantity) + /// The for the quantity's unit. + public static UnitInfo GetUnitInfo(this IQuantity quantity) + where TQuantity : IQuantity where TUnit : struct, Enum { return quantity.QuantityInfo[quantity.Unit];