Revert IQuantity.UnitInfo DIM, expose GetUnitInfo() extension on all TFMs#1657
Revert IQuantity.UnitInfo DIM, expose GetUnitInfo() extension on all TFMs#1657angularsen merged 2 commits intomasterfrom
Conversation
…all TFMs Reverts the default interface members added in #1649 and instead exposes GetUnitInfo() / GetUnitInfo<TUnit>() 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) <noreply@anthropic.com>
Code Review — PR #1657: Revert IQuantity.UnitInfo DIM, expose GetUnitInfo() extension on all TFMsOverviewThis PR reverts the default interface member Breaking Changes
// before
UnitInfo info = quantity.UnitInfo;
// after
UnitInfo info = quantity.GetUnitInfo();Since Rationale — AgreedThe three reasons given in the PR description are all solid:
Code Quality
Minor Notes
SummaryThis is a well-reasoned, minimal revert. The rationale is convincing and the implementation is clean. LGTM once the breaking change is noted in the release notes. |
|
@angularsen This is surely better than having them on the interface, but here are my thoughts:
Bottom line- I don't mind the addition, but if I were to ever need to use one of these properties in my own projects- I'd probably write my own |
|
I'm fine either way, but still can't see any good uses for having a convenience method for the |
Replaces the IQuantity<TUnit> -> UnitInfo<TUnit> overload with IQuantity<TQuantity, TUnit> -> UnitInfo<TQuantity, TUnit>, 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<Length, LengthUnit> - IQuantity q; q.GetUnitInfo() -> UnitInfo (non-generic fallback) - IQuantity<TUnit> q; q.GetUnitInfo() -> UnitInfo (non-generic fallback) The IQuantity<TUnit> reference case loses its typed UnitInfo<TUnit> return, but that scenario is uncommon compared to the concretely-typed receiver case which now returns the richer UnitInfo<TQuantity, TUnit>. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code ReviewOverviewReverts the DesignThe rationale is well explained: DIMs on a struct-based type add boxing cost for every call, and a custom The overload resolution table in the PR description is accurate and helpful:
The loss of typed return for Breaking ChangesRemoves TestsRenaming SummaryClean revert + improvement. No concerns. |
|
I changed it to return |
PR #1657's CI failed at restore time with: error NU1901: Warning As Error: Package 'NuGet.Packaging' 7.0.1 has a known low severity vulnerability error NU1901: Warning As Error: Package 'NuGet.Protocol' 7.0.1 has a known low severity vulnerability These are pulled in transitively by build tooling (CodeGen) and cannot be upgraded without breaking other constraints. Two changes: - Add NU1901 (low) and NU1902 (moderate) to WarningsNotAsErrors in Directory.Build.props so they remain visible as warnings but no longer fail the build via TreatWarningsAsErrors. High (NU1903) and critical (NU1904) advisories still fail the build. - CodeGen.csproj had its own WarningsNotAsErrors that overrode (not appended to) the one in Directory.Build.props. Prefix it with $(WarningsNotAsErrors); so the project inherits the NU codes (and the obsolete codes) while keeping its nullability suppressions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #1657's CI failed at restore time with: error NU1901: Warning As Error: Package 'NuGet.Packaging' 7.0.1 has a known low severity vulnerability error NU1901: Warning As Error: Package 'NuGet.Protocol' 7.0.1 has a known low severity vulnerability These are pulled in transitively by build tooling (CodeGen) and cannot be upgraded without breaking other constraints. Two changes: - Add NU1901 (low) and NU1902 (moderate) to WarningsNotAsErrors in Directory.Build.props so they remain visible as warnings but no longer fail the build via TreatWarningsAsErrors. High (NU1903) and critical (NU1904) advisories still fail the build. - CodeGen.csproj had its own WarningsNotAsErrors that overrode (not appended to) the one in Directory.Build.props. Prefix it with $(WarningsNotAsErrors); so the project inherits the NU codes (and the obsolete codes) while keeping its nullability suppressions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #1657's CI failed at restore time with: error NU1901: Warning As Error: Package 'NuGet.Packaging' 7.0.1 has a known low severity vulnerability error NU1901: Warning As Error: Package 'NuGet.Protocol' 7.0.1 has a known low severity vulnerability These are pulled in transitively by build tooling (CodeGen) and cannot be upgraded without breaking other constraints. Two changes: - Add NU1901 (low) and NU1902 (moderate) to WarningsNotAsErrors in Directory.Build.props so they remain visible as warnings but no longer fail the build via TreatWarningsAsErrors. High (NU1903) and critical (NU1904) advisories still fail the build. - CodeGen.csproj had its own WarningsNotAsErrors that overrode (not appended to) the one in Directory.Build.props. Prefix it with $(WarningsNotAsErrors); so the project inherits the NU codes (and the obsolete codes) while keeping its nullability suppressions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary Stops the build from failing on low/moderate NuGet audit advisories while keeping them visible as warnings. ## Why PR #1657's CI failed at restore time with: ``` error NU1901: Warning As Error: Package 'NuGet.Packaging' 7.0.1 has a known low severity vulnerability error NU1901: Warning As Error: Package 'NuGet.Protocol' 7.0.1 has a known low severity vulnerability ``` These are pulled in transitively by build tooling (CodeGen) and cannot be upgraded without breaking other constraints. ## Changes - Add `NU1901` (low) and `NU1902` (moderate) to `<WarningsNotAsErrors>` in `Directory.Build.props`. They remain visible as warnings but no longer fail the build via `TreatWarningsAsErrors`. `NU1903` (high) and `NU1904` (critical) still fail the build. - `CodeGen.csproj` had its own `<WarningsNotAsErrors>` that overrode (not appended to) the one in `Directory.Build.props`. Prefix it with `$(WarningsNotAsErrors);` so the project inherits the NU codes (and the obsolete codes) while keeping its nullability suppressions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary Bumps centrally-managed NuGet package versions in [Directory.Packages.props](Directory.Packages.props) to the latest stable release. | Package | From | To | |---------|------|----| | Microsoft.NET.Test.Sdk | 18.0.1 | 18.5.1 | | NuGet.Protocol | 7.0.1 | 7.3.1 | | Serilog | 4.3.0 | 4.3.1 | `NuGet.Protocol` 7.3.1 clears the low-severity NU1901 advisories that triggered the original CI failure on #1657. As a result this PR makes #1660 unnecessary as a build-unblocker, though the comment-and-policy clean-up there still has independent value. `System.CommandLine.DragonFruit` is left at the existing `0.4.0-alpha.22272.1`; no newer stable version is published to the configured sources. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>


Summary
Reverts the default interface members added in #1649 and exposes
GetUnitInfo()as extension methods on all target frameworks instead. The typed overload returns the most specificUnitInfo<TQuantity, TUnit>when overload resolution can infer both type parameters from the receiver.Rationale
Per @lipchev's review feedback on #1649:
QuantityInfo[UnitKey]— no new state, sets a precedent for further interface bloat (Dimensionsetc.).IQuantityimplementor overridingUnitInfowould silently diverge from internal lookups (UnitConverter,UnitAbbreviationsCache,QuantityFormatter) which all go throughQuantityInfo, not the new property.Mass.Zero.UnitInfothroughIQuantityon a struct quantity boxes the receiver per call; concrete quantities don't override the property so the DIM path is the only one available.The extension method gives the same call-site ergonomics (
quantity.GetUnitInfo()) on every TFM with none of these issues.Overload resolution
Length q;GetUnitInfo<TQuantity, TUnit>(IQuantity<TQuantity, TUnit>)UnitInfo<Length, LengthUnit>IQuantity q;GetUnitInfo(IQuantity)UnitInfoIQuantity<TUnit> q;GetUnitInfo(IQuantity)(fallback)UnitInfoThe
IQuantity<TUnit>reference case loses its typed return —IQuantity<TUnit>does not satisfy theIQuantity<TQuantity, TUnit>constraint, so resolution falls back to the non-generic overload. That scenario is uncommon compared to the concretely-typed receiver case, which is now strictly better than before (returnsUnitInfo<TQuantity, TUnit>instead ofUnitInfo<TUnit>).Follow-up
PR #1658 stacks on top of this branch: adds
static abstract InfoonIQuantityOfType<T>/IQuantity<TSelf, TUnitType>, obsoletes the instanceQuantityInfomember, and provides matchingGetQuantityInfo()extensions. Out of scope for this PR.🤖 Generated with Claude Code