diff --git a/.github/workflows/unitsnet-modular-ci.yml b/.github/workflows/unitsnet-modular-ci.yml index 93da207317..d32ef685ce 100644 --- a/.github/workflows/unitsnet-modular-ci.yml +++ b/.github/workflows/unitsnet-modular-ci.yml @@ -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 diff --git a/UnitsNet.Modular/ARCHITECTURE.md b/UnitsNet.Modular/ARCHITECTURE.md index 8eb48231bc..d60c6f1eae 100644 --- a/UnitsNet.Modular/ARCHITECTURE.md +++ b/UnitsNet.Modular/ARCHITECTURE.md @@ -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 @@ -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 @@ -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 diff --git a/UnitsNet.Modular/README.md b/UnitsNet.Modular/README.md index f21072769c..1757bde6bf 100644 --- a/UnitsNet.Modular/README.md +++ b/UnitsNet.Modular/README.md @@ -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 @@ -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; @@ -93,21 +94,29 @@ internal interface ApplicationUnits : IInclude; ``` -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 @@ -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` 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 + + + +``` + +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. @@ -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). diff --git a/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/ApplicationUnits.cs b/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/ApplicationUnits.cs new file mode 100644 index 0000000000..2179611795 --- /dev/null +++ b/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/ApplicationUnits.cs @@ -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, + IInclude, + IInclude; diff --git a/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/Program.cs b/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/Program.cs new file mode 100644 index 0000000000..4389cada25 --- /dev/null +++ b/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/Program.cs @@ -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}"); diff --git a/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/README.md b/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/README.md new file mode 100644 index 0000000000..9768334003 --- /dev/null +++ b/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/README.md @@ -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 +``` diff --git a/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/UnitsNet.Modular.GettingStarted.Sample.csproj b/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/UnitsNet.Modular.GettingStarted.Sample.csproj new file mode 100644 index 0000000000..d596f6eb96 --- /dev/null +++ b/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/UnitsNet.Modular.GettingStarted.Sample.csproj @@ -0,0 +1,23 @@ + + + Exe + net10.0 + latest + enable + enable + false + + + true + + + + + + + $(UnitsNetModularSampleLocalFeed) + + + + + diff --git a/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/run.ps1 b/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/run.ps1 new file mode 100644 index 0000000000..99a4c2e422 --- /dev/null +++ b/UnitsNet.Modular/Samples/UnitsNet.Modular.GettingStarted.Sample/run.ps1 @@ -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." +} diff --git a/UnitsNet.Modular/UnitsNet.Modular.Generator.Tests/DiagnosticDocumentationTests.cs b/UnitsNet.Modular/UnitsNet.Modular.Generator.Tests/DiagnosticDocumentationTests.cs new file mode 100644 index 0000000000..763e2ab39a --- /dev/null +++ b/UnitsNet.Modular/UnitsNet.Modular.Generator.Tests/DiagnosticDocumentationTests.cs @@ -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)); + } +} diff --git a/UnitsNet.Modular/UnitsNet.Modular.Generator/UnitsNetModularGenerator.cs b/UnitsNet.Modular/UnitsNet.Modular.Generator/UnitsNetModularGenerator.cs index fed5383d67..4fbf3379ed 100644 --- a/UnitsNet.Modular/UnitsNet.Modular.Generator/UnitsNetModularGenerator.cs +++ b/UnitsNet.Modular/UnitsNet.Modular.Generator/UnitsNetModularGenerator.cs @@ -20,6 +20,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator private const string GenerationNamespace = "UnitsNet.Modular"; private const string IncludeName = "IInclude"; private const string IncludeProfileName = "IIncludeProfile"; + private const string DocumentationUrl = + "https://github.com/angularsen/UnitsNet/tree/master/UnitsNet.Modular"; private static readonly DiagnosticDescriptor MissingDefinition = new DiagnosticDescriptor( "UNM001", @@ -27,7 +29,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Type '{0}' is selected as a quantity but has no UnitsNet.Modular quantity definition", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#select-quantities"); private static readonly DiagnosticDescriptor EmptyUnitSet = new DiagnosticDescriptor( "UNM002", @@ -35,7 +38,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Pattern '{0}' matched no units in quantity '{1}'", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#filter-units"); private static readonly DiagnosticDescriptor InvalidDefinition = new DiagnosticDescriptor( "UNM003", @@ -43,7 +47,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Quantity '{0}' must define its base unit '{1}'", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#quantity-definition-json"); private static readonly DiagnosticDescriptor InvalidJsonDefinition = new DiagnosticDescriptor( "UNM004", @@ -51,7 +56,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Definition file '{0}' is invalid: {1}", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#quantity-definition-json"); private static readonly DiagnosticDescriptor DuplicateDefinition = new DiagnosticDescriptor( "UNM005", @@ -59,7 +65,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Quantity definition ID '{0}' is provided by more than one JSON file", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#register-a-definition"); private static readonly DiagnosticDescriptor InvalidPattern = new DiagnosticDescriptor( "UNM006", @@ -67,7 +74,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Pattern '{0}' for quantity '{1}' is invalid: {2}", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#filter-units"); private static readonly DiagnosticDescriptor InvalidRelationDefinition = new DiagnosticDescriptor( "UNM010", @@ -75,7 +83,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Relation file '{0}' is invalid: {1}", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#add-quantity-relationships"); private static readonly DiagnosticDescriptor InvalidRelationSet = new DiagnosticDescriptor( "UNM011", @@ -83,7 +92,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "{0}", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#add-quantity-relationships"); private static readonly DiagnosticDescriptor MissingUnitSet = new DiagnosticDescriptor( "UNM012", @@ -91,7 +101,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Unit-set type selected for quantity '{0}' has no UnitSet attribute or no patterns", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#filter-units"); private static readonly DiagnosticDescriptor ConflictingTargetDefinition = new DiagnosticDescriptor( "UNM013", @@ -99,7 +110,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Definitions '{0}' and '{1}' both generate '{2}'", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#module-declaration"); private static readonly DiagnosticDescriptor MultipleModules = new DiagnosticDescriptor( "UNM014", @@ -107,7 +119,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Compilation defines multiple UnitsNet.Modular modules ({0}); compose selections into one module using include profiles", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#module-declaration"); private static readonly DiagnosticDescriptor MissingAffineOffset = new DiagnosticDescriptor( "UNM015", @@ -115,7 +128,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Affine quantity '{0}' requires offset quantity '{1}'; include both quantities in the module", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#arithmetic-and-relationships"); private static readonly DiagnosticDescriptor LegacyUnitsNetReference = new DiagnosticDescriptor( "UNM016", @@ -123,7 +137,8 @@ public sealed class UnitsNetModularGenerator : IIncrementalGenerator "Remove the legacy UnitsNet package reference; a UnitsNet.Modular module generates its own UnitsNet quantity types and contracts", "UnitsNet.Modular", DiagnosticSeverity.Error, - true); + true, + helpLinkUri: DocumentationUrl + "#namespaces"); public void Initialize(IncrementalGeneratorInitializationContext context) { diff --git a/UnitsNet.Modular/UnitsNet.Modular/Modular/AuthoringContracts.cs b/UnitsNet.Modular/UnitsNet.Modular/Modular/AuthoringContracts.cs index b38464f165..3aee4f1ab4 100644 --- a/UnitsNet.Modular/UnitsNet.Modular/Modular/AuthoringContracts.cs +++ b/UnitsNet.Modular/UnitsNet.Modular/Modular/AuthoringContracts.cs @@ -2,56 +2,216 @@ namespace UnitsNet.Modular; -/// Marks an interface as the generation boundary for a consumer-owned quantity module. +/// +/// Marks the single interface that selects the quantities and units generated into a consumer-owned +/// assembly. +/// +/// +/// +/// Apply this attribute to one declarative interface in the project that will own the generated +/// quantity structs and unit enums. The interface is never instantiated or implemented. Instead, +/// inherit , , +/// or to describe the generated API, then build the project. +/// +/// +/// A compilation can declare one module. Built-in quantities retain the familiar UnitsNet +/// namespace and their unit enums use UnitsNet.Units unless a target namespace is supplied. +/// Relationships such as Length / Duration = Speed are generated when every participating +/// quantity is selected. Do not reference the legacy UnitsNet package in the same project. +/// +/// +/// +/// Select Length, Duration, and Speed, including all their units: +/// +/// using UnitsNet.Modular; +/// using Catalog = UnitsNet.Modular.BuiltIns; +/// +/// [UnitsNetModule] +/// internal interface ApplicationUnits : +/// IInclude<Catalog.LengthSpec>, +/// IInclude<Catalog.DurationSpec>, +/// IInclude<Catalog.SpeedSpec>; +/// +/// After building, ordinary application code can use UnitsNet.Length, +/// UnitsNet.Duration, UnitsNet.Speed, and their unit enums. +/// +/// +/// UnitsNet.Modular quick start +/// +/// +/// Complete module configuration +/// [AttributeUsage(AttributeTargets.Interface)] public sealed class UnitsNetModuleAttribute : Attribute { - /// Creates a module that preserves each definition's declared namespace. + /// + /// Creates a module that preserves each selected definition's declared namespace. + /// public UnitsNetModuleAttribute() { } - /// Creates a module that emits all selected quantities into . + /// + /// Creates a module that emits all selected quantities into . + /// + /// + /// The CLR namespace for generated quantity and unit-enum types. The compatibility namespace + /// UnitsNet is special-cased so its unit enums use UnitsNet.Units. + /// public UnitsNetModuleAttribute(string targetNamespace) => TargetNamespace = targetNamespace; - /// Gets the optional CLR namespace override for generated quantities. + /// + /// Gets the optional CLR namespace override applied to every selected quantity definition. + /// public string? TargetNamespace { get; } } -/// Declares glob or regular-expression patterns used to select public units. +/// +/// Declares exact, glob, or regular-expression patterns used to select units from a quantity spec. +/// +/// +/// Patterns match expanded invariant unit names, not abbreviations. Bare patterns use convenient +/// glob matching; prefix a pattern with glob: or regex: to select the matching mode +/// explicitly. The quantity's base unit is always included so every generated quantity remains +/// convertible. A unit set is selected through . +/// +/// +/// +/// [UnitSet("Meter", "Millimeter", "Kilometer")] +/// internal interface CommonLengthUnits; +/// +/// [UnitSet("regex:.*Meter$")] +/// internal interface MeterUnits; +/// +/// +/// +/// Unit filtering documentation +/// [AttributeUsage(AttributeTargets.Interface)] [CLSCompliant(false)] public sealed class UnitSetAttribute : Attribute { - /// Creates a reusable unit selection. + /// Creates a reusable unit selection from one or more unit-name patterns. + /// + /// Exact, glob, or regex:-prefixed patterns matched against invariant unit names. + /// public UnitSetAttribute(params string[] patterns) => Patterns = patterns; - /// Gets the unit-name patterns. + /// Gets the unit-name patterns used by this selection. public string[] Patterns { get; } } -/// Binds a quantity spec to a stable semantic quantity ID. +/// Binds a quantity spec interface to a stable semantic quantity ID. +/// +/// Use this attribute when authoring a custom quantity or a definition package. The semantic ID +/// connects the spec to a *.unitsnet.json definition supplied as an +/// AdditionalFiles item. The recommended type name is the generated quantity name followed +/// by Spec. Published definition packages should expose public specs and keep their semantic +/// IDs stable; the package contributes definitions, while the consuming application owns the +/// generated quantity types. +/// +/// +/// +/// [QuantitySpec("Contoso.Measurements.WidgetCount")] +/// public interface WidgetCountSpec; +/// +/// [UnitsNetModule] +/// internal interface ApplicationUnits : IInclude<WidgetCountSpec>; +/// +/// +/// +/// Custom quantity documentation +/// +/// +/// Definition package documentation +/// [AttributeUsage(AttributeTargets.Interface)] public sealed class QuantitySpecAttribute : Attribute { /// Creates a binding to the semantic quantity ID . + /// + /// The definition's stable namespace-qualified ID, such as + /// Contoso.Measurements.WidgetCount. + /// public QuantitySpecAttribute(string semanticId) => SemanticId = semanticId; /// Gets the stable semantic quantity ID. public string SemanticId { get; } } -/// Selects all units from a quantity spec. +/// Selects a quantity spec and all of its units for generation. +/// +/// A built-in spec from UnitsNet.Modular.BuiltIns or a custom interface marked with +/// . +/// +/// +/// Inherit this interface from a module or a reusable profile. +/// The spec type is an authoring input; the generator emits the corresponding quantity struct and +/// unit enum, not a concrete implementation of the spec interface. +/// +/// +/// +/// [UnitsNetModule] +/// internal interface ApplicationUnits : +/// IInclude<UnitsNet.Modular.BuiltIns.LengthSpec>; +/// +/// public interface IInclude { } -/// Selects units matching from a quantity spec. +/// +/// Selects a quantity spec and the units matching for generation. +/// +/// +/// A built-in or custom quantity spec. +/// +/// +/// An interface marked with . +/// +/// +/// The base unit is generated even when none of the unit-set patterns select it, preserving a valid +/// conversion anchor. A direct filtered include on the module overrides a profile's selection for +/// the same quantity. +/// +/// +/// +/// [UnitSet("Meter", "Kilometer")] +/// internal interface ApplicationLengthUnits; +/// +/// [UnitsNetModule] +/// internal interface ApplicationUnits : +/// IInclude<UnitsNet.Modular.BuiltIns.LengthSpec, ApplicationLengthUnits>; +/// +/// public interface IInclude { } -/// Composes a reusable quantity-selection profile into a module. +/// Composes a reusable quantity-selection profile into a module or another profile. +/// +/// An interface that composes quantity specs, unit sets, or other profiles. +/// +/// +/// Profiles provide reusable defaults. Direct quantity selections on the module override a profile's +/// unit selection for that quantity. Use +/// UnitsNet.Modular.Profiles.AllQuantitiesProfile when source compatibility with the complete +/// built-in catalog is preferred over a lean selection. +/// +/// +/// +/// internal interface MechanicsProfile : +/// IInclude<UnitsNet.Modular.BuiltIns.LengthSpec>, +/// IInclude<UnitsNet.Modular.BuiltIns.DurationSpec>, +/// IInclude<UnitsNet.Modular.BuiltIns.SpeedSpec>; +/// +/// [UnitsNetModule] +/// internal interface ApplicationUnits : IIncludeProfile<MechanicsProfile>; +/// +/// +/// +/// Profile documentation +/// public interface IIncludeProfile { }