Skip to content

Commit 1067c30

Browse files
angularsenclaude
andcommitted
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<TQuantity> and IQuantity<TSelf,TUnitType> 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<TUnit>()`) 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<TSelf, TUnitType> Info { get; }`, which directly satisfies the typed static abstract. The non-generic `IQuantityOfType<TSelf>.Info` is satisfied by a default static implementation in IQuantity<TSelf,TUnitType>: `static QuantityInfo IQuantityOfType<TSelf>.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<T>), and `TSelf.Info` (via static abstract on IQuantity<TSelf,TUnit>). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 60a875a commit 1067c30

12 files changed

Lines changed: 157 additions & 8 deletions

File tree

Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/UnitToStringConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
3131
if (!(value is IQuantity quantity))
3232
throw new ArgumentException("Expected value of type UnitsNet.IQuantity.", nameof(value));
3333

34-
Enum unitEnumValue = _settings.GetDefaultUnit(quantity.QuantityInfo.UnitType);
34+
Enum unitEnumValue = _settings.GetDefaultUnit(quantity.GetQuantityInfo().UnitType);
3535
int significantDigits = _settings.SignificantDigits;
3636

3737
IQuantity quantityInUnit = quantity.ToUnit(unitEnumValue);

UnitsNet.Benchmark/Conversions/ToUnit/QuantityConversionBenchmarks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public double ConvertOnce()
1919
double result = 0;
2020
foreach (IQuantity quantity in Quantities)
2121
{
22-
foreach (UnitInfo unitInfo in quantity.QuantityInfo.UnitInfos)
22+
foreach (UnitInfo unitInfo in quantity.GetQuantityInfo().UnitInfos)
2323
{
2424
result = quantity.As(unitInfo.Value);
2525
}

UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ public override void WriteJson(JsonWriter writer, IQuantity? quantity, JsonSeria
9999
/// <returns>The string representation associated with the given quantity</returns>
100100
protected string GetQuantityType(IQuantity quantity)
101101
{
102+
#pragma warning disable CS0618 // IQuantity.QuantityInfo: serialization must work for custom quantities not registered in UnitsNetSetup.Default.
102103
return _quantities[quantity.QuantityInfo.Name].Name;
104+
#pragma warning restore CS0618
103105
}
104106

105107
/// <inheritdoc />

UnitsNet.Serialization.JsonNet/UnitsNetBaseJsonConverter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ protected ValueUnit ConvertIQuantity(IQuantity quantity)
164164
{
165165
quantity = quantity ?? throw new ArgumentNullException(nameof(quantity));
166166

167+
#pragma warning disable CS0618 // IQuantity.QuantityInfo: serialization must work for custom quantities not registered in UnitsNetSetup.Default.
167168
return new ValueUnit {Value = (double)quantity.Value, Unit = $"{quantity.QuantityInfo.UnitType.Name}.{quantity.Unit}"};
169+
#pragma warning restore CS0618
168170
}
169171

170172
/// <summary>

UnitsNet.Tests/CustomCode/IQuantityTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,58 @@ public void GetUnitInfo_MatchesUnit()
116116
});
117117
}
118118

119+
[Fact]
120+
public void GetQuantityInfo_NonGeneric_ReturnsRegisteredQuantityInfo()
121+
{
122+
IQuantity quantity = new Mass(1.0, MassUnit.Kilogram);
123+
124+
QuantityInfo info = quantity.GetQuantityInfo();
125+
126+
Assert.Same(Mass.Info, info);
127+
}
128+
129+
[Fact]
130+
public void GetQuantityInfo_Typed_ReturnsRegisteredQuantityInfo()
131+
{
132+
IQuantity<MassUnit> quantity = new Mass(1.0, MassUnit.Kilogram);
133+
134+
QuantityInfo<MassUnit> info = quantity.GetQuantityInfo();
135+
136+
Assert.Same(Mass.Info, info);
137+
}
138+
139+
[Fact]
140+
public void StaticAbstract_Info_ReturnsSameAsTypedInfo()
141+
{
142+
// Calls IQuantity<TSelf, TUnitType>.Info via the static abstract member.
143+
QuantityInfo<Mass, MassUnit> typedInfo = Mass.Info;
144+
QuantityInfo<Mass, MassUnit> viaStaticAbstract = StaticAbstractAccess<Mass, MassUnit>();
145+
146+
Assert.Same(typedInfo, viaStaticAbstract);
147+
148+
static QuantityInfo<TSelf, TUnit> StaticAbstractAccess<TSelf, TUnit>()
149+
where TSelf : IQuantity<TSelf, TUnit>
150+
where TUnit : struct, Enum
151+
{
152+
return TSelf.Info;
153+
}
154+
}
155+
156+
[Fact]
157+
public void StaticAbstract_Info_NonGeneric_ReturnsSameAsTypedInfo()
158+
{
159+
// Calls IQuantityOfType<TQuantity>.Info via the static abstract member.
160+
QuantityInfo info = StaticAbstractAccess<Mass>();
161+
162+
Assert.Same((QuantityInfo)Mass.Info, info);
163+
164+
static QuantityInfo StaticAbstractAccess<TQuantity>()
165+
where TQuantity : IQuantityOfType<TQuantity>
166+
{
167+
return TQuantity.Info;
168+
}
169+
}
170+
119171
[Fact]
120172
public void ToUnit_UnitSystem_ThrowsArgumentExceptionIfNotSupported()
121173
{

UnitsNet.Tests/CustomQuantities/HowMuch.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public double As(HowMuchUnit unit)
3131

3232
#region IQuantity
3333

34-
public static readonly QuantityInfo<HowMuch, HowMuchUnit> Info = new(
34+
public static QuantityInfo<HowMuch, HowMuchUnit> Info { get; } = new(
3535
nameof(HowMuch),
3636
HowMuchUnit.Some,
3737
new UnitDefinition<HowMuchUnit>[]

UnitsNet/Extensions/LinearQuantityExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,11 @@ public static TQuantity Sum<TQuantity, TUnit>(this IEnumerable<TQuantity> quanti
162162
resultValue += enumerator.Current!.GetValue(unitKey);
163163
}
164164

165+
#if NET
166+
return TQuantity.From(resultValue, unit);
167+
#else
165168
return firstQuantity.QuantityInfo.From(resultValue, unit);
169+
#endif
166170
}
167171

168172
/// <summary>

UnitsNet/Extensions/LogarithmicQuantityExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,11 @@ public static TQuantity Sum<TQuantity, TUnit>(this IEnumerable<TQuantity> quanti
203203
sumInLinearSpace += enumerator.Current!.GetValue(unitKey).ToLinearSpace(logarithmicScalingFactor);
204204
}
205205

206+
#if NET
207+
return TQuantity.From(sumInLinearSpace.ToLogSpace(logarithmicScalingFactor), targetUnit);
208+
#else
206209
return firstQuantity.QuantityInfo.From(sumInLinearSpace.ToLogSpace(logarithmicScalingFactor), targetUnit);
210+
#endif
207211
}
208212

209213
/// <summary>
@@ -342,7 +346,11 @@ public static TQuantity ArithmeticMean<TQuantity, TUnit>(this IEnumerable<TQuant
342346
}
343347

344348
var avgInLinearSpace = sumInLinearSpace / nbQuantities;
349+
#if NET
350+
return TQuantity.From(avgInLinearSpace.ToLogSpace(logarithmicScalingFactor), unit);
351+
#else
345352
return firstQuantity.QuantityInfo.From(avgInLinearSpace.ToLogSpace(logarithmicScalingFactor), unit);
353+
#endif
346354
}
347355

348356
/// <summary>
@@ -473,7 +481,11 @@ public static TQuantity GeometricMean<TQuantity, TUnit>(this IEnumerable<TQuanti
473481
}
474482

475483
var geometricMean = RootN(sumInLogSpace, nbQuantities);
484+
#if NET
485+
return TQuantity.From(geometricMean, unitKey.ToUnit<TUnit>());
486+
#else
476487
return firstQuantity.QuantityInfo.From(geometricMean, unitKey.ToUnit<TUnit>());
488+
#endif
477489
}
478490

479491
/// <summary>

UnitsNet/Extensions/QuantityExtensions.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,30 @@ namespace UnitsNet;
1111
/// </summary>
1212
public static class QuantityExtensions
1313
{
14+
/// <summary>
15+
/// Gets the <see cref="UnitsNet.QuantityInfo"/> for the given quantity instance, looked up via
16+
/// <see cref="UnitsNetSetup.Default"/>.
17+
/// </summary>
18+
/// <remarks>
19+
/// Use the static <c>TSelf.Info</c> directly when you have a typed quantity reference for the best performance.
20+
/// This extension is convenient when working with an <see cref="IQuantity"/> reference where the concrete
21+
/// type is not known at compile time.
22+
/// </remarks>
23+
/// <param name="quantity">The quantity instance.</param>
24+
/// <returns>The <see cref="UnitsNet.QuantityInfo"/> registered in <see cref="UnitsNetSetup.Default"/> for the quantity's runtime type.</returns>
25+
public static QuantityInfo GetQuantityInfo(this IQuantity quantity)
26+
{
27+
return UnitsNetSetup.Default.Quantities.GetQuantityInfo(quantity.GetType());
28+
}
29+
30+
/// <inheritdoc cref="GetQuantityInfo(IQuantity)"/>
31+
/// <typeparam name="TUnit">The unit enum type of the quantity.</typeparam>
32+
public static QuantityInfo<TUnit> GetQuantityInfo<TUnit>(this IQuantity<TUnit> quantity)
33+
where TUnit : struct, Enum
34+
{
35+
return (QuantityInfo<TUnit>)UnitsNetSetup.Default.Quantities.GetQuantityInfo(quantity.GetType());
36+
}
37+
1438
/// <summary>
1539
/// Gets the <see cref="UnitInfo"/> for the unit this quantity was constructed with.
1640
/// </summary>
@@ -24,7 +48,7 @@ public static class QuantityExtensions
2448
/// <returns>The <see cref="UnitInfo"/> for the quantity's unit.</returns>
2549
public static UnitInfo GetUnitInfo(this IQuantity quantity)
2650
{
27-
return quantity.QuantityInfo[quantity.UnitKey];
51+
return quantity.GetQuantityInfo()[quantity.UnitKey];
2852
}
2953

3054
/// <summary>
@@ -44,7 +68,11 @@ public static UnitInfo<TQuantity, TUnit> GetUnitInfo<TQuantity, TUnit>(this IQua
4468
where TQuantity : IQuantity<TQuantity, TUnit>
4569
where TUnit : struct, Enum
4670
{
71+
#if NET
72+
return TQuantity.Info[quantity.Unit];
73+
#else
4774
return quantity.QuantityInfo[quantity.Unit];
75+
#endif
4876
}
4977

5078
/// <inheritdoc cref="IQuantity.As(UnitKey)" />
@@ -72,7 +100,11 @@ internal static double GetValue<TQuantity>(this TQuantity quantity, UnitKey toUn
72100
public static double As<TQuantity>(this TQuantity quantity, UnitSystem unitSystem)
73101
where TQuantity : IQuantity
74102
{
103+
#if NET
104+
return quantity.GetValue(quantity.GetQuantityInfo().GetDefaultUnit(unitSystem).UnitKey);
105+
#else
75106
return quantity.GetValue(quantity.QuantityInfo.GetDefaultUnit(unitSystem).UnitKey);
107+
#endif
76108
}
77109

78110
/// <summary>
@@ -100,7 +132,7 @@ public static TQuantity ToUnit<TQuantity>(this TQuantity quantity, UnitSystem un
100132
where TQuantity : IQuantityOfType<TQuantity>
101133
{
102134
#if NET
103-
QuantityInfo quantityInfo = quantity.QuantityInfo;
135+
QuantityInfo quantityInfo = TQuantity.Info;
104136
UnitKey unitKey = quantityInfo.GetDefaultUnit(unitSystem).UnitKey;
105137
return TQuantity.Create(quantity.As(unitKey), unitKey);
106138
#else
@@ -277,6 +309,10 @@ internal static TQuantity ArithmeticMean<TQuantity, TUnit>(this IEnumerable<TQua
277309
nbValues++;
278310
}
279311

312+
#if NET
313+
return TQuantity.From(sumOfValues / nbValues, unit);
314+
#else
280315
return firstQuantity.QuantityInfo.From(sumOfValues / nbValues, unit);
316+
#endif
281317
}
282318
}

UnitsNet/IQuantity.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Licensed under MIT No Attribution, see LICENSE file at the root.
1+
// Licensed under MIT No Attribution, see LICENSE file at the root.
22
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
33

44
namespace UnitsNet
@@ -11,6 +11,13 @@ public interface IQuantity : IFormattable
1111
/// <summary>
1212
/// Information about the quantity type, such as unit values and names.
1313
/// </summary>
14+
/// <remarks>
15+
/// Kept for back-compat with netstandard2.0. On .NET 5+, prefer the static <c>TSelf.Info</c>
16+
/// property or the <c>GetQuantityInfo()</c> extension method on <see cref="QuantityExtensions"/>.
17+
/// </remarks>
18+
#if NET
19+
[Obsolete("Kept for back-compat with netstandard2.0. On .NET 5+, use the static TSelf.Info property or the GetQuantityInfo() extension method.")]
20+
#endif
1421
QuantityInfo QuantityInfo { get; }
1522

1623
/// <summary>
@@ -82,6 +89,9 @@ public interface IQuantity<TUnitType> : IQuantity
8289
new TUnitType Unit { get; }
8390

8491
/// <inheritdoc cref="IQuantity.QuantityInfo"/>
92+
#if NET
93+
[Obsolete("Kept for back-compat with netstandard2.0. On .NET 5+, use the static TSelf.Info property or the GetQuantityInfo() extension method.")]
94+
#endif
8595
new QuantityInfo<TUnitType> QuantityInfo { get; }
8696

8797
/// <summary>
@@ -96,10 +106,12 @@ public interface IQuantity<TUnitType> : IQuantity
96106

97107
#region Implementation of IQuantity
98108

109+
#pragma warning disable CS0618 // Type or member is obsolete
99110
QuantityInfo IQuantity.QuantityInfo
100111
{
101112
get => QuantityInfo;
102113
}
114+
#pragma warning restore CS0618
103115

104116
Enum IQuantity.Unit
105117
{
@@ -121,6 +133,16 @@ public interface IQuantityOfType<out TQuantity> : IQuantity
121133
where TQuantity : IQuantity
122134
{
123135
#if NET
136+
/// <summary>
137+
/// The static <see cref="UnitsNet.QuantityInfo"/> for this quantity type.
138+
/// </summary>
139+
/// <remarks>
140+
/// Implemented by every quantity as a public static <c>Info</c> property. Prefer this and the
141+
/// <see cref="QuantityExtensions.GetQuantityInfo(IQuantity)"/> extension method over the
142+
/// obsolete instance <see cref="IQuantity.QuantityInfo"/> property.
143+
/// </remarks>
144+
public static abstract QuantityInfo Info { get; }
145+
124146
/// <summary>
125147
/// Creates an instance of the quantity from a specified value and unit.
126148
/// </summary>
@@ -144,9 +166,15 @@ public interface IQuantity<TSelf, TUnitType> : IQuantityOfType<TSelf>, IQuantity
144166
where TUnitType : struct, Enum
145167
{
146168
/// <inheritdoc cref="IQuantity.QuantityInfo"/>
169+
#if NET
170+
[Obsolete("Kept for back-compat with netstandard2.0. On .NET 5+, use the static TSelf.Info property or the GetQuantityInfo() extension method.")]
171+
#endif
147172
new QuantityInfo<TSelf, TUnitType> QuantityInfo { get; }
148173

149174
#if NET
175+
/// <inheritdoc cref="IQuantityOfType{TQuantity}.Info"/>
176+
public new static abstract QuantityInfo<TSelf, TUnitType> Info { get; }
177+
150178
/// <summary>
151179
/// Creates an instance of the quantity from a specified value and unit.
152180
/// </summary>
@@ -157,10 +185,14 @@ public interface IQuantity<TSelf, TUnitType> : IQuantityOfType<TSelf>, IQuantity
157185

158186
static TSelf IQuantityOfType<TSelf>.Create(double value, UnitKey unit) => TSelf.From(value, unit.ToUnit<TUnitType>());
159187

188+
static QuantityInfo IQuantityOfType<TSelf>.Info => TSelf.Info;
189+
190+
#pragma warning disable CS0618 // Type or member is obsolete
160191
QuantityInfo<TUnitType> IQuantity<TUnitType>.QuantityInfo
161192
{
162193
get => QuantityInfo;
163194
}
195+
#pragma warning restore CS0618
164196

165197
IQuantity<TUnitType> IQuantity<TUnitType>.ToUnit(TUnitType unit)
166198
{

0 commit comments

Comments
 (0)