UnitsNet.Modular
6.0.0-alpha.2
Prefix Reserved
See the version list below for details.
dotnet add package UnitsNet.Modular --version 6.0.0-alpha.2
NuGet\Install-Package UnitsNet.Modular -Version 6.0.0-alpha.2
<PackageReference Include="UnitsNet.Modular" Version="6.0.0-alpha.2" />
<PackageVersion Include="UnitsNet.Modular" Version="6.0.0-alpha.2" />
<PackageReference Include="UnitsNet.Modular" />
paket add UnitsNet.Modular --version 6.0.0-alpha.2
#r "nuget: UnitsNet.Modular, 6.0.0-alpha.2"
#:package UnitsNet.Modular@6.0.0-alpha.2
#addin nuget:?package=UnitsNet.Modular&version=6.0.0-alpha.2&prerelease
#tool nuget:?package=UnitsNet.Modular&version=6.0.0-alpha.2&prerelease
UnitsNet Modular
Generate only the strongly typed quantities and units your application needs.
UnitsNet.Modular combines a small runtime with a Roslyn source generator. A consumer selects quantities from the UnitsNet catalog, optionally filters their units, and can add application-specific or third-party JSON definitions. The generator emits quantity structs, unit enums, conversions, parsing, formatting, localization, arithmetic, and relationships directly into a consumer-owned assembly.
The result keeps the familiar strengths of UnitsNet without requiring every application to carry the complete catalog:
- strongly typed quantities and unit enums;
- compile-time selection of quantities and units;
- built-in, custom, and third-party definitions in one generated model;
- affine, logarithmic, and nonlinear conversions;
- localized parsing and formatting;
- cross-quantity operators and aggregation;
- immutable runtime discovery and System.Text.Json support;
- trimming and Native AOT-friendly generated code.
Experimental: UnitsNet.Modular is an alpha proof of concept. Its API, package structure, and compatibility guarantees may change as the architecture is evaluated.
Want to try it without installing anything? Open the browser-based UnitsNet.Modular playground. It builds a focused sample with the real source generator, opens the quantity selection and custom definition files, and keeps generated C# available for inspection.
Contents
- Install
- Namespaces
- Quick start
- Choose a project structure
- Use generated quantities
- Configure generation
- Add custom quantities
- Add quantity relationships
- Publish a definition package
- Dynamic lookup and serialization
- Diagnostics
- Current scope and limitations
Install
UnitsNet.Modular supports .NET 8, .NET 9, and .NET 10. Install the prerelease package in the project that will own the generated quantities:
dotnet add package UnitsNet.Modular --prerelease
The package includes the runtime, quantity contracts, metadata types, and source generator. No separate runtime, contracts, or analyzer package is required.
Namespaces
The package and assembly are named UnitsNet.Modular, but general quantity concepts use the
familiar UnitsNet namespace. Generated built-in quantities, quantity contracts, metadata,
unit-system policy, reusable quantity math, and the immutable generated registry therefore stay
close to source-compatible with UnitsNet. Built-in unit enums use UnitsNet.Units.
Only APIs that compose or describe a consumer-owned module use UnitsNet.Modular: module
attributes, specs, profiles, and selection contracts. Public implementation types called only by
emitted source live under
UnitsNet.Modular.SourceGen, are hidden from IntelliSense, and are not intended for direct use.
The UnitsNet and UnitsNet.Modular packages cannot be referenced together in the same consumer
project. Their similarly named quantity contracts and generated types have different assembly
identities. Replace one package with the other at a generation boundary instead of mixing them.
Quick start
Declare one module interface and select the built-in quantities to generate:
using UnitsNet;
using UnitsNet.Modular;
using UnitsNet.Units;
using Catalog = UnitsNet.Modular.BuiltIns;
namespace MyApplication.Units;
[UnitsNetModule]
internal interface ApplicationUnits :
IInclude<Catalog.LengthSpec>,
IInclude<Catalog.DurationSpec>,
IInclude<Catalog.SpeedSpec>;
Build the project. UnitsNet.Modular generates Length, Duration, Speed, their unit enums, and the
relationships between them into the familiar UnitsNet and UnitsNet.Units namespaces.
Length route = Length.FromKilometers(1.2);
Length remaining = Length.Parse("500 m");
Length total = route + remaining;
Speed pace = total / Duration.FromMinutes(2);
Console.WriteLine(total.ToUnit(LengthUnit.Meter)); // 1700 m
Console.WriteLine(pace);
Only selected quantities are generated. Each selected quantity includes all its units unless a unit set filters them.
Choose a project structure
One application project
For a small application, place the module interface in the application project itself. The generated types become part of that application's assembly.
MyTool
├── MyTool.csproj -> UnitsNet.Modular
├── ApplicationUnits.cs
└── Program.cs
Shared consumer-owned units project
For a multi-project application, generate quantities once in a dedicated units project and reference that project everywhere else:
MyApplication.slnx
└── src
├── MyApplication.Units
│ ├── MyApplication.Units.csproj -> UnitsNet.Modular + definition packages
│ └── ApplicationUnits.cs
├── MyApplication.Domain -> MyApplication.Units
├── MyApplication.Persistence -> MyApplication.Units
├── MyApplication.Api -> MyApplication.Units
└── MyApplication.Cli -> MyApplication.Units
flowchart LR
BuiltIns["UnitsNet.Modular built-in specs"]
ThirdParty["Third-party definition packages"]
Units["MyApplication.Units<br/>selection + generation"]
Assembly["MyApplication.Units.dll<br/>generated quantity types"]
Apps["Domain, API, UI, persistence, services"]
BuiltIns --> Units
ThirdParty --> Units
Units --> Assembly
Assembly --> Apps
This is the recommended setup. A generated public type belongs to the assembly into which it is
generated. Generating Length independently in two assemblies creates two different CLR types,
even when both came from the same spec. One application-owned generation boundary gives all
application projects the same type identity.
Full catalog with UnitsNet-style namespaces
Generate the complete catalog into the established UnitsNet and UnitsNet.Units namespaces when
source compatibility is more important than assembly size:
using UnitsNet.Modular;
using UnitsNet.Modular.Profiles;
[UnitsNetModule]
internal interface CompatibilityUnits : IIncludeProfile<AllQuantitiesProfile>;
This targets source compatibility for common construction, conversion, parsing, formatting,
arithmetic, and aggregation code. It does not make the generated structs binary-compatible with
types from UnitsNet.dll.
Third-party quantities
A third-party definition package supplies specs rather than precompiled quantity structs. The consumer references the package, selects its public quantity specs in the application units project, and generates the third-party and built-in quantities together:
using Acme.Measurements.Definitions;
using UnitsNet.Modular;
using Catalog = UnitsNet.Modular.BuiltIns;
[UnitsNetModule]
internal interface ApplicationUnits :
IInclude<Catalog.LengthSpec>,
IInclude<WidgetCountSpec>,
IInclude<WidgetDistanceSpec>;
This lets the generator emit operators between custom and built-in quantities and avoids type identity conflicts between independently compiled quantity packages.
Use generated quantities
Construct and convert
Length a = Length.FromKilometers(1.5);
Length b = new(500, LengthUnit.Meter);
Length c = Length.From(2, LengthUnit.Mile);
double meters = a.As(LengthUnit.Meter);
Length inMeters = a.ToUnit(LengthUnit.Meter);
double converted = Length.Convert(1.5, LengthUnit.Kilometer, LengthUnit.Meter);
As() returns the numeric value in another unit. ToUnit() returns a quantity storing that unit.
The static Convert() method converts a raw numeric value without constructing a quantity.
Arithmetic and relationships
Linear quantities support ordinary arithmetic:
Length total = Length.FromMeters(2) + Length.FromCentimeters(50);
Length scaled = total * 3;
double ratio = total / Length.FromMeters(1);
Cross-quantity operators are emitted when all participating quantities are selected:
Area area = Length.FromMeters(2) * Length.FromMeters(3);
Speed speed = Length.FromKilometers(10) / Duration.FromHours(1);
Force force = Mass.FromKilograms(5) * Acceleration.FromMetersPerSecondSquared(9.81);
Pressure pressure = force / Area.FromSquareMeters(2);
Multiplication relationships are generated in both operand orders when commutative. Division is inferred unless the relation explicitly disables it.
Affine quantities use a selected linear offset quantity:
Temperature freezing = Temperature.FromDegreesCelsius(0);
Temperature boiling = Temperature.FromDegreesCelsius(100);
TemperatureDelta range = boiling - freezing;
Temperature adjusted = freezing + TemperatureDelta.FromDegreesCelsius(2);
Selecting an affine quantity without its offset quantity produces diagnostic UNM015.
Logarithmic quantities retain logarithmic arithmetic instead of being treated as linear values.
Parse, format, and localize
using System.Globalization;
Length distance = Length.Parse("1.5 km", CultureInfo.InvariantCulture);
bool parsed = Length.TryParse("500 m", CultureInfo.InvariantCulture, out Length result);
LengthUnit unit = Length.ParseUnit("km", CultureInfo.InvariantCulture);
string abbreviation = Length.GetAbbreviation(LengthUnit.Kilometer, CultureInfo.InvariantCulture);
string text = distance.ToString("F2", CultureInfo.InvariantCulture); // 1.50 km
Unit singular names and plural names parse case-insensitively. Configured abbreviations are case-sensitive. The requested culture is used first, with the definition's localization fallback behavior supplying the default abbreviation.
Aggregate
Generated extension methods delegate reusable algorithms to the UnitsNet runtime:
Length sum = new[]
{
Length.FromKilometers(1),
Length.FromMeters(500),
}.Sum();
Length average = new[]
{
Length.FromMeters(1),
Length.FromCentimeters(300),
}.Average(LengthUnit.Meter);
Linear quantities provide Sum() and Average() overloads, including selector and target-unit
forms. Affine quantities provide meaningful averages. Logarithmic quantities provide Sum(),
ArithmeticMean(), and GeometricMean() with logarithmic semantics.
Inspect immutable metadata
Each generated quantity exposes one strongly typed, immutable metadata object. Less-common discovery data lives there instead of being duplicated across the quantity API, and the generated registry stores that exact same instance:
QuantityInfo<Length, LengthUnit> info = Length.Info;
UnitInfo<LengthUnit> kilometer = info[LengthUnit.Kilometer];
Length value = Length.From(1.5, kilometer.Value);
string abbreviation = kilometer.GetDefaultAbbreviation(CultureInfo.InvariantCulture);
Debug.Assert(info.BaseUnit.Value == LengthUnit.Meter);
Debug.Assert(info.Units.Contains(kilometer));
Debug.Assert(ReferenceEquals(info, Quantity.Registry.Get(typeof(Length))));
The quantity type owns common value behavior (Value, Unit, Zero, From, Convert, As,
ToUnit, parsing, and formatting). Info owns identity, base-unit metadata, the immutable Units
collection, and base dimensions. UnitInfo<TUnit>.Value is the represented enum value;
SingularName and PluralName describe it. BaseUnitInfo, UnitInfos, and UnitInfo.Name are
hidden source-compatibility aliases. Unlike the legacy mutable metadata model, generated metadata
does not expose configurable conversion expressions or global registration.
Use an immutable unit system
UnitSystem and BaseUnits describe a preferred set of constituent units without changing global
state:
using UnitsNet;
Length length = Length.From(1.5, UnitSystem.SI);
double meters = Length.FromKilometers(1.5).As(UnitSystem.SI);
Length normalized = length.ToUnit(UnitSystem.SI);
Resolution considers only units selected into the current module.
Configure generation
Authoring naming convention
Authoring types describe generation inputs, so their names distinguish them from the concrete types they produce:
| Suffix | Purpose | Example |
|---|---|---|
Spec |
Identifies one built-in or custom quantity specification | LengthSpec, HowMuchSpec |
UnitSet |
Selects a reusable subset of a spec's units | MetricLengthUnitSet |
Profile |
Composes several specs and unit sets | MechanicsProfile |
The module interface names the consumer-owned generation boundary and can use an application-oriented
name such as ApplicationUnits. The *Spec suffix is the authoring convention for the corresponding
generated quantity and unit enum: LengthSpec specifies the Length quantity and its
LengthUnit enum.
Module declaration
[UnitsNetModule] marks the single generation boundary in a compilation:
[UnitsNetModule]
internal interface ApplicationUnits :
IInclude<UnitsNet.Modular.BuiltIns.LengthSpec>;
A compilation can contain one module marker. Compose a larger selection with profiles rather than
declaring multiple modules. UNM014 reports multiple module markers before they emit colliding
types.
Without a target namespace, each definition keeps its declared namespace:
- built-in definitions generate quantities into
UnitsNetand unit enums intoUnitsNet.Units; - custom definitions generate into their JSON
Namespace; - the generated
Quantityfacade is placed inUnitsNetwhen the module includes built-ins, or in the module interface's namespace for a custom-only module.
Pass a target namespace to override every selected definition and emit it into one namespace:
[UnitsNetModule("Contoso.Measurements")]
internal interface ApplicationUnits :
IInclude<UnitsNet.Modular.BuiltIns.LengthSpec>;
Targeting UnitsNet explicitly has the same compatibility behavior as the built-in default:
quantity types use UnitsNet and unit enums use UnitsNet.Units.
Select quantities
Include every unit of a definition:
IInclude<UnitsNet.Modular.BuiltIns.LengthSpec>
Include a filtered unit set:
IInclude<UnitsNet.Modular.BuiltIns.LengthSpec, MetricLengthUnitSet>
Built-in spec names add the Spec suffix to quantity definition names in the UnitsNet catalog, so
the Length specification is selected with UnitsNet.Modular.BuiltIns.LengthSpec. The specs are generated
by the analyzer in UnitsNet.Modular.BuiltIns; generated quantities retain their familiar names,
such as Length and LengthUnit. Built-in and custom specs both declare their stable semantic ID
with [QuantitySpec]; the namespace and Spec suffix are naming conventions, not lookup
rules.
Use profiles
UnitsNet.Modular currently supplies two profiles:
| Profile | Selection |
|---|---|
UnitsNet.Modular.Profiles.AllQuantitiesProfile |
Every built-in catalog quantity and unit |
UnitsNet.Modular.Profiles.AllSiProfile |
Focused SI mechanics chain used by the POC sample |
Include a profile and add individual definitions:
[UnitsNetModule]
internal interface ApplicationUnits :
IIncludeProfile<UnitsNet.Modular.Profiles.AllQuantitiesProfile>,
IInclude<MyCustomSpec>;
Create reusable application profiles from the same authoring interfaces:
internal interface MechanicsProfile :
IInclude<UnitsNet.Modular.BuiltIns.LengthSpec>,
IInclude<UnitsNet.Modular.BuiltIns.DurationSpec>,
IInclude<UnitsNet.Modular.BuiltIns.SpeedSpec>;
internal interface ProductProfile :
IIncludeProfile<MechanicsProfile>,
IInclude<WidgetCountSpec>;
[UnitsNetModule]
internal interface ApplicationUnits :
IIncludeProfile<ProductProfile>;
Profiles can be nested. A direct IInclude<TQuantitySpec, TUnitSet> on the module overrides profile
unit selections for that quantity. Direct IInclude<TQuantitySpec> selects every unit.
Filter units
Declare a reusable unit set with [UnitSet]:
[UnitSet("Meter", "Millimeter", "Kilometer")]
internal interface CommonLengthUnitSet;
[UnitSet("glob:*Meter")]
internal interface MeterUnitSet;
[UnitSet("regex:^(Meter|Centi.*|Kilo.*)$")]
internal interface MetricUnitSet;
Pattern behavior:
| Syntax | Behavior |
|---|---|
Meter |
Bare glob; exact name when it contains no * |
glob:*Meter |
Case-insensitive glob where * matches any characters |
regex:.*Meter$ |
Case-insensitive, culture-invariant regular expression with a timeout |
Patterns match expanded singular unit names, not abbreviations. Prefix expansion happens before
filtering, so .*Meter$ can match Meter, Millimeter, and Kilometer. The base unit is always
included to keep the quantity convertible. Invalid patterns and patterns matching no unit are
compile-time errors.
Select files with MSBuild
Use Roslyn's native AdditionalFiles item for custom quantity and relation files:
<ItemGroup>
<AdditionalFiles Include="Definitions/*.unitsnet.json"
UnitsNetDefinition="true" />
<AdditionalFiles Include="Definitions/*.unitsnet.relations.json"
UnitsNetRelation="true" />
</ItemGroup>
The metadata is optional when the files use the conventional suffixes:
*.unitsnet.json;*.unitsnet.relations.json.
Metadata is useful for ordinary names such as Length.json.
The package also supports the custom item aliases below:
<ItemGroup>
<UnitsNetDefinition Include="Definitions/Length.json" />
<UnitsNetRelation Include="Definitions/Relations.json" />
</ItemGroup>
The packaged MSBuild targets map these aliases to AdditionalFiles. Native AdditionalFiles is
recommended because some IDE project models, including current Rider versions, do not reliably pass
custom build actions to the design-time Roslyn host.
Inspect generated source
Generated files normally stay in Roslyn's in-memory compilation. To write them beneath obj for
debugging or review, add:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>
$(BaseIntermediateOutputPath)Generated
</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
Do not compile that output directory explicitly; the compiler already receives the generated sources from Roslyn.
Add custom quantities
Register a definition
Add a JSON file to the units project:
<ItemGroup>
<AdditionalFiles Include="HowMuch.unitsnet.json"
UnitsNetDefinition="true" />
</ItemGroup>
Bind a public or internal quantity spec interface to the definition's stable semantic ID:
using UnitsNet.Modular;
namespace Fictional.Measurements.Definitions;
[QuantitySpec("Fictional.Measurements.HowMuch")]
public interface HowMuchSpec;
Select the spec in the module:
[UnitsNetModule]
internal interface ApplicationUnits : IInclude<HowMuchSpec>;
The spec ID must match Namespace.Name in the JSON definition. If Namespace is omitted, it
defaults to UnitsNet. Treat this semantic ID as a package-boundary identifier: use a namespace
you own so independently authored definition packages cannot describe unrelated quantities with
the same identity.
Quantity definition JSON
A compact nonlinear definition with prefixes and localization looks like this:
{
"Name": "HowMuch",
"Namespace": "Fictional.Measurements",
"BaseUnit": "Some",
"BaseDimensions": {},
"Units": [
{
"SingularName": "Some",
"PluralName": "Some",
"FromUnitToBaseFunc": "{x}",
"FromBaseToUnitFunc": "{x}",
"Prefixes": [ "Kilo" ],
"Localization": [
{
"Culture": "en-US",
"Abbreviations": [ "sm" ]
},
{
"Culture": "nb-NO",
"Abbreviations": [ "noe" ],
"AbbreviationsForPrefixes": {
"Kilo": "knoe"
}
}
]
},
{
"SingularName": "Magnitude",
"PluralName": "Magnitudes",
"FromUnitToBaseFunc": "Math.Pow({x}, 2)",
"FromBaseToUnitFunc": "Math.Sqrt({x})",
"Localization": [
{
"Culture": "en-US",
"Abbreviations": [ "mag" ]
}
]
}
]
}
Supported quantity fields:
| Field | Required | Meaning |
|---|---|---|
Name |
Yes | Generated quantity type name |
Namespace |
No | Definition namespace and part of its semantic ID; defaults to UnitsNet |
BaseUnit |
Yes | SingularName of the unit used as the conversion base |
Units |
Yes | Unit definitions; one must match BaseUnit |
BaseDimensions |
No | SI dimension exponents keyed by L, M, T, I, Θ, N, and J |
AffineOffsetType |
No | Linear offset quantity name or semantic ID required by an affine quantity |
Logarithmic |
No | String boolean such as "True"; defaults to false |
LogarithmicScalingFactor |
No | Invariant numeric string used by logarithmic aggregation; defaults to 1 |
The base-dimension symbols are L (length), M (mass), T (time), I (electric current),
Θ (temperature), N (amount of substance), and J (luminous intensity). Omitted exponents
default to zero. An unqualified AffineOffsetType resolves in the quantity's semantic namespace.
Supported unit fields:
| Field | Required | Meaning |
|---|---|---|
SingularName |
Yes | Unit enum member and singular parse name |
PluralName |
Yes | Plural parse name |
FromUnitToBaseFunc |
Yes | Converts {x} from this unit to the quantity base unit |
FromBaseToUnitFunc |
Yes | Converts {x} from the base unit to this unit |
BaseUnits |
No | Constituent base-unit names keyed by the seven SI symbols |
Prefixes |
No | Prefix names expanded into additional units |
Localization |
No | Culture-specific abbreviations |
Each localization contains:
| Field | Required | Meaning |
|---|---|---|
Culture |
Recommended | Culture name such as en-US or nb-NO |
Abbreviations |
No | Ordered abbreviations; the first is used for formatting |
AbbreviationsForPrefixes |
No | Prefix-specific string or string-array overrides |
Property names are case-insensitive. Comments and trailing commas are accepted so existing UnitsNet catalog files can be consumed without rewriting them.
Supported decimal prefixes are Femto, Pico, Nano, Micro, Milli, Centi, Deci, Deca,
Hecto, Kilo, Mega, Giga, Tera, Peta, and Exa. Supported binary prefixes are Kibi,
Mebi, Gibi, Tebi, Pebi, and Exbi.
Conversion expressions
Both conversion functions are required and must be inverses over the useful domain of the unit. UnitsNet.Modular validates and emits the expressions at compile time; it does not compile strings or use reflection at runtime.
The expression language supports:
- numeric literals and
{x}; - unary
+and-; +,-,*,/, and%;- parentheses;
Math.PIandMath.E;Math.Abs,Math.Exp,Math.Log,Math.Log10,Math.Pow, andMath.Sqrt.
Examples:
{
"FromUnitToBaseFunc": "({x} * 9 / 5) + 32",
"FromBaseToUnitFunc": "({x} - 32) * 5 / 9"
}
{
"FromUnitToBaseFunc": "Math.Pow({x}, 2)",
"FromBaseToUnitFunc": "Math.Sqrt({x})"
}
Add quantity relationships
Add a relation file as an AdditionalFiles item:
<AdditionalFiles Include="Fictional.unitsnet.relations.json"
UnitsNetRelation="true" />
Structured relations use stable semantic quantity IDs and invariant unit names:
[
{
"result": {
"quantity": "Fictional.Measurements.HowMuchDistance",
"unit": "SomeMeter"
},
"left": {
"quantity": "Fictional.Measurements.HowMuch",
"unit": "Some"
},
"operator": "*",
"right": {
"quantity": "UnitsNet.Length",
"unit": "Meter"
},
"noInferredDivision": false
}
]
Only multiplication relations are declared. The generator:
- emits both operand orders when the operands are different;
- infers the corresponding division operator;
- skips inferred division when
noInferredDivisionis true; - emits operators only when every participating quantity is selected;
- uses the relation's anchor units internally even when a unit filter omits those units publicly.
The special endpoint quantities "double" and "1" describe scalar and reciprocal relationships.
The older UnitsNet string form is also accepted:
[
"Area.SquareMeter = Length.Meter * Length.Meter",
"Speed.MeterPerSecond = Length.Meter * Frequency.Hertz -- NoInferredDivision"
]
Structured relations are recommended for third-party definitions because fully qualified semantic IDs remain unambiguous across namespaces.
Publish a definition package
A definition package distributes public quantity specs, JSON definitions, relationships, and an MSBuild props file, but does not generate or ship the resulting quantity structs.
Acme.Measurements.Definitions
├── DefinitionMarkers.cs
├── Definitions
│ ├── WidgetCount.unitsnet.json
│ └── Acme.unitsnet.relations.json
├── build
│ └── Acme.Measurements.Definitions.props
└── Acme.Measurements.Definitions.csproj
Pack the definition files and props:
<ItemGroup>
<None Include="Definitions/*.unitsnet.json"
Pack="true"
PackagePath="build/definitions" />
<None Include="Definitions/*.unitsnet.relations.json"
Pack="true"
PackagePath="build/definitions" />
<None Include="build/Acme.Measurements.Definitions.props"
Pack="true"
PackagePath="build/Acme.Measurements.Definitions.props" />
</ItemGroup>
The props file contributes those files directly to the referencing consumer's compilation:
<Project>
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)definitions/*.unitsnet.json"
UnitsNetDefinition="true" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)definitions/*.unitsnet.relations.json"
UnitsNetRelation="true" />
</ItemGroup>
</Project>
Quantity specs should be public so the consumer can select them. Keep their
[QuantitySpec] semantic IDs stable once published.
An organization can instead publish one canonical compiled units assembly for several controlled applications. That is a deployment choice, not the primary composition model. Independently compiled modules do not share type identity or automatically gain cross-module operators.
Dynamic lookup and serialization
Every module receives one immutable registry containing only its selected quantities:
using UnitsNet;
var registry = GeneratedQuantityRegistry.Instance;
IQuantityDescriptor length = registry.Get("Length");
IQuantity<double> parsed = length.Parse("1.5 km");
double meters = length.Convert(1.5, "Kilometer", "Meter");
IQuantity<double> created = registry.Create(
500,
"Length",
"Meter");
The registry supports lookup by semantic QuantityId, definition name, quantity CLR type, and unit
enum CLR type. Descriptors expose selected units, abbreviations, base dimensions, creation,
conversion, parsing, formatting, and stored value/unit access. TryGet, TryCreate, TryConvert,
and TryParse variants are available for non-throwing workflows.
When a module includes built-ins, the generator emits the source-compatible Quantity facade into
UnitsNet:
using UnitsNet;
IQuantity<double> value =
UnitsNet.Quantity.From(1.5, "Length", "Kilometer");
IQuantity<double> parsed =
UnitsNet.Quantity.Parse(typeof(Length), "1.5 km");
A custom-only module places its facade in the module interface's namespace unless the
UnitsNetModule attribute specifies an explicit target namespace.
System.Text.Json
Register the generated, AOT-safe converter factory:
using System.Text.Json;
using UnitsNet;
var options = new JsonSerializerOptions();
options.Converters.Add(GeneratedQuantityRegistry.JsonConverter);
string json = JsonSerializer.Serialize(Length.FromKilometers(1.5), options);
Length restored = JsonSerializer.Deserialize<Length>(json, options);
The shape is:
{
"Value": 1.5,
"Unit": "Kilometer"
}
Applications own the compatibility and versioning of serialized contracts. Semantic quantity IDs
and invariant unit names are the recommended boundary between independently compiled modules. The
generated converter handles selected concrete quantity types. Deserializing a polymorphic
IQuantity<double> directly is deliberately unsupported because the serialized shape does not
carry a CLR type or semantic quantity ID; resolve a descriptor by semantic ID at that boundary
instead.
Diagnostics
UnitsNet.Modular reports authoring problems at compile time:
| ID | Meaning |
|---|---|
UNM001 |
A selected quantity spec has no built-in or JSON definition |
UNM002 |
A unit pattern matched no unit |
UNM003 |
A definition does not contain its declared base unit |
UNM004 |
A JSON quantity definition is invalid |
UNM005 |
Multiple JSON files provide the same semantic quantity ID |
UNM006 |
A glob or regular expression is invalid |
UNM010 |
A relation file is invalid |
UNM011 |
Selected relations are ambiguous or cannot be resolved |
UNM012 |
A selected unit set has no patterns |
UNM013 |
Definitions collide after applying the target namespace |
UNM014 |
A compilation declares more than one module |
UNM015 |
An affine quantity's offset quantity is not selected |
UNM016 |
The module project also references the incompatible legacy UnitsNet assembly |
Current scope and limitations
- UnitsNet.Modular is a design probe, not yet a committed replacement for UnitsNet.
- Generated quantity values currently use
double. - Runtime mutation of conversion functions, abbreviations, or global defaults is deliberately not supported; definitions and generated metadata are immutable.
- Quantity and unit selection happens at compile time.
- Source compatibility with common UnitsNet APIs is a goal; binary compatibility is not.
- The
UnitsNetandUnitsNet.Modularpackages are alternative implementations and cannot be referenced together in one consumer project. - Definition packages ship specs. Generated types in different assemblies have different CLR identities.
- Unit filters match expanded invariant unit names, not localized abbreviations.
- The System.Text.Json integration is a proof of concept; applications own persisted-contract versioning.
- Specialized
Length.ParseFeetInchesparsing andPressureelevation modeling are currently explicit compatibility exclusions.
Samples and design documents
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 6.0.0-alpha.3 | 89 | 8/2/2026 |
| 6.0.0-alpha.2 | 92 | 8/1/2026 |