Skip to content

Proposal: generate custom quantities against the UnitsNet package #1745

Description

@tmilnthorp

Problem

Adding a quantity of your own on top of the UnitsNet package is possible, but it's a lot of hand-written code. The HowMuch sample (Samples/UnitsNetSetup.Configuration/ConfigureWithCustomQuantities.cs) is about 250 lines:

  • a unit enum and a struct implementing ILinearQuantity<,>;
  • hand-written equality, comparison, parsing and formatting;
  • a QuantityInfo, plus a .resx file for abbreviations.

It still lacks the generated API that built-in quantities have, such as HowMuch.FromSomes(1), howMuch.Somes, and operators with built-in quantities. The docs call this approach "highly experimental and incomplete".

UnitsNet.Modular generates custom quantities from JSON, with the same API as built-in ones. But it replaces UnitsNet rather than working with it: the two can't be referenced together (UNM016). An application that uses UnitsNet today can't add one quantity this way.

Finding: generated code is close to working outside UnitsNet

I copied generated quantity code (Turbidity and Length, renamed) into a separate project that references the built UnitsNet.dll, and compiled it. The only errors come from 7 internal APIs that generated code calls:

Internal API Used for Public alternative
new QuantityValue(numerator, denominator) Exact conversion factors QuantityValue.FromTerms, or make the constructor public
QuantityValue.PowerOfTen Large factors QuantityValue.FromPowerOfTen
Comparison.GetHashCode(Type, QuantityValue) GetHashCode Make public, or generate HashCode.Combine
UnitParser.Parse/TryParse overloads taking a QuantityInfo ParseUnit Make public
QuantityInfo.GetDefaultUnit(UnitSystem) Constructor taking a UnitSystem Make public
ExceptionHelper.CreateArgumentException CompareTo(object) Generate the exception inline
UnitsNetSetup.CreateQuantityInfo Configuring built-in quantities Not needed; an external quantity registers through setup instead

Everything else that generated code uses, including QuantityInfo, UnitDefinition, UnitParser.Default and QuantityFormatter, is already public.

Proposal

Let applications generate their own quantities with the CodeGen that generates the built-in ones, as part of their build:

<!-- MyApp.csproj, which references UnitsNet -->
<ItemGroup>
  <PackageReference Include="UnitsNet.CodeGen" PrivateAssets="all" />
  <UnitsNetDefinition Include="Units/*.json" />
</ItemGroup>

The JSON uses the same schema as Common/UnitDefinitions. On build, CodeGen generates only those quantities into obj/, in the same shape as the built-in ones: From<Unit> methods, unit properties, Parse/ToString, and relations. They compile into the application's own assembly.

This builds on #1742, which runs CodeGen from MSBuild during the repo's own build. The package would contain CodeGen and an MSBuild targets file that runs it on the application's definitions. What's needed:

  1. CodeGen options: input and output paths, a target namespace, and an "external" mode that generates only the given definitions using public APIs.
  2. Abbreviations: generated .restext files embedded in the application's assembly, passed to QuantityInfo as a ResourceManager. The HowMuch sample already does this.
  3. Registration: a generated extension, such as UnitsNetSetup.ConfigureDefaults(b => b.WithMyQuantities()), wrapping WithAdditionalQuantities.
  4. Relations with built-in quantities: the package ships the built-in JSON so CodeGen can generate operators like HowMuch * Length = Mass on the custom type. Operators between two built-in quantities can't be added this way.
  5. Unit enum values: each application keeps its own enum value file next to its JSON, like Common/UnitEnumValues.g.json, so values stay stable as units are added.

Compared with UnitsNet.Modular

This proposal UnitsNet.Modular
Works with the UnitsNet package Yes No, it replaces it
Runtime setup, QuantityValue, UnitConverter All existing features Compile-time only, double values
Selecting a subset of built-in quantities No Yes
Integration MSBuild step in the application's build Source generator
Type identity Generated types belong to the generating assembly Same

Question: is Modular the planned direction for v7? If so, it may be better to let Modular's generator target the UnitsNet package, rather than adding a second generator path. I'd like your view before going further.

Possible steps

Each is additive and independently useful:

  1. Make the internal helpers above public, or generate public alternatives. This also helps hand-written custom quantities today.
  2. Add CodeGen options for paths, namespace and external mode.
  3. Package CodeGen with the MSBuild targets.
  4. Later, units as objects. If Proposal: units as objects in v6 (UnitOf<TQuantity> instead of unit enums) #1740 lands, generated custom quantities use UnitOf<TQuantity> units instead of an enum, so there are no enum values to coordinate between packages.

Related: #1742 (CodeGen runs in the build), #1740 (units as objects), #902 (source generators), #1181 (one package per quantity), #1728 (feedback on a custom quantity built with Modular), UnitsNet.Modular.

🤖 Generated with Claude Code

https://claude.ai/code/session_012XKhDsyHDc5BHrmibxScqG

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions