Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/unitsnet-modular-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ jobs:
--output Artifacts/UnitsNet.Modular.AotSmoke
./Artifacts/UnitsNet.Modular.AotSmoke/UnitsNet.Modular.Lean.Sample

- name: Run isolated NuGet consumer
- name: Run getting-started NuGet consumer
shell: pwsh
run: ./UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/run.ps1

- name: Run custom-definition NuGet consumer
shell: pwsh
run: ./UnitsNet.Modular/Samples/UnitsNet.Modular.NuGet.Sample/run.ps1

Expand Down
23 changes: 16 additions & 7 deletions UnitsNet.Modular/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ directly references it.
- `Samples/UnitsNet.Modular.Compatibility.Generated.Sample`: the exact same linked consumer source using
generated quantities.
- `Samples/UnitsNet.Modular.Custom.Sample`: a fictional `HowMuch` quantity in its own namespace.
- `Samples/UnitsNet.Modular.GettingStarted.Sample`: the package-based two-file quick start with
Length, Duration, Speed, and their generated relationship.
- `Samples/UnitsNet.Modular.NuGet.Sample`: an isolated real-consumer scenario using only a locally packed
`PackageReference` and consumer-owned JSON.
- `Samples/DefinitionPackages/Fictional.Measurements.Definitions`: a packable definition-only NuGet
Expand All @@ -222,9 +224,11 @@ directly references it.
consumers sharing the exact generated CLR types from `ConsumerOwned.Units`.

Feature and compatibility samples use project references because they exercise generated behavior
inside this repository. `UnitsNet.Modular.NuGet.Sample` and `ConsumerOwned.Units` deliberately cross the
local package boundary: the former covers a minimal consumer-owned definition, while the latter
composes separately packed quantity specs into a shared application assembly.
inside this repository. `UnitsNet.Modular.GettingStarted.Sample`,
`UnitsNet.Modular.NuGet.Sample`, and `ConsumerOwned.Units` deliberately cross the local package
boundary: the first mirrors the documentation quick start, the second covers a minimal
consumer-owned definition, and the last composes separately packed quantity specs into a shared
application assembly.

The compatibility test project uses aliased references to compare both implementations' selected
public API and unit names without introducing concrete-type ambiguity. It compares against the
Expand Down Expand Up @@ -400,10 +404,15 @@ after packing. Pass
`-p:UnitsNetModularPackForPublish=true` to create the MinVer-derived publish version instead; CI sets
this explicitly.

Run `pwsh UnitsNet.Modular/Samples/UnitsNet.Modular.NuGet.Sample/run.ps1` from the repository root for the
clean-room check. The script provides an isolated package cache and disables repository
`Directory.Build.*` imports; the sample build dependency performs the pack and restore before
executing the consumer.
Run either package-facing sample from the repository root:

```powershell
pwsh UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/run.ps1
pwsh UnitsNet.Modular/Samples/UnitsNet.Modular.NuGet.Sample/run.ps1
```

Each script provides an isolated package cache and disables repository `Directory.Build.*` imports;
the sample build dependency performs the pack and restore before executing the consumer.

## Versioning and CI

Expand Down
92 changes: 78 additions & 14 deletions UnitsNet.Modular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ definition files, and keeps generated C# available for inspection.
- [Publish a definition package](#publish-a-definition-package)
- [Dynamic lookup and serialization](#dynamic-lookup-and-serialization)
- [Diagnostics](#diagnostics)
- [Troubleshooting](#troubleshooting)
- [Current scope and limitations](#current-scope-and-limitations)

## Install
Expand Down Expand Up @@ -76,12 +77,12 @@ identities. Replace one package with the other at a generation boundary instead

## Quick start

Declare one module interface and select the built-in quantities to generate:
Add these two files to the project that references `UnitsNet.Modular`.

`ApplicationUnits.cs` declares the generation boundary and selects the built-in quantities:

```csharp
using UnitsNet;
using UnitsNet.Modular;
using UnitsNet.Units;
using Catalog = UnitsNet.Modular.BuiltIns;

namespace MyApplication.Units;
Expand All @@ -93,21 +94,29 @@ internal interface ApplicationUnits :
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.
`Program.cs` uses the generated API:

```csharp
using UnitsNet;
using UnitsNet.Units;

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);
Console.WriteLine($"Total: {total.ToUnit(LengthUnit.Meter):F0}");
Console.WriteLine($"Pace: {pace:F1}");
```

Only selected quantities are generated. Each selected quantity includes all its units unless a
unit set filters them.
Build the project. The source generator sees `ApplicationUnits`, then emits `Length`, `Duration`,
`Speed`, and their unit enums into the consumer project's assembly. Because every participant is
selected, it also emits the `Length / Duration = Speed` relationship used above.

The `*Spec` interfaces are compile-time selections, not the generated quantities themselves. Each
selected quantity includes all its units unless a unit set filters them. The project containing the
module owns the generated CLR types, so other projects should reference that project rather than
declare another module.

## Choose a project structure

Expand Down Expand Up @@ -868,6 +877,54 @@ UnitsNet.Modular reports authoring problems at compile time:
| `UNM015` | An affine quantity's offset quantity is not selected |
| `UNM016` | The module project also references the incompatible legacy `UnitsNet` assembly |

Each diagnostic links to the relevant configuration documentation from IDEs that display analyzer
help links.

## Troubleshooting

### A generated quantity type is not found

Confirm that the project either declares a single `[UnitsNetModule]` or references the
consumer-owned units project that does. Select the quantity with `IInclude<TSpec>` or a profile,
then build the module project. Check the build output for `UNM` diagnostics; the generator does not
emit a quantity that was not selected.

If the build succeeds but editor completion remains stale, inspect the IDE's source-generator node
to confirm that quantity sources were emitted. Rebuild and reload the project or solution to refresh
the design-time Roslyn host. This can be necessary after changing analyzer packages or
`AdditionalFiles` inputs even though command-line builds already see the generated code.

### A custom JSON definition is ignored

Use Roslyn's native `AdditionalFiles` item so command-line and design-time builds receive the same
input:

```xml
<ItemGroup>
<AdditionalFiles Include="Definitions/*.unitsnet.json" />
</ItemGroup>
```

The definition's `Namespace.Name` must match the semantic ID on `[QuantitySpec]`, and the spec must
also be selected by the module. See [Add custom quantities](#add-custom-quantities).

### A relationship operator is missing

Relationships are emitted only when every participating quantity is selected. For example,
`Length / Duration` requires `LengthSpec`, `DurationSpec`, and `SpeedSpec`. Add the missing result or
operand quantity and rebuild.

### A filtered base unit is still generated

This is intentional. Every selected quantity keeps its base unit as a conversion anchor even when a
unit-set pattern does not match it.

### The project reports `UNM016`

`UnitsNet` and `UnitsNet.Modular` are alternative implementations and cannot be referenced together
in the module project. Remove the legacy package reference or move the generated quantities behind a
separate assembly boundary. See the [migration guide](MIGRATION.md) for compatibility options.

## Current scope and limitations

- UnitsNet.Modular is a design probe, not yet a committed replacement for UnitsNet.
Expand All @@ -888,8 +945,15 @@ UnitsNet.Modular reports authoring problems at compile time:

## Samples and design documents

- [Samples](https://github.com/angularsen/UnitsNet/tree/master/UnitsNet.Modular/Samples)
- [Consumer-owned package and project-reference scenarios](https://github.com/angularsen/UnitsNet/tree/master/UnitsNet.Modular/Samples/ConsumerOwned)
- [Custom definition package](https://github.com/angularsen/UnitsNet/tree/master/UnitsNet.Modular/Samples/DefinitionPackages/Fictional.Measurements.Definitions)
- [Architecture](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Modular/ARCHITECTURE.md)
- [Migration notes](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Modular/MIGRATION.md)
| Start here | Scenario |
|---|---|
| [Getting started](Samples/UnitsNet.Modular.GettingStarted.Sample) | Minimal package-based `Length`, `Duration`, and `Speed` application matching the quick start |
| [Lean selection](Samples/UnitsNet.Modular.Lean.Sample) | Select individual quantities and filter their units |
| [Custom quantity](Samples/UnitsNet.Modular.Custom.Sample) | Generate an application-owned quantity from JSON |
| [Playground](Samples/UnitsNet.Modular.Playground) | Explore relationships, aggregation, metadata, serialization, and custom definitions |
| [Consumer-owned units](Samples/ConsumerOwned) | Share one generated quantity assembly across several projects |
| [Definition package](Samples/DefinitionPackages/Fictional.Measurements.Definitions) | Publish reusable specs and definitions without compiled quantity structs |
| [Compatibility pair](Samples/UnitsNet.Modular.Compatibility.Shared) | Compile the same consumer source against UnitsNet and UnitsNet.Modular |

For design rationale and compatibility details, continue with
[Architecture](ARCHITECTURE.md) and [Migration notes](MIGRATION.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Licensed under MIT No Attribution, see LICENSE file at the root.

using UnitsNet.Modular;
using Catalog = UnitsNet.Modular.BuiltIns;

namespace MyApplication.Units;

[UnitsNetModule]
internal interface ApplicationUnits :
IInclude<Catalog.LengthSpec>,
IInclude<Catalog.DurationSpec>,
IInclude<Catalog.SpeedSpec>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Licensed under MIT No Attribution, see LICENSE file at the root.

using UnitsNet;
using UnitsNet.Units;

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: {total.ToUnit(LengthUnit.Meter):F0}");
Console.WriteLine($"Pace: {pace:F1}");
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# UnitsNet.Modular getting started

This minimal console app matches the two-file quick start in the main
[UnitsNet.Modular documentation](../../README.md). It selects `Length`, `Duration`, and `Speed`,
then constructs, parses, converts, and combines the generated quantities.

In an ordinary application, install the package and copy `ApplicationUnits.cs` and `Program.cs`:

```shell
dotnet add package UnitsNet.Modular --prerelease
dotnet run
```

The project file in this repository contains additional maintainer-only automation that packs the
current checkout to the repository-local NuGet feed before restoring the sample. Consumers do not
need that import or any of the `UnitsNetModularSample*` properties.

Run the repository scenario from its root with:

```powershell
pwsh UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/run.ps1
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>

<!-- The packaged analyzer brings its private dependencies; repository ProjectReference plumbing must stay off. -->
<UnitsNetModularSampleUsePackagedAnalyzer>true</UnitsNetModularSampleUsePackagedAnalyzer>
</PropertyGroup>

<!-- Repository-only automation that packs and restores the newest local package. -->
<Import Project="../UnitsNet.Modular.LocalPackages.targets" />

<PropertyGroup>
<RestoreSources>$(UnitsNetModularSampleLocalFeed)</RestoreSources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="UnitsNet.Modular" Version="$(UnitsNetModularSampleLocalVersion)" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Licensed under MIT No Attribution, see LICENSE file at the root.

$ErrorActionPreference = 'Stop'

$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path
$sampleProject = Join-Path $PSScriptRoot 'UnitsNet.Modular.GettingStarted.Sample.csproj'
$restoreId = [DateTime]::UtcNow.ToString('yyyyMMddHHmmssfff')
$restorePackages = Join-Path $repositoryRoot "Artifacts\UnitsNet.Modular.GettingStarted.Sample\packages\$restoreId"

# The sample's local build dependency packs and restores the latest package before compilation.
& dotnet run `
--project $sampleProject `
-p:ImportDirectoryBuildProps=false `
-p:ImportDirectoryBuildTargets=false `
"-p:RestorePackagesPath=$restorePackages"
if ($LASTEXITCODE -ne 0) {
throw "Running the getting-started sample failed."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed under MIT No Attribution, see LICENSE file at the root.

using System.Reflection;
using Microsoft.CodeAnalysis;
using Xunit;

namespace UnitsNet.Modular.Generator.Tests;

public sealed class DiagnosticDocumentationTests
{
[Fact]
public void EveryDiagnosticLinksToRelevantDocumentation()
{
DiagnosticDescriptor[] descriptors = typeof(UnitsNetModularGenerator)
.GetFields(BindingFlags.NonPublic | BindingFlags.Static)
.Where(field => field.FieldType == typeof(DiagnosticDescriptor))
.Select(field => (DiagnosticDescriptor)field.GetValue(null)!)
.ToArray();

Assert.Equal(13, descriptors.Length);
Assert.All(
descriptors,
descriptor => Assert.StartsWith(
"https://github.com/angularsen/UnitsNet/tree/master/UnitsNet.Modular#",
descriptor.HelpLinkUri,
StringComparison.Ordinal));
}
}
Loading
Loading