From 1067c30a1192caeb677c217db604bc0000dcbfc6 Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Fri, 1 May 2026 22:18:00 +0200 Subject: [PATCH] Add static abstract Info and obsolete instance QuantityInfo on .NET 5+ Per discussion on #1657, this introduces a static abstract `Info` member on the IQuantityOfType and IQuantity interfaces under #if NET, marks the existing instance QuantityInfo property as [Obsolete] on .NET 5+, and adds GetQuantityInfo() extension methods on QuantityExtensions so callers have a single discoverable API that works on every TFM. Why - The instance QuantityInfo property invariably returns a per-type static value. Exposing it as an instance member implies it can vary per instance, which it cannot, and incurs interface dispatch (boxing on structs) for every call. - The static abstract member lets generic algorithms reach the info with `TSelf.Info` directly, no boxing, no virtual call. - The extension method pair (`GetQuantityInfo()` / `GetQuantityInfo()`) is the discoverable replacement for callers that only have an `IQuantity` reference. It looks the quantity up via `UnitsNetSetup.Default.Quantities`. - Keeping the instance property obsolete (warning) instead of removing it preserves source compatibility for existing callers and the netstandard2.0 contract. We can promote to error / remove once netstandard2.0 is dropped. Implementation notes - Generated quantities already expose `public static QuantityInfo Info { get; }`, which directly satisfies the typed static abstract. The non-generic `IQuantityOfType.Info` is satisfied by a default static implementation in IQuantity: `static QuantityInfo IQuantityOfType.Info => TSelf.Info;`. No codegen change required. - The IQuantity bridge `QuantityInfo IQuantity.QuantityInfo => QuantityInfo;` chain inside the interfaces uses #pragma to suppress the obsolete warning on the bridge itself. - Internal callers in UnitsNet were migrated to either `TSelf.Info` / `TSelf.From` (where the generic constraint allows) or `quantity.GetQuantityInfo()` (where it doesn't). Callers that must keep working for custom quantities not registered in `UnitsNetSetup.Default` (JsonNet serialization, debugger proxy, QuantityTypeConverter) keep using the instance member with a `#pragma warning disable CS0618` and a comment explaining why. - HowMuch test custom quantity changed `public static readonly` field to `public static QuantityInfo Info { get; }` property to satisfy the static abstract. - Tests added for round-trip equivalence between `Mass.Info`, `mass.GetQuantityInfo()`, `TQuantity.Info` (via static abstract on IQuantityOfType), and `TSelf.Info` (via static abstract on IQuantity). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Converters/UnitToStringConverter.cs | 2 +- .../ToUnit/QuantityConversionBenchmarks.cs | 2 +- .../AbbreviatedUnitsConverter.cs | 2 + .../UnitsNetBaseJsonConverter.cs | 2 + UnitsNet.Tests/CustomCode/IQuantityTests.cs | 52 +++++++++++++++++++ UnitsNet.Tests/CustomQuantities/HowMuch.cs | 2 +- .../Extensions/LinearQuantityExtensions.cs | 4 ++ .../LogarithmicQuantityExtensions.cs | 12 +++++ UnitsNet/Extensions/QuantityExtensions.cs | 40 +++++++++++++- UnitsNet/IQuantity.cs | 34 +++++++++++- UnitsNet/QuantityDisplay.cs | 11 +++- UnitsNet/QuantityTypeConverter.cs | 2 + 12 files changed, 157 insertions(+), 8 deletions(-) diff --git a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/UnitToStringConverter.cs b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/UnitToStringConverter.cs index f7f2806f73..7292c45de3 100644 --- a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/UnitToStringConverter.cs +++ b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/UnitToStringConverter.cs @@ -31,7 +31,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn if (!(value is IQuantity quantity)) throw new ArgumentException("Expected value of type UnitsNet.IQuantity.", nameof(value)); - Enum unitEnumValue = _settings.GetDefaultUnit(quantity.QuantityInfo.UnitType); + Enum unitEnumValue = _settings.GetDefaultUnit(quantity.GetQuantityInfo().UnitType); int significantDigits = _settings.SignificantDigits; IQuantity quantityInUnit = quantity.ToUnit(unitEnumValue); diff --git a/UnitsNet.Benchmark/Conversions/ToUnit/QuantityConversionBenchmarks.cs b/UnitsNet.Benchmark/Conversions/ToUnit/QuantityConversionBenchmarks.cs index 926ca0f2d3..08c304fc59 100644 --- a/UnitsNet.Benchmark/Conversions/ToUnit/QuantityConversionBenchmarks.cs +++ b/UnitsNet.Benchmark/Conversions/ToUnit/QuantityConversionBenchmarks.cs @@ -19,7 +19,7 @@ public double ConvertOnce() double result = 0; foreach (IQuantity quantity in Quantities) { - foreach (UnitInfo unitInfo in quantity.QuantityInfo.UnitInfos) + foreach (UnitInfo unitInfo in quantity.GetQuantityInfo().UnitInfos) { result = quantity.As(unitInfo.Value); } diff --git a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs index 10fc5b9b19..1514d89d0f 100644 --- a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs +++ b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs @@ -99,7 +99,9 @@ public override void WriteJson(JsonWriter writer, IQuantity? quantity, JsonSeria /// The string representation associated with the given quantity protected string GetQuantityType(IQuantity quantity) { +#pragma warning disable CS0618 // IQuantity.QuantityInfo: serialization must work for custom quantities not registered in UnitsNetSetup.Default. return _quantities[quantity.QuantityInfo.Name].Name; +#pragma warning restore CS0618 } /// diff --git a/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs b/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs index 73f4cce110..d4f6053579 100644 --- a/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs +++ b/UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs @@ -164,7 +164,9 @@ protected ValueUnit ConvertIQuantity(IQuantity quantity) { quantity = quantity ?? throw new ArgumentNullException(nameof(quantity)); +#pragma warning disable CS0618 // IQuantity.QuantityInfo: serialization must work for custom quantities not registered in UnitsNetSetup.Default. return new ValueUnit {Value = (double)quantity.Value, Unit = $"{quantity.QuantityInfo.UnitType.Name}.{quantity.Unit}"}; +#pragma warning restore CS0618 } /// diff --git a/UnitsNet.Tests/CustomCode/IQuantityTests.cs b/UnitsNet.Tests/CustomCode/IQuantityTests.cs index 2aee75a27d..db4b6936f6 100644 --- a/UnitsNet.Tests/CustomCode/IQuantityTests.cs +++ b/UnitsNet.Tests/CustomCode/IQuantityTests.cs @@ -116,6 +116,58 @@ public void GetUnitInfo_MatchesUnit() }); } + [Fact] + public void GetQuantityInfo_NonGeneric_ReturnsRegisteredQuantityInfo() + { + IQuantity quantity = new Mass(1.0, MassUnit.Kilogram); + + QuantityInfo info = quantity.GetQuantityInfo(); + + Assert.Same(Mass.Info, info); + } + + [Fact] + public void GetQuantityInfo_Typed_ReturnsRegisteredQuantityInfo() + { + IQuantity quantity = new Mass(1.0, MassUnit.Kilogram); + + QuantityInfo info = quantity.GetQuantityInfo(); + + Assert.Same(Mass.Info, info); + } + + [Fact] + public void StaticAbstract_Info_ReturnsSameAsTypedInfo() + { + // Calls IQuantity.Info via the static abstract member. + QuantityInfo typedInfo = Mass.Info; + QuantityInfo viaStaticAbstract = StaticAbstractAccess(); + + Assert.Same(typedInfo, viaStaticAbstract); + + static QuantityInfo StaticAbstractAccess() + where TSelf : IQuantity + where TUnit : struct, Enum + { + return TSelf.Info; + } + } + + [Fact] + public void StaticAbstract_Info_NonGeneric_ReturnsSameAsTypedInfo() + { + // Calls IQuantityOfType.Info via the static abstract member. + QuantityInfo info = StaticAbstractAccess(); + + Assert.Same((QuantityInfo)Mass.Info, info); + + static QuantityInfo StaticAbstractAccess() + where TQuantity : IQuantityOfType + { + return TQuantity.Info; + } + } + [Fact] public void ToUnit_UnitSystem_ThrowsArgumentExceptionIfNotSupported() { diff --git a/UnitsNet.Tests/CustomQuantities/HowMuch.cs b/UnitsNet.Tests/CustomQuantities/HowMuch.cs index 7a59f20a8d..36cb65f121 100644 --- a/UnitsNet.Tests/CustomQuantities/HowMuch.cs +++ b/UnitsNet.Tests/CustomQuantities/HowMuch.cs @@ -31,7 +31,7 @@ public double As(HowMuchUnit unit) #region IQuantity - public static readonly QuantityInfo Info = new( + public static QuantityInfo Info { get; } = new( nameof(HowMuch), HowMuchUnit.Some, new UnitDefinition[] diff --git a/UnitsNet/Extensions/LinearQuantityExtensions.cs b/UnitsNet/Extensions/LinearQuantityExtensions.cs index d6183625c9..f56448b8fd 100644 --- a/UnitsNet/Extensions/LinearQuantityExtensions.cs +++ b/UnitsNet/Extensions/LinearQuantityExtensions.cs @@ -162,7 +162,11 @@ public static TQuantity Sum(this IEnumerable quanti resultValue += enumerator.Current!.GetValue(unitKey); } +#if NET + return TQuantity.From(resultValue, unit); +#else return firstQuantity.QuantityInfo.From(resultValue, unit); +#endif } /// diff --git a/UnitsNet/Extensions/LogarithmicQuantityExtensions.cs b/UnitsNet/Extensions/LogarithmicQuantityExtensions.cs index 7f28720e05..f79f8874e8 100644 --- a/UnitsNet/Extensions/LogarithmicQuantityExtensions.cs +++ b/UnitsNet/Extensions/LogarithmicQuantityExtensions.cs @@ -203,7 +203,11 @@ public static TQuantity Sum(this IEnumerable quanti sumInLinearSpace += enumerator.Current!.GetValue(unitKey).ToLinearSpace(logarithmicScalingFactor); } +#if NET + return TQuantity.From(sumInLinearSpace.ToLogSpace(logarithmicScalingFactor), targetUnit); +#else return firstQuantity.QuantityInfo.From(sumInLinearSpace.ToLogSpace(logarithmicScalingFactor), targetUnit); +#endif } /// @@ -342,7 +346,11 @@ public static TQuantity ArithmeticMean(this IEnumerable @@ -473,7 +481,11 @@ public static TQuantity GeometricMean(this IEnumerable()); +#else return firstQuantity.QuantityInfo.From(geometricMean, unitKey.ToUnit()); +#endif } /// diff --git a/UnitsNet/Extensions/QuantityExtensions.cs b/UnitsNet/Extensions/QuantityExtensions.cs index 229907569d..2cedba1cdc 100644 --- a/UnitsNet/Extensions/QuantityExtensions.cs +++ b/UnitsNet/Extensions/QuantityExtensions.cs @@ -11,6 +11,30 @@ namespace UnitsNet; /// public static class QuantityExtensions { + /// + /// Gets the for the given quantity instance, looked up via + /// . + /// + /// + /// Use the static TSelf.Info directly when you have a typed quantity reference for the best performance. + /// This extension is convenient when working with an reference where the concrete + /// type is not known at compile time. + /// + /// The quantity instance. + /// The registered in for the quantity's runtime type. + public static QuantityInfo GetQuantityInfo(this IQuantity quantity) + { + return UnitsNetSetup.Default.Quantities.GetQuantityInfo(quantity.GetType()); + } + + /// + /// The unit enum type of the quantity. + public static QuantityInfo GetQuantityInfo(this IQuantity quantity) + where TUnit : struct, Enum + { + return (QuantityInfo)UnitsNetSetup.Default.Quantities.GetQuantityInfo(quantity.GetType()); + } + /// /// Gets the for the unit this quantity was constructed with. /// @@ -24,7 +48,7 @@ public static class QuantityExtensions /// The for the quantity's unit. public static UnitInfo GetUnitInfo(this IQuantity quantity) { - return quantity.QuantityInfo[quantity.UnitKey]; + return quantity.GetQuantityInfo()[quantity.UnitKey]; } /// @@ -44,7 +68,11 @@ public static UnitInfo GetUnitInfo(this IQua where TQuantity : IQuantity where TUnit : struct, Enum { +#if NET + return TQuantity.Info[quantity.Unit]; +#else return quantity.QuantityInfo[quantity.Unit]; +#endif } /// @@ -72,7 +100,11 @@ internal static double GetValue(this TQuantity quantity, UnitKey toUn public static double As(this TQuantity quantity, UnitSystem unitSystem) where TQuantity : IQuantity { +#if NET + return quantity.GetValue(quantity.GetQuantityInfo().GetDefaultUnit(unitSystem).UnitKey); +#else return quantity.GetValue(quantity.QuantityInfo.GetDefaultUnit(unitSystem).UnitKey); +#endif } /// @@ -100,7 +132,7 @@ public static TQuantity ToUnit(this TQuantity quantity, UnitSystem un where TQuantity : IQuantityOfType { #if NET - QuantityInfo quantityInfo = quantity.QuantityInfo; + QuantityInfo quantityInfo = TQuantity.Info; UnitKey unitKey = quantityInfo.GetDefaultUnit(unitSystem).UnitKey; return TQuantity.Create(quantity.As(unitKey), unitKey); #else @@ -277,6 +309,10 @@ internal static TQuantity ArithmeticMean(this IEnumerable /// Information about the quantity type, such as unit values and names. /// + /// + /// Kept for back-compat with netstandard2.0. On .NET 5+, prefer the static TSelf.Info + /// property or the GetQuantityInfo() extension method on . + /// +#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 QuantityInfo QuantityInfo { get; } /// @@ -82,6 +89,9 @@ public interface IQuantity : IQuantity new TUnitType Unit { 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 QuantityInfo QuantityInfo { get; } /// @@ -96,10 +106,12 @@ public interface IQuantity : IQuantity #region Implementation of IQuantity +#pragma warning disable CS0618 // Type or member is obsolete QuantityInfo IQuantity.QuantityInfo { get => QuantityInfo; } +#pragma warning restore CS0618 Enum IQuantity.Unit { @@ -121,6 +133,16 @@ public interface IQuantityOfType : IQuantity where TQuantity : IQuantity { #if NET + /// + /// The static for this quantity type. + /// + /// + /// Implemented by every quantity as a public static Info property. Prefer this and the + /// extension method over the + /// obsolete instance property. + /// + public static abstract QuantityInfo Info { get; } + /// /// Creates an instance of the quantity from a specified value and unit. /// @@ -144,9 +166,15 @@ public interface IQuantity : IQuantityOfType, IQuantity where TUnitType : struct, Enum { /// +#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 QuantityInfo QuantityInfo { get; } #if NET + /// + public new static abstract QuantityInfo Info { get; } + /// /// Creates an instance of the quantity from a specified value and unit. /// @@ -157,10 +185,14 @@ public interface IQuantity : IQuantityOfType, IQuantity static TSelf IQuantityOfType.Create(double value, UnitKey unit) => TSelf.From(value, unit.ToUnit()); + static QuantityInfo IQuantityOfType.Info => TSelf.Info; + +#pragma warning disable CS0618 // Type or member is obsolete QuantityInfo IQuantity.QuantityInfo { get => QuantityInfo; } +#pragma warning restore CS0618 IQuantity IQuantity.ToUnit(TUnitType unit) { diff --git a/UnitsNet/QuantityDisplay.cs b/UnitsNet/QuantityDisplay.cs index 166afc9544..4395ad7386 100644 --- a/UnitsNet/QuantityDisplay.cs +++ b/UnitsNet/QuantityDisplay.cs @@ -32,7 +32,9 @@ internal readonly struct AbbreviationDisplay public AbbreviationDisplay(IQuantity quantity) { _quantity = quantity; +#pragma warning disable CS0618 // IQuantity.QuantityInfo: debug proxy must work for custom quantities not registered in UnitsNetSetup.Default. QuantityInfo quantityQuantityInfo = quantity.QuantityInfo; +#pragma warning restore CS0618 IQuantity baseQuantity = quantity.ToUnit(quantityQuantityInfo.BaseUnitInfo.Value); Conversions = quantityQuantityInfo.UnitInfos.Select(x => new ConvertedQuantity(baseQuantity, x)).ToArray(); } @@ -70,8 +72,11 @@ internal readonly struct UnitDisplay public UnitDisplay(IQuantity quantity) { Unit = quantity.Unit; - IQuantity baseQuantity = quantity.ToUnit(quantity.QuantityInfo.BaseUnitInfo.Value); - Conversions = quantity.QuantityInfo.UnitInfos.Select(x => new ConvertedQuantity(baseQuantity, x.Value)).ToArray(); +#pragma warning disable CS0618 // IQuantity.QuantityInfo: debug proxy must work for custom quantities not registered in UnitsNetSetup.Default. + QuantityInfo info = quantity.QuantityInfo; +#pragma warning restore CS0618 + IQuantity baseQuantity = quantity.ToUnit(info.BaseUnitInfo.Value); + Conversions = info.UnitInfos.Select(x => new ConvertedQuantity(baseQuantity, x.Value)).ToArray(); } [DebuggerBrowsable(DebuggerBrowsableState.Never)] @@ -110,7 +115,9 @@ internal readonly struct QuantityConvertor public QuantityConvertor(IQuantity quantity) { QuantityToString = new StringFormatsDisplay(quantity); +#pragma warning disable CS0618 // IQuantity.QuantityInfo: debug proxy must work for custom quantities not registered in UnitsNetSetup.Default. QuantityInfo quantityQuantityInfo = quantity.QuantityInfo; +#pragma warning restore CS0618 IQuantity baseQuantity = quantity.ToUnit(quantityQuantityInfo.BaseUnitInfo.Value); QuantityToUnit = quantityQuantityInfo.UnitInfos.Select(x => new ConvertedQuantity(baseQuantity.ToUnit(x.Value), x)).ToArray(); } diff --git a/UnitsNet/QuantityTypeConverter.cs b/UnitsNet/QuantityTypeConverter.cs index ad4350abef..dcbed81dfa 100644 --- a/UnitsNet/QuantityTypeConverter.cs +++ b/UnitsNet/QuantityTypeConverter.cs @@ -153,8 +153,10 @@ private static TAttribute? GetAttribute< // Ensure the attribute's unit is compatible with this converter's quantity. if (attribute?.UnitType != null) { +#pragma warning disable CS0618 // IQuantity.QuantityInfo is obsolete on .NET 5+; use it here so consumers can author custom quantities (e.g. HowMuch in tests) without registering them in UnitsNetSetup.Default. string converterQuantityName = default(TQuantity).QuantityInfo.Name; string attributeQuantityName = Quantity.From(1, attribute.UnitType).QuantityInfo.Name; +#pragma warning restore CS0618 if (converterQuantityName != attributeQuantityName) { throw new ArgumentException($"The {attribute.GetType()}'s UnitType [{attribute.UnitType}] is not compatible with the converter's quantity [{converterQuantityName}].");