From 28f190714c0306410364affd42fe33ab32edd378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Gro=C3=9F?= Date: Fri, 30 Oct 2026 12:19:46 +0100 Subject: [PATCH 1/3] feat: generate the arithmetic operators a plain value object declares A plain value object that declares IAdditionOperators, ISubtractionOperators, IMultiplyOperators, IDivisionOperators, IModulusOperators, IUnaryNegationOperators or IUnaryPlusOperators, with itself as the first type argument, gets those operators and their checked forms, where the wrapped type implements the same interface with the value object replaced by itself. `Money * 3m` and `Money / Money` as a decimal ratio work the same way: an operand or result of another type passes through. Each operator calls the wrapped type's through GeneratedArithmetic, a new [EditorBrowsable(Never)] helper in the base package: a call through a type parameter constrained to the interface, unchecked or checked as stated. Written as `left.Value + right.Value`, two bytes were added as ints and char's explicit operators were out of reach. No reflection, so Native AOT needs nothing; the smoke test's aot consumer uses one directly, through generic code and in a checked context. Never by default and never on a validated value object: the interface then stays unimplemented and fails with CS0535, as before. A hand-written operator for the same operands, or an explicit implementation of the interface, its own or a base record's, is kept with its checked form (decision 121, refining 28), so 2.3 code builds unchanged and nothing new is an Error. A hand-written checked operator alone keeps failing with CS9025. Input class: the declaration's shape (GTS), over the generated-type axes. The differential oracle ArithmeticParityTests runs 22 declarations (sbyte to BigInteger, char, Half, float, double, decimal, a record class over int and double, a nested and an internal one) against the wrapped type's own operators, unchecked and checked, on bounds, zero, one, minus one and the floating-point specials. Seeded: no checked forms (20 tests fail), a checked + beside a hand-written + (1), an explicit implementation not kept (2), operators on a validated value object (1), a lone hand-written checked + completed (2), % not generated (CS0535). EF Core translates an operator between two value objects as the wrapped type's arithmetic (ef_add on SQLite); one with another operand throws (#186), and a hand-written one would run as the database's arithmetic (#187). Closes #184 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 + ...opt-in-and-a-hand-written-one-is-a-seam.md | 67 + design/adr/README.md | 1 + design/aspects.md | 12 +- design/packages.md | 4 +- docs/value-objects.md | 47 +- .../AspectOrder.cs | 1 + src/CodoMetis.TypeKit.Generators/README.md | 10 + .../ValueObjectArithmeticAspect.Templates.cs | 57 + .../ValueObjectArithmeticAspect.cs | 192 ++ .../ValueObjectAspect.cs | 1 + .../ValueObjectDeclaration.cs | 9 + .../CompilerServices/GeneratedArithmetic.cs | 139 ++ .../ArithmeticQueryTests.cs | 70 + .../Probes.cs | 14 + .../ArithmeticBuildTests.cs | 89 + .../ArithmeticParityTests.cs | 186 ++ .../ArithmeticTests.cs | 213 ++ .../GeneratedSurface.verified.txt | 1898 +++++++++++++++++ .../Docs/ValueObjectsSamples.cs | 31 + test/oracles/axes.md | 4 + test/oracles/claims.json | 134 ++ test/oracles/oracles.json | 6 + ...ithmetic-checked-forms-not-generated.patch | 17 + .../Consumers/aot/Domain/Shop.cs | 5 +- .../Consumers/aot/Web/Program.cs | 13 + tools/ConsumerSmokeTest/Program.cs | 1 + 27 files changed, 3217 insertions(+), 8 deletions(-) create mode 100644 design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md create mode 100644 src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.Templates.cs create mode 100644 src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs create mode 100644 src/CodoMetis.TypeKit/CompilerServices/GeneratedArithmetic.cs create mode 100644 test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/ArithmeticQueryTests.cs create mode 100644 test/CodoMetis.TypeKit.Generators.Tests/ArithmeticBuildTests.cs create mode 100644 test/CodoMetis.TypeKit.Generators.Tests/ArithmeticParityTests.cs create mode 100644 test/CodoMetis.TypeKit.Generators.Tests/ArithmeticTests.cs create mode 100644 test/oracles/seeds/arithmetic-checked-forms-not-generated.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 12eb00bc..4f12319d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ section is the GitHub release's notes. ## 2.4.0 — Unreleased +### Added + +- Generators: a plain value object that declares `IAdditionOperators`, `ISubtractionOperators`, `IMultiplyOperators`, `IDivisionOperators`, `IModulusOperators`, `IUnaryNegationOperators` or `IUnaryPlusOperators` gets their operators, `Money + Money` or `Money * 3m`, each the underlying type's own, overflow and `checked` context included; nothing without the interface, nothing on a validated value object, and an operator you wrote is kept ([Add, subtract and scale](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/value-objects.md#add-subtract-and-scale)). + ### Changed - Packages: the public surface is checked against 2.3.0's, so nothing 2.3.0 ships is removed or changed in 2.4.0. diff --git a/design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md b/design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md new file mode 100644 index 00000000..b8f6bbf2 --- /dev/null +++ b/design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md @@ -0,0 +1,67 @@ +# 0121. Arithmetic operators are opt-in by their interfaces, and a hand-written one is a seam + +Date: 2026-10-30 +Status: accepted + +Context: [#184](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/184), under the 2.4.0 epic +[#177](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/177); [Aspects](../aspects.md); refines +[decision 28](0028-seams-declared-everything-else-refused.md) under [decision 34](0034-errors-fixed-within-major-version-warnings.md). +EricksonLopez.DomainPrimitives generates arithmetic on every numeric primitive +([KB-055](../knowledge/KB-055-ericksonlopez-domainprimitives.md)); TypeKit wrote `Money.From(a.Value + b.Value)`. + +**A plain value object gets the arithmetic operators of the operator interfaces it declares, and nothing +else.** `ValueObjectArithmeticAspect` reads `IAdditionOperators`, `ISubtractionOperators`, +`IMultiplyOperators`, `IDivisionOperators`, `IModulusOperators`, `IUnaryNegationOperators` and +`IUnaryPlusOperators` among the interfaces the value object implements, directly or through another +interface, whose first type argument is the value object. It generates an interface's operator, and its +`checked` form where the interface has one, when the wrapped type implements the same interface with the +value object replaced by the wrapped type throughout: `IMultiplyOperators` needs +`IMultiplyOperators`, `IDivisionOperators` (a ratio) needs +the same. An operand or a result of another type passes through as it is. + +- **Opt-in, never by default, never by attribute.** Declaring the interface is what a generic-math constraint + reads anyway, and the operators a value object has are visible on its declaration. `TSelf * TSelf` is + allowed when declared: whether two amounts multiply is the domain's question, not TypeKit's. +- **The wrapped type's operator, through `GeneratedArithmetic`** in the base package: a call through a type + parameter constrained to the interface, unchecked or checked, as `GeneratedFormatting` calls formatting. + Written as `left.Value + right.Value`, two `byte`s were added as `int`s, and `char`, whose operator + interfaces are explicit, could not be reached at all. Each helper states its context (`unchecked(...)`, + `checked(...)`), so the consumer's `CheckForOverflowUnderflow` changes nothing: the context picks the + operator, as it picks the wrapped type's, and the operator does what the wrapped type's does in it. No + reflection, so Native AOT needs nothing (the smoke test's `aot` consumer calls one directly, through + generic code and in a checked context). +- **Never on a validated value object.** An operator cannot return a `Result`, and a sum outside the range + `Create` accepts would have to throw or skip `Create`. The declared interface stays unimplemented and the + build fails with CS0535, as it did before. So does an interface the wrapped type does not match: + `DateTime`, `TimeSpan`, `DateOnly` and `Guid` implement none of the operator interfaces (measured in the + .NET 10 reference assemblies: they were dropped before .NET 7 shipped). +- **A hand-written operator is a seam.** One the value object declares for the same operands, of either kind, + whatever it returns, or an explicit implementation of the interface, its own or a base record's, is kept, + and neither form of that operator is generated; the operators not written are. Decision 28 would make it + CMTK1011, but code that implemented these interfaces by hand on 2.3 must build unchanged on 2.4, and + decision 34 lets no minor report more under an Error. Nothing generated depends on an arithmetic operator, + so a kept one disagrees with nothing, which is what a seam needs. Its `checked` form goes with it: a + generated `checked +` beside a hand-written `+` did the wrapped type's arithmetic in a checked context and + the user's everywhere else. The other way round, a hand-written `checked +` without its `+` is CS9025, and + generating the `+` would have built it, adding by the wrapped type's rules outside a checked context and + the user's inside one; it is kept failing. A public operator generated beside a base record's explicit implementation + would not implement the interface, which the base does: generic code would reach the base's, `a + b` + ours. +- **One operator per signature.** C# allows one `operator /(Money, Money)`, so of two interfaces that ask for + it, the first declared gets it and the second fails the build (CS0738), to be implemented explicitly by hand. +- **A record class's operand of its own type** is checked for `null` and throws `ArgumentNullException` + naming it, where dereferencing it threw `NullReferenceException` inside generated code. +- **In an EF Core query** an operator between two value objects is translated as the wrapped type's + arithmetic over the columns and the database computes it (`ef_add` on SQLite, measured); EF never runs + the operator. One with another operand or result throws there, since EF applies the value object's + converter to the other side ([#186](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/186)), + and a hand-written operator would run as the database's arithmetic, not as written + ([#187](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/187)). +- **Left out:** `++` and `--`, the bitwise and shift operators, and `T * Money` (C# cannot declare it on + `Money`, whose interface's first type argument must be itself). + +`ArithmeticParityTests` holds it as a differential oracle: 22 declarations (every numeric wrapped type from +`sbyte` to `BigInteger`, `char`, `Half`, `float`, `double`, `decimal`, both record shapes, nested and +internal) against the wrapped type's own operators, unchecked and checked, over a catalogue of bounds, zero, +one, minus one and the floating-point specials. `ArithmeticTests` holds the seam and the other operand forms, +`ArithmeticBuildTests` the CS0535, CS0738 and CS9025 cases, and `ArithmeticQueryTests` the EF Core translation. diff --git a/design/adr/README.md b/design/adr/README.md index 245695ff..64063bdd 100644 --- a/design/adr/README.md +++ b/design/adr/README.md @@ -127,3 +127,4 @@ per area ([design](../plan.md)) link to the records they rest on. | [0118](0118-an-error-may-learn-a-form-only-new-api-can-write.md) | An Error may report, in a minor version, a form that only an API new in that version can write, since no build that worked before contains it: CMTK0003's dropped assertion of `CodoMetis.TypeKit.Testing` on a `Task` or `ValueTask` in 2.3.0, its `ConfigureAwait`, `WaitAsync` and branch forms (#152) included, known by the package's assembly; the forms freeze at the tag; refines decision 34; one exception the maintainer took into 2.3.0, a dropped `Task.WhenAny` of tasks of results (#159) | 2026-10-29 | accepted; widened by [0119](0119-a-gap-found-before-the-tag-ships-in-that-release.md) | | [0119](0119-a-gap-found-before-the-tag-ships-in-that-release.md) | A form an Error misses, found before its version is tagged, is reported in that version when it loses what the rule protects in every build, though code built on the previous minor can contain it: 2.3.0's CMTK0003 on a `Task.WhenAny` of results (#159), `Wait()` on a branch (#160), an array in `Task.WaitAll`/`WaitAny` (#161), a dropped `Task.WhenEach` (#162), and a `WhenAny` of held tasks silent; the rule stops at the tag, after which decision 34 holds; refines 34 and 118 | 2026-10-29 | accepted | | [0120](0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md) | A `net11.0` target joins a 2.x minor when a feature needs it, and a dependency floor may rise in a minor, on every target or on `net11.0` alone, if it is documented (the Semantic Versioning FAQ calls a dependency update with an unchanged public API compatible; a project that references the dependency lower gets NU1605, measured on SDK 11 RC1, so the changelog names the floor, who it reaches and both ways out, and the smoke test shows it); a union new on `net11.0` is an addition and may join a 2.x minor (a `LangVersion` 14 consumer who matches on it gets CS9327, and on Metalama 2026.1 CS8652, measured); Metalama is one version for every target and TypeKit follows the maintained Metalama year in a minor: 2.3 and 2.4 stay on 2026.1 and 2.5.0 moves both targets to 2027.0 once stable (a direct 2026.1 reference gets NU1605 and stays on 2.4.x; a woven `net11.0` project goes from C# 14 to 15; conditions: a license-free CI and clean-container build, the ceiling and the 2.4.x line documented); the SDK 11 spike comes first (#170); 3.0.0 stays the `net10.0` drop and a change of an existing contract, timed to .NET 12 LTS, and .NET 11 is no reason for a major; refines decision 44 | 2026-10-29 | accepted | +| [0121](0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md) | A plain value object gets the arithmetic operators (`+`, `-`, `*`, `/`, `%`, unary `-` and `+`, and their `checked` forms) of the operator interfaces it declares, where the wrapped type implements the same interface with the value object replaced by itself; each calls the wrapped type's through `GeneratedArithmetic`, a constrained generic call with its context stated; never by default and never on a validated value object, which keeps CS0535; a hand-written operator for the same operands, or an explicit implementation of the interface, its own or a base record's, is a seam, kept with its `checked` form, so 2.3 code builds unchanged; refines decision 28 | 2026-10-30 | accepted | diff --git a/design/aspects.md b/design/aspects.md index 3c1a23bd..c36016ab 100644 --- a/design/aspects.md +++ b/design/aspects.md @@ -1,6 +1,6 @@ # Aspects -- **Aspect-class count.** Eleven classes: ten for value objects and `ClosedSetAspect`, whose design is +- **Aspect-class count.** Twelve classes: eleven for value objects and `ClosedSetAspect`, whose design is its own note ([Closed sets](closed-sets.md)). The Metalama Open Source edition (MIT) has no aspect-class limit (pricing page, 2026-09-26). The unlicensed CI build confirms it (phase 6). - **Entry points.** `ValueObjectAspect` introduces three private helpers on every value @@ -9,7 +9,15 @@ `__TryFromText` (`false`). A plain value object constructs directly; a validated one calls `Create` through `GeneratedFactories` in the base package. The exception is the explicit `IValueObjectMaterializer.Materialize`, by contract. `MinValue`/`MaxValue` are only generated for - a plain value object. + a plain value object, and so are arithmetic operators, which wrap their result through the private + constructor as `From` does. +- **Arithmetic is opt-in (decided 2026-10-30, [decision 121](adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md)).** + `ValueObjectArithmeticAspect` generates the operators of the operator interfaces a plain value object + declares, where the wrapped type implements the same interface with the value object replaced by itself, + each calling the wrapped type's through `GeneratedArithmetic`, a constrained generic call with its context + stated. A hand-written operator for the same operands, or an explicit implementation of the interface, its + own or a base record's, is a seam: kept with its `checked` form, never CMTK1011, so code that wrote them on + 2.3 builds unchanged. - **Declarations that cannot be generated** are errors, so no type is left half-generated: CMTK1000/1001/1002 (not `partial`, not a record, a struct not `readonly`), CMTK1003 (more than one marker), CMTK1004 (a validated marker whose first type argument is another type), CMTK1005 (a diff --git a/design/packages.md b/design/packages.md index 54eb4bcd..f5f1505d 100644 --- a/design/packages.md +++ b/design/packages.md @@ -2,7 +2,7 @@ | Package | Depends on | Metalama | Contents | |---|---|---|---| -| `CodoMetis.TypeKit` | Analyzers (flows, [Analyzers](analyzers.md)) | **no** | `Option`, `Result`, `Result` and their extensions, all carrying `NotWireTypeJsonConverterFactory` ([Option and Result](option-result.md)); the value-object contracts `IValueObject<,>`, `IPlainValueObject<,>`, `IValue`, `IValidatedValue<,,>`, `IValueObjectMaterializer<,>` ([EF Core](ef-core.md)); the closed-set contracts `IClosedSet<>`, `IFault`, `IFaultSet<>` and `FaultKind`; `Fault` and `Fault.Of`, and `Fault` and `Fault`, a fault of one of several types ([decision 78](adr/0078-fault-sets-compose-side-by-side.md)); `Patch`, `PatchState` and `PatchJsonConverterFactory`; `Result.Catch`/`CatchAsync`; the text guards `AsText()`/`AsIdentifier()` (`StringTextExtensions`) and `TextFault`; the type-argument constraint attributes `[Abstract]`, `[Concrete]`, `[Sealed]`, `[Record]`; `RequireCustomInitializationAttribute`, `RequireParameterlessConstructorAttribute` and `ConstructorAccessibility`; `GuidValueExtensions` (`OrderId.New()` from a version 7 Guid); and what only generated code and the satellites use ([Discovery](discovery.md), [Aspects](aspects.md), [Native AOT](native-aot.md)): `GeneratedValueObjectAttribute<,>`, `GeneratedClosedSetAttribute<>`, `IValueObjectVisitor<>`, `IClosedSetVisitor<>`, `TranslatedAsWrappedValueAttribute`, `HandWrittenOrderingAttribute`, `GeneratedFactories`, `GeneratedParsing`, `GeneratedFormatting`, `GeneratedJson`, `GeneratedJsonPlan<>`, `GeneratedClosedSets`, `IStoredJsonConverterSource` | +| `CodoMetis.TypeKit` | Analyzers (flows, [Analyzers](analyzers.md)) | **no** | `Option`, `Result`, `Result` and their extensions, all carrying `NotWireTypeJsonConverterFactory` ([Option and Result](option-result.md)); the value-object contracts `IValueObject<,>`, `IPlainValueObject<,>`, `IValue`, `IValidatedValue<,,>`, `IValueObjectMaterializer<,>` ([EF Core](ef-core.md)); the closed-set contracts `IClosedSet<>`, `IFault`, `IFaultSet<>` and `FaultKind`; `Fault` and `Fault.Of`, and `Fault` and `Fault`, a fault of one of several types ([decision 78](adr/0078-fault-sets-compose-side-by-side.md)); `Patch`, `PatchState` and `PatchJsonConverterFactory`; `Result.Catch`/`CatchAsync`; the text guards `AsText()`/`AsIdentifier()` (`StringTextExtensions`) and `TextFault`; the type-argument constraint attributes `[Abstract]`, `[Concrete]`, `[Sealed]`, `[Record]`; `RequireCustomInitializationAttribute`, `RequireParameterlessConstructorAttribute` and `ConstructorAccessibility`; `GuidValueExtensions` (`OrderId.New()` from a version 7 Guid); and what only generated code and the satellites use ([Discovery](discovery.md), [Aspects](aspects.md), [Native AOT](native-aot.md)): `GeneratedValueObjectAttribute<,>`, `GeneratedClosedSetAttribute<>`, `IValueObjectVisitor<>`, `IClosedSetVisitor<>`, `TranslatedAsWrappedValueAttribute`, `HandWrittenOrderingAttribute`, `GeneratedFactories`, `GeneratedParsing`, `GeneratedFormatting`, `GeneratedArithmetic`, `GeneratedJson`, `GeneratedJsonPlan<>`, `GeneratedClosedSets`, `IStoredJsonConverterSource` | | `CodoMetis.TypeKit.Analyzers` | — | no | Roslyn analyzers and code fixes ([Analyzers](analyzers.md)). Nobody references it directly: it reaches every consumer through the base package | | `CodoMetis.TypeKit.Generators` | TypeKit, Metalama.Framework 2026.1.x (flows) | yes | Internal aspects (the value-object family and `ClosedSetAspect`), internal `TransitiveProjectFabric`, `AspectOrder`. Future generated type families join this package too: one fabric, one aspect order | | `CodoMetis.TypeKit.EntityFrameworkCore` | TypeKit, EF Core Relational | no | Generic converter, convention, `.Value` / `GetValue` / `ValueOrNull` translators, `UseTypeKit()` | @@ -25,7 +25,7 @@ objects from a referenced domain assembly never needs Metalama itself. | `CodoMetis.TypeKit` (2.0.0) | `Fault`, `FaultRuleExtensions` and `FaultClosedSetRuleExtensions` (2.1), `Patch`, `PatchState`, `PatchJsonConverterFactory`, `StringTextExtensions`, `TextFault`, the constraint attributes, `RequireParameterlessConstructorAttribute`, `ConstructorAccessibility` | | `CodoMetis.TypeKit.ClosedSets` | `IClosedSet<>`, `IFault`, `IFaultSet<>`, `FaultKind`: what a user declares a closed set or a fault set with | | `CodoMetis.TypeKit.ValueObjects` | Every contract a user declares or constrains on: `IValue`, `IValidatedValue`, `IValueObject`, `IPlainValueObject`, `IValueObjectMaterializer`, and `StoredJsonConverterFactory` | -| `CodoMetis.TypeKit.CompilerServices` | What only generated code and the satellites touch, all `[EditorBrowsable(Never)]`: `GeneratedValueObjectAttribute`, `GeneratedClosedSetAttribute`, `IValueObjectVisitor`, `IClosedSetVisitor`, `TranslatedAsWrappedValueAttribute`, `HandWrittenOrderingAttribute`, `GeneratedFactories`, `GeneratedParsing`, `GeneratedFormatting`, `GeneratedJson`, `GeneratedJsonPlan`, `GeneratedClosedSets`, `IStoredJsonConverterSource`. The name follows `System.Runtime.CompilerServices` | +| `CodoMetis.TypeKit.CompilerServices` | What only generated code and the satellites touch, all `[EditorBrowsable(Never)]`: `GeneratedValueObjectAttribute`, `GeneratedClosedSetAttribute`, `IValueObjectVisitor`, `IClosedSetVisitor`, `TranslatedAsWrappedValueAttribute`, `HandWrittenOrderingAttribute`, `GeneratedFactories`, `GeneratedParsing`, `GeneratedFormatting`, `GeneratedArithmetic`, `GeneratedJson`, `GeneratedJsonPlan`, `GeneratedClosedSets`, `IStoredJsonConverterSource`. The name follows `System.Runtime.CompilerServices` | | `CodoMetis.TypeKit.EntityFrameworkCore` | `ValueObjectConverter<,>`, `ClosedSetConverter`; the plugins, internal | | `CodoMetis.TypeKit.AspNetCore` | the schema transformers, internal; `FaultProblem`, `FaultResponse`, `FaultResponseOptions`, `FaultStatusAttribute`, `IFaultFeature` | | `Microsoft.AspNetCore.Http` | `ToHttpResult()`, for a `Result` and for a closed set's fault (`TypeKitHttpResultExtensions`, `TypeKitClosedSetHttpResultExtensions`), beside ASP.NET's own `Results` ([decision 22](adr/0022-registration-extensions-live-framework-s-namespaces.md)) | diff --git a/docs/value-objects.md b/docs/value-objects.md index d31036e4..82aadc85 100644 --- a/docs/value-objects.md +++ b/docs/value-objects.md @@ -194,6 +194,48 @@ bool ordered = Percentage.FromKnownGood(5) < Percentage.FromKnownGood(50); // - **Culture.** The generated `ToString()` uses the invariant culture, so `Weight.From(1.5m)` prints `1.5` on any machine. +### Add, subtract and scale + +```csharp +public readonly partial record struct Money : IValue, + IAdditionOperators, + ISubtractionOperators, + IMultiplyOperators, // Money * 3m + IDivisionOperators; // two amounts make a ratio +``` + +```csharp +Money total = Money.From(10.50m) + Money.From(4.50m); // 15.00 +Money tripled = total * 3m; // 45.00 +decimal share = Money.From(3m) / Money.From(4m); // 0.75 +``` + +Declare the operator interfaces you want, and TypeKit generates their operators. Each operator does what +the underlying type's operator does: the same result, the same overflow, the same checked context. +Nothing is generated unless you declare it. + +The first type argument is the value object. The other operand and the result can each be the value +object or another type, as long as the underlying type has that operator itself. `Money * 3m` needs +`decimal * decimal`, and `Money / Money` returning a `decimal` needs the same. + +**Good to know** + +- **Which interfaces.** `IAdditionOperators`, `ISubtractionOperators`, `IMultiplyOperators`, + `IDivisionOperators`, `IModulusOperators`, `IUnaryNegationOperators` and `IUnaryPlusOperators`. +- **Numbers have them, dates don't.** The number types and `char` implement these interfaces. + `DateTime`, `TimeSpan`, `DateOnly` and `Guid` implement none of them, so a value object over one gets + no operator, and the interface you declared fails the build (CS0535). +- **Plain value objects only.** A validated value object gets none. An operator can't return a + `Result`, and a sum outside the range `Create` accepts would have to throw. The interface you + declared fails the build (CS0535) until you write the operator yourself. +- **Your own operator is kept.** Write one yourself, or implement the interface explicitly, and TypeKit + keeps it, with its checked form. It generates only the operators you haven't written. +- **The checked context.** `checked(a + b)` uses the checked operator, as it would for an `int`. A value + object over an `int` throws `OverflowException` there, and wraps around outside it. +- **In an EF Core query.** An operator between two value objects runs in the database, as it would for + the underlying type: `o.Price + o.Tax > limit` keeps the rows memory keeps. One with another operand or + result, such as `o.Price * 2m`, throws there for now. Write `o.Price.Value * 2m` instead. + ## Enums, dates and other types ```csharp @@ -290,7 +332,7 @@ else fails the build (CMTK0004, [EF Core](ef-core.md#reading-back-without-valida You can write five members yourself: `TryFrom`, `FromKnownGood`, `Revalidate`, `CompareTo(TSelf)` and `ToString()`. TypeKit keeps yours and builds everything that depends on them from them. Writing any other generated member by hand fails the build (CMTK1011), and so does a second field next to the -value (CMTK1012). +value (CMTK1012). An arithmetic operator you write is kept too ([Add, subtract and scale](#add-subtract-and-scale)). ```csharp public readonly partial record struct Priority : IValue @@ -332,7 +374,8 @@ don't collide. The OpenAPI document names the schema the same way, `ShipmentId`. The interfaces use static abstract members, so one generic method can serve every value object of a kind. Where the underlying type allows it, the generated types also implement `IEqualityOperators`, -`IComparisonOperators` and `IMinMaxValue`, so they fit generic-math constraints. +`IComparisonOperators` and `IMinMaxValue`, so they fit generic-math constraints. The arithmetic operator +interfaces you declare fit them too. ```csharp static Result, TFault> CreateAll(IEnumerable values) diff --git a/src/CodoMetis.TypeKit.Generators/AspectOrder.cs b/src/CodoMetis.TypeKit.Generators/AspectOrder.cs index dc795319..6dbf59cb 100644 --- a/src/CodoMetis.TypeKit.Generators/AspectOrder.cs +++ b/src/CodoMetis.TypeKit.Generators/AspectOrder.cs @@ -20,6 +20,7 @@ typeof(ValueObjectFormattableAspect), typeof(ValueObjectComparableAspect), typeof(ValueObjectMinMaxValueAspect), + typeof(ValueObjectArithmeticAspect), typeof(ValueObjectTypeConverterAspect), typeof(ValueObjectConvertibleAspect), typeof(ValueObjectCompanionAspect) diff --git a/src/CodoMetis.TypeKit.Generators/README.md b/src/CodoMetis.TypeKit.Generators/README.md index 8d3818be..438acf9b 100644 --- a/src/CodoMetis.TypeKit.Generators/README.md +++ b/src/CodoMetis.TypeKit.Generators/README.md @@ -187,6 +187,7 @@ type of your own that does. | `IEqualityOperators` | every value object | The generic-math form of `==`/`!=`, so a value object satisfies that constraint. | | `IComparable`, `IComparable`, `IComparisonOperators`, `<` `>` `<=` `>=` | when the underlying type is comparable | Strings compare ordinally, so ordering agrees with equality. | | `MinValue`, `MaxValue`, `IMinMaxValue` | `IValue` over a type whose own static `MinValue`/`MaxValue` are of that type | Never for a validated value object. | +| `+`, `-`, `*`, `/`, `%`, unary `-` and `+`, and their `checked` forms | `IValue` that declares `IAdditionOperators`, `ISubtractionOperators`, `IMultiplyOperators`, `IDivisionOperators`, `IModulusOperators`, `IUnaryNegationOperators` or `IUnaryPlusOperators`, where `T` implements the same interface with `T` in place of the value object | Only what you declare, and never for a validated value object. Each operator calls `T`'s, so the result, the overflow and the checked context are `T`'s. A `null` operand of a record class throws `ArgumentNullException`. An operator you write is kept (see "Write your own members" below). | | `[TypeConverter]` with a nested converter | when parsing is generated | MVC model binding and the reflection-based configuration binder use it. The configuration binding source generator, which `PublishAot` and `PublishTrimmed` turn on, doesn't bind value objects and leaves them `default`. Bind the underlying type there. | | `IConvertible` | when the underlying type is convertible | Explicit, so `Convert.ToInt64(quantity)` and `Convert.ChangeType(quantity, typeof(Quantity))` work. | | `{TSelf}Extensions.GetValue()` and `ValueOrNull()` | every value object an extension class can reach (not a `private` or `protected` nested one) | For EF Core queries, where they translate to the column. | @@ -215,6 +216,15 @@ method that calls `From` or `Create`. A value object may declare its own `TryFrom`, `FromKnownGood`, `Revalidate`, `CompareTo(TSelf)` and `ToString()`. Each one is kept, and whatever depends on it is built from it. +An arithmetic operator you write is kept as well, and so is an explicit implementation of its +interface, yours or a base record's. Its `checked` form is then yours too: TypeKit generates neither +form of that operator, and generates the ones you didn't write. A `checked` operator you write needs the +plain one beside it, as C# requires (CS9025), and TypeKit doesn't write that one for you either. Code +that wrote these operators by hand before 2.4 builds unchanged. C# allows one operator per pair of operand types, so of two interfaces +that ask for the same operator, such as `IDivisionOperators` and +`IDivisionOperators`, only the first is generated, and the second fails the build +(CS0738) until you implement it explicitly. + Writing any other generated member or attribute by hand fails the build, naming it (CMTK1011). That covers: diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.Templates.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.Templates.cs new file mode 100644 index 00000000..2c56be3f --- /dev/null +++ b/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.Templates.cs @@ -0,0 +1,57 @@ +using Metalama.Framework.Aspects; +using Metalama.Framework.Code; +using Metalama.Framework.Code.SyntaxBuilders; + +namespace CodoMetis.TypeKit.Generators; + +internal sealed partial class ValueObjectArithmeticAspect +{ + // The wrapped type's operator, through GeneratedArithmetic: a call through a type parameter + // constrained to the interface, which never boxes, reaches an explicit implementation, and adds two + // bytes as bytes where `left.Value + right.Value` added them as ints. A result of the value object's + // type is wrapped by the private constructor, as From does, since a plain value object has no rules. + // A record class's operand can be null, and is refused naming it rather than dereferenced. + + [Template] + public static dynamic? BinaryOperatorTemplate(dynamic? left, dynamic? right) + { + var tag = (ArithmeticOperatorArguments)meta.Tags.Source!; + + if (meta.CompileTime(tag.ChecksNull)) + { + ArgumentNullException.ThrowIfNull(left); + + if (meta.CompileTime(tag.UnwrapsRight)) + ArgumentNullException.ThrowIfNull(right); + } + + // Inlined, not held in a local: a local of the template is a local of the woven operator. + var leftOperand = Operand("left.Value", tag.Helper.Parameters[0]); + var rightOperand = Operand(tag.UnwrapsRight ? "right.Value" : "right", tag.Helper.Parameters[1]); + + if (meta.CompileTime(tag.Constructor is not null)) + return tag.Constructor!.Invoke(tag.Helper.Invoke(leftOperand.Value, rightOperand.Value)); + + return tag.Helper.Invoke(leftOperand.Value, rightOperand.Value); + } + + [Template] + public static dynamic? UnaryOperatorTemplate(dynamic? value) + { + var tag = (ArithmeticOperatorArguments)meta.Tags.Source!; + + if (meta.CompileTime(tag.ChecksNull)) + ArgumentNullException.ThrowIfNull(value); + + var operand = Operand("value.Value", tag.Helper.Parameters[0]); + + if (meta.CompileTime(tag.Constructor is not null)) + return tag.Constructor!.Invoke(tag.Helper.Invoke(operand.Value)); + + return tag.Helper.Invoke(operand.Value); + } + + /// An operand typed as the helper's parameter takes it, so the woven call needs no cast. + [CompileTime] + private static IExpression Operand(string code, IParameter parameter) => ExpressionFactory.Parse(code, parameter.Type, false); +} diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs new file mode 100644 index 00000000..5a9acb86 --- /dev/null +++ b/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs @@ -0,0 +1,192 @@ +using System.Numerics; +using CodoMetis.TypeKit.CompilerServices; +using Metalama.Framework.Aspects; +using Metalama.Framework.Code; + +namespace CodoMetis.TypeKit.Generators; + +/// An arithmetic operator interface, the operators it declares, and the helpers that call the wrapped type's. +[CompileTime] +internal sealed class ArithmeticOperator +{ + public required OperatorKind Kind { get; init; } + public required OperatorKind? CheckedKind { get; init; } + public required string Helper { get; init; } + public required string? CheckedHelper { get; init; } + + /// Two operands, TSelf and TOther, or one. + public required bool IsBinary { get; init; } + + /// The operator interface is, or for any other interface. + public static ArithmeticOperator? Of(INamedType definition) + { + ArithmeticOperator Binary(OperatorKind kind, OperatorKind? checkedKind, string helper, string? checkedHelper) => + new() { Kind = kind, CheckedKind = checkedKind, Helper = helper, CheckedHelper = checkedHelper, IsBinary = true }; + + ArithmeticOperator Unary(OperatorKind kind, OperatorKind? checkedKind, string helper, string? checkedHelper) => + new() { Kind = kind, CheckedKind = checkedKind, Helper = helper, CheckedHelper = checkedHelper, IsBinary = false }; + + if (definition.Equals(typeof(IAdditionOperators<,,>).ToNamedType())) + return Binary(OperatorKind.Addition, OperatorKind.CheckedAddition, nameof(GeneratedArithmetic.Add), nameof(GeneratedArithmetic.CheckedAdd)); + if (definition.Equals(typeof(ISubtractionOperators<,,>).ToNamedType())) + return Binary(OperatorKind.Subtraction, OperatorKind.CheckedSubtraction, nameof(GeneratedArithmetic.Subtract), nameof(GeneratedArithmetic.CheckedSubtract)); + if (definition.Equals(typeof(IMultiplyOperators<,,>).ToNamedType())) + return Binary(OperatorKind.Multiplication, OperatorKind.CheckedMultiply, nameof(GeneratedArithmetic.Multiply), nameof(GeneratedArithmetic.CheckedMultiply)); + if (definition.Equals(typeof(IDivisionOperators<,,>).ToNamedType())) + return Binary(OperatorKind.Division, OperatorKind.CheckedDivision, nameof(GeneratedArithmetic.Divide), nameof(GeneratedArithmetic.CheckedDivide)); + if (definition.Equals(typeof(IModulusOperators<,,>).ToNamedType())) + return Binary(OperatorKind.Modulus, null, nameof(GeneratedArithmetic.Remainder), null); + if (definition.Equals(typeof(IUnaryNegationOperators<,>).ToNamedType())) + return Unary(OperatorKind.UnaryNegation, OperatorKind.CheckedUnaryNegation, nameof(GeneratedArithmetic.Negate), nameof(GeneratedArithmetic.CheckedNegate)); + if (definition.Equals(typeof(IUnaryPlusOperators<,>).ToNamedType())) + return Unary(OperatorKind.UnaryPlus, null, nameof(GeneratedArithmetic.Plus), null); + + return null; + } +} + +/// What one generated operator's template needs. +[CompileTime] +internal sealed class ArithmeticOperatorArguments +{ + /// The GeneratedArithmetic method, with its type arguments: the wrapped type's operator. + public required IMethod Helper { get; init; } + + /// The right operand is the value object, so its wrapped value is passed; otherwise the operand itself. + public required bool UnwrapsRight { get; init; } + + /// The private constructor, where the result is the value object, built from the wrapped type's result. + public required IConstructor? Constructor { get; init; } + + /// A record class: an operand of its type can be null. + public required bool ChecksNull { get; init; } +} + +/// +/// The arithmetic operators of the operator interfaces a plain value object declares: +, +/// -, *, /, % and unary - and +, each with its checked form where +/// the interface has one. Each is the wrapped type's own, called through GeneratedArithmetic. +/// +/// +/// +/// Opt in, never by default: an interface the value object declares, such as +/// IAdditionOperators<Money, Money, Money>, where the wrapped type implements the same +/// interface with the value object replaced by itself (IAdditionOperators<decimal, decimal, +/// decimal>). An operand or a result of another type is passed through as it is, so +/// IMultiplyOperators<Money, decimal, Money> scales and +/// IDivisionOperators<Money, Money, decimal> is a ratio. Where the wrapped type does not match, +/// nothing is generated and the declaration fails as it did before, with CS0535. +/// +/// +/// Never for a validated value object: an operator cannot return a Result, and a sum outside the +/// range Create accepts would have to throw or skip it. +/// +/// +/// An operator the value object already has is kept, with its checked form: one it declares for the +/// same operands, or an explicit implementation of the interface, its own or a base record's. Code that +/// implemented these interfaces by hand before compiles unchanged, so this is no refusal (decision 121). +/// +/// +internal sealed partial class ValueObjectArithmeticAspect : TypeAspect +{ + public override void BuildAspect(IAspectBuilder builder) + { + if (!builder.AspectInstance.Predecessors.TryGetState(out var state)) + return; + + if (state.Kind == ValueObjectKind.Validated) + { + builder.SkipAspect(); + return; + } + + var target = builder.Target; + var valueType = state.ValueType.GetTarget(); + var constructor = state.PrivateConstructor.GetTarget(); + var helpers = typeof(GeneratedArithmetic).ToNamedType(); + + // Two interfaces asking for one operator, as IDivisionOperators beside + // IDivisionOperators would, get it once; C# allows no second, and the + // other interface stays to be implemented explicitly by hand. + List<(OperatorKind Kind, IType? Right)> introduced = []; + + foreach (var declared in target.AllImplementedInterfaces) + { + if (ArithmeticOperator.Of(declared.Definition) is not { } @operator || !declared.TypeArguments[0].Equals(target)) + continue; + + var right = @operator.IsBinary ? declared.TypeArguments[1] : null; + var result = declared.TypeArguments[@operator.IsBinary ? 2 : 1]; + + // The wrapped type's own interface: the value object replaced by the wrapped type throughout. + IType Unwrapped(IType type) => type.Equals(target) ? valueType : type; + + IType[] matching = right is null ? [valueType, Unwrapped(result)] : [valueType, Unwrapped(right), Unwrapped(result)]; + + if (!valueType.IsConvertibleTo(declared.Definition.MakeGenericInstance(matching))) + continue; + + if (DeclaredByHand(target, declared, @operator, right) || introduced.Contains((@operator.Kind, right))) + continue; + + introduced.Add((@operator.Kind, right)); + + Introduce(@operator.Kind, @operator.Helper); + + if (@operator is { CheckedKind: { } checkedKind, CheckedHelper: { } checkedHelper }) + Introduce(checkedKind, checkedHelper); + + void Introduce(OperatorKind kind, string helperName) + { + var helper = helpers.Methods.Single(method => method.Name == helperName).MakeGenericInstance(matching); + + var tags = new ArithmeticOperatorArguments + { + Helper = helper, + UnwrapsRight = right is not null && right.Equals(target), + Constructor = result.Equals(target) ? constructor : null, + ChecksNull = target.IsReferenceType == true + }; + + builder.IntroduceMethod( + right is null ? nameof(UnaryOperatorTemplate) : nameof(BinaryOperatorTemplate), + IntroductionScope.Static, + OverrideStrategy.Ignore, + method => + { + method.Parameters[0].Type = target; + if (right is not null) method.Parameters[1].Type = right; + method.ReturnType = result; + method.OperatorKind = kind; + method.AddAttribute(CodeAnnotations.CompilerGenerated); + }, + tags: tags); + } + } + + if (introduced.Count == 0) + builder.SkipAspect(); + } + + /// + /// The value object has the operator already, which is kept with its checked form: it declares one of + /// either kind for the same operands, whatever it returns, or it or a base record implements the + /// interface explicitly. A checked operator generated beside a hand-written unchecked one would do the + /// wrapped type's arithmetic in a checked context and the user's everywhere else, and a plain one + /// generated beside a hand-written checked one, which C# refuses alone (CS9025), the other way round. + /// + internal static bool DeclaredByHand(INamedType target, INamedType declared, ArithmeticOperator @operator, IType? right) + { + foreach (var method in target.Methods) + { + if (method is { IsImplicitlyDeclared: true } or { IsExplicitInterfaceImplementation: true }) continue; + if (method.OperatorKind != @operator.Kind && method.OperatorKind != @operator.CheckedKind) continue; + + if (right is null ? method.Parameters is [{ } only] && only.Type.Equals(target) + : method.Parameters is [{ } left, { } other] && left.Type.Equals(target) && other.Type.Equals(right)) + return true; + } + + return ValueObjectDeclaration.ImplementsExplicitly(target, declared); + } +} diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectAspect.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectAspect.cs index db8d4e9f..5f5f1c47 100644 --- a/src/CodoMetis.TypeKit.Generators/ValueObjectAspect.cs +++ b/src/CodoMetis.TypeKit.Generators/ValueObjectAspect.cs @@ -100,6 +100,7 @@ public override void BuildAspect(IAspectBuilder builder) builder.Outbound.AddAspect(); builder.Outbound.AddAspect(); builder.Outbound.AddAspect(); + builder.Outbound.AddAspect(); builder.Outbound.AddAspect(); builder.Outbound.AddAspect(); builder.Outbound.AddAspect(); diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs index 79a27700..8578ae5c 100644 --- a/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs +++ b/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs @@ -339,6 +339,15 @@ internal static IEnumerable ExplicitImplementationsOf(INamedType target, } } + /// + /// or a base record implements a member of + /// explicitly, compared as the instance it is, type arguments included: what a generic base implements + /// for IAdditionOperators<TSelf, TSelf, TSelf> is read for the value object it derives. + /// + internal static bool ImplementsExplicitly(INamedType target, INamedType @interface) => + SelfAndBases(target).Any(type => type.Methods.Any(method => method is { IsImplicitlyDeclared: false, IsExplicitInterfaceImplementation: true } + && method.ExplicitInterfaceImplementations.Any(implemented => implemented.DeclaringType.Equals(@interface)))); + /// /// How a validated value object declares Create: as an explicit implementation of the /// marker's, and as a static method the generated code can call as {TSelf}.Create(value). diff --git a/src/CodoMetis.TypeKit/CompilerServices/GeneratedArithmetic.cs b/src/CodoMetis.TypeKit/CompilerServices/GeneratedArithmetic.cs new file mode 100644 index 00000000..e5910241 --- /dev/null +++ b/src/CodoMetis.TypeKit/CompilerServices/GeneratedArithmetic.cs @@ -0,0 +1,139 @@ +using System.ComponentModel; +using System.Numerics; + +namespace CodoMetis.TypeKit.CompilerServices; + +/// +/// Calls the wrapped type's arithmetic operators through a type parameter, for the operators that +/// CodoMetis.TypeKit.Generators generates where a plain value object declares an operator interface. +/// +/// +/// +/// A call through a type parameter constrained to the interface is what the value object promises: +/// the wrapped type's own implementation of that interface. Written as left.Value + right.Value, +/// two s or two s were added as s, and an operator +/// a type implements explicitly could not be reached at all. +/// +/// +/// Each unchecked operator is and each checked one +/// here, whatever the calling project's overflow checking: the context picks the operator, as it picks +/// the wrapped type's, and the operator does what the wrapped type's does in it. +/// +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class GeneratedArithmetic +{ + /// ' + of . + /// The left operand. + /// The right operand. + /// The type whose operator it is. + /// The right operand's type. + /// The result's type. + /// The sum. + public static TResult Add(TSelf left, TOther right) where TSelf : IAdditionOperators => + unchecked(left + right); + + /// ' checked + of . + /// The left operand. + /// The right operand. + /// The type whose operator it is. + /// The right operand's type. + /// The result's type. + /// The sum. + public static TResult CheckedAdd(TSelf left, TOther right) where TSelf : IAdditionOperators => + checked(left + right); + + /// ' - of . + /// The left operand. + /// The right operand. + /// The type whose operator it is. + /// The right operand's type. + /// The result's type. + /// The difference. + public static TResult Subtract(TSelf left, TOther right) where TSelf : ISubtractionOperators => + unchecked(left - right); + + /// ' checked - of . + /// The left operand. + /// The right operand. + /// The type whose operator it is. + /// The right operand's type. + /// The result's type. + /// The difference. + public static TResult CheckedSubtract(TSelf left, TOther right) where TSelf : ISubtractionOperators => + checked(left - right); + + /// ' * of . + /// The left operand. + /// The right operand. + /// The type whose operator it is. + /// The right operand's type. + /// The result's type. + /// The product. + public static TResult Multiply(TSelf left, TOther right) where TSelf : IMultiplyOperators => + unchecked(left * right); + + /// ' checked * of . + /// The left operand. + /// The right operand. + /// The type whose operator it is. + /// The right operand's type. + /// The result's type. + /// The product. + public static TResult CheckedMultiply(TSelf left, TOther right) where TSelf : IMultiplyOperators => + checked(left * right); + + /// ' / of . + /// The left operand. + /// The right operand. + /// The type whose operator it is. + /// The right operand's type. + /// The result's type. + /// The quotient. + public static TResult Divide(TSelf left, TOther right) where TSelf : IDivisionOperators => + unchecked(left / right); + + /// ' checked / of . + /// The left operand. + /// The right operand. + /// The type whose operator it is. + /// The right operand's type. + /// The result's type. + /// The quotient. + public static TResult CheckedDivide(TSelf left, TOther right) where TSelf : IDivisionOperators => + checked(left / right); + + /// ' % of . + /// The left operand. + /// The right operand. + /// The type whose operator it is. + /// The right operand's type. + /// The result's type. + /// The remainder. + public static TResult Remainder(TSelf left, TOther right) where TSelf : IModulusOperators => + unchecked(left % right); + + /// ' - of . + /// The operand. + /// The type whose operator it is. + /// The result's type. + /// The negation. + public static TResult Negate(TSelf value) where TSelf : IUnaryNegationOperators => + unchecked(-value); + + /// ' checked - of . + /// The operand. + /// The type whose operator it is. + /// The result's type. + /// The negation. + public static TResult CheckedNegate(TSelf value) where TSelf : IUnaryNegationOperators => + checked(-value); + + /// ' + of . + /// The operand. + /// The type whose operator it is. + /// The result's type. + /// The operand, as the type's unary plus gives it. + public static TResult Plus(TSelf value) where TSelf : IUnaryPlusOperators => + +value; +} diff --git a/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/ArithmeticQueryTests.cs b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/ArithmeticQueryTests.cs new file mode 100644 index 00000000..4220a677 --- /dev/null +++ b/test/CodoMetis.TypeKit.EntityFrameworkCore.Tests/ArithmeticQueryTests.cs @@ -0,0 +1,70 @@ +using CodoMetis.TypeKit.Generators.Probes; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace CodoMetis.TypeKit.EntityFrameworkCore.Tests; + +/// +/// The arithmetic operators a value object declares, in a query: between two value objects EF translates them as +/// it translates the wrapped type's, and the database computes them, so a filter keeps the rows memory keeps. +/// +/// +/// EF reads the operator as SQL arithmetic and never runs it (ef_add("p"."Price", "p"."Tax") on SQLite, +/// measured 2026-10-30, as for two columns). With an operand or a result of another type +/// (Price * 2m, Tax / Price as a ) it applies the value object's converter to the +/// other side, and the query throws: .Value * 2m is the form that translates. +/// +public sealed class ArithmeticQueryTests : IDisposable +{ + private readonly SqliteConnection _connection = new("Data Source=:memory:"); + + [Fact] + public void An_operator_between_value_objects_runs_in_the_database() + { + using var db = Seeded(); + + db.Priced.Where(p => p.Price + p.Tax > ProbeMoney.From(3m)).Select(p => p.Id).ToList().ShouldBe([2]); + db.Priced.Where(p => p.Price - p.Tax < ProbeMoney.From(2m)).Select(p => p.Id).ToList().ShouldBe([1]); + db.Priced.Where(p => -p.Price < ProbeMoney.From(-5m)).Select(p => p.Id).ToList().ShouldBe([2]); + db.Priced.OrderBy(p => p.Id).Select(p => (p.Price + p.Tax).Value).ToList().ShouldBe([2.0m, 12.0m]); + db.Priced.Where(p => p.Price + p.Tax > ProbeMoney.From(3m)).ToQueryString().ShouldContain("ef_add("); + } + + [Fact] + public void An_operator_with_another_operand_throws_and_Value_translates() + { + using var db = Seeded(); + + Should.Throw(() => db.Priced.Where(p => p.Price * 2m > ProbeMoney.From(10m)).ToList()); + Should.Throw(() => db.Priced.Select(p => p.Tax / p.Price).ToList()); + db.Priced.Where(p => p.Price.Value * 2m > 10m).Select(p => p.Id).ToList().ShouldBe([2]); + } + + public void Dispose() => _connection.Dispose(); + + private PricedDb Seeded() + { + _connection.Open(); + var db = new PricedDb(new DbContextOptionsBuilder().UseSqlite(_connection).UseTypeKit().Options); + db.Database.EnsureCreated(); + db.Priced.AddRange(new Priced { Id = 1, Price = ProbeMoney.From(1.5m), Tax = ProbeMoney.From(0.5m) }, + new Priced { Id = 2, Price = ProbeMoney.From(10m), Tax = ProbeMoney.From(2m) }); + db.SaveChanges(); + db.ChangeTracker.Clear(); + return db; + } + + public sealed class Priced + { + public int Id { get; set; } + + public ProbeMoney Price { get; set; } + + public ProbeMoney Tax { get; set; } + } + + public sealed class PricedDb(DbContextOptions options) : DbContext(options) + { + public DbSet Priced => Set(); + } +} diff --git a/test/CodoMetis.TypeKit.Generators.Probes/Probes.cs b/test/CodoMetis.TypeKit.Generators.Probes/Probes.cs index 252c6a7e..f98c2bd6 100644 --- a/test/CodoMetis.TypeKit.Generators.Probes/Probes.cs +++ b/test/CodoMetis.TypeKit.Generators.Probes/Probes.cs @@ -387,3 +387,17 @@ public sealed partial class ProbeKindlessFault : IFaultSet public FaultKind Kind { get; } } + +/// +/// Declares the arithmetic operator interfaces: each operator is generated, and is the wrapped type's. +/// Scaled and divided by the wrapped type, and divided by itself into a ratio of it. +/// +public readonly partial record struct ProbeMoney : IValue, + System.Numerics.IAdditionOperators, + System.Numerics.ISubtractionOperators, + System.Numerics.IMultiplyOperators, + System.Numerics.IDivisionOperators, + System.Numerics.IDivisionOperators, + System.Numerics.IModulusOperators, + System.Numerics.IUnaryNegationOperators, + System.Numerics.IUnaryPlusOperators; diff --git a/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticBuildTests.cs b/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticBuildTests.cs new file mode 100644 index 00000000..92cf3c00 --- /dev/null +++ b/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticBuildTests.cs @@ -0,0 +1,89 @@ +namespace CodoMetis.TypeKit.Generators.Tests; + +/// +/// An operator interface the generators do not implement stays to be implemented, and the build fails with +/// C#'s own error naming the operator, as it did before they implemented any: on a validated value object, over +/// a wrapped type without that operator (a , a , which implements none +/// of the operator interfaces), for the second of two interfaces that ask for one operator, and beside a +/// hand-written checked operator that lacks the plain one C# requires. +/// +/// +/// In a consumer of its own: beside a compiler error, Metalama reported no error in the code it generated into a +/// declaration's own file (), so these declarations would blind the shared +/// consumer. That the generated operators compile is shown by the test assembly, which declares them. +/// +public sealed class ArithmeticBuildTests(ArithmeticBuildTests.Consumer consumer) : IClassFixture +{ + [Theory] + [InlineData("ValidatedSum", "IAdditionOperators.operator +(ValidatedSum, ValidatedSum)")] + [InlineData("TextSum", "IAdditionOperators.operator +(TextSum, TextSum)")] + [InlineData("Moment", "IAdditionOperators.operator +(Moment, TimeSpan)")] + public void An_operator_the_generators_cannot_write_is_left_to_implement(string type, string member) => + consumer.Errors.ShouldContain(error => error.Id == "CS0535" && error.Message.StartsWith($"'{type}' does not implement interface member '{member}'"), consumer.Output); + + /// C# allows one operator /(TwoQuotients, TwoQuotients): it is generated once, for the first interface. + [Fact] + public void The_second_interface_asking_for_one_operator_is_left_to_implement() => + consumer.Errors.ShouldContain(error => error.Id == "CS0738" && error.Message.StartsWith("'TwoQuotients' does not implement interface member 'IDivisionOperators.operator /(TwoQuotients, TwoQuotients)'"), consumer.Output); + + /// + /// A hand-written checked + makes addition the user's: the + C# requires beside it is theirs to + /// write. Generated, it would build, and add by the wrapped type's rules everywhere but a checked context. + /// + [Fact] + public void A_hand_written_checked_operator_is_not_completed() => + consumer.Errors.ShouldContain(error => error.Id == "CS9025" && error.Message.Contains("OnlyChecked"), consumer.Output); + + /// Nothing but C#'s own errors: no aspect failed on these declarations (LAMA0041, LAMA0611). + [Fact] + public void Every_error_is_one_of_the_intended_ones() => + consumer.Errors.Select(error => error.Id).Distinct().Order().ShouldBe(["CS0535", "CS0738", "CS9025"], ignoreOrder: false, customMessage: consumer.Output); + + /// The consumer, built once per test run. + public sealed class Consumer : IAsyncLifetime + { + private const string Declarations = + """ + using System.Numerics; + using CodoMetis.TypeKit; + using CodoMetis.TypeKit.ValueObjects; + + namespace Consumer; + + public enum Fault { Refused } + + public readonly partial record struct ValidatedSum : IValidatedValue, IAdditionOperators + { + public static Result Create(int value) => new ValidatedSum(value); + } + + public readonly partial record struct TextSum : IValue, IAdditionOperators; + + public readonly partial record struct Moment : IValue, IAdditionOperators; + + public readonly partial record struct TwoQuotients : IValue, + IDivisionOperators, + IDivisionOperators; + + public readonly partial record struct OnlyChecked : IValue, IAdditionOperators + { + public static OnlyChecked operator checked +(OnlyChecked left, OnlyChecked right) => From(checked(left.Value + right.Value)); + } + """; + + private readonly string _directory = Path.Combine(BuildOutcomeTests.Consumer.RepositoryRoot(), "artifacts", $"consumer-{Guid.NewGuid():N}"); + + public IReadOnlyList Errors { get; private set; } = []; + + public string Output { get; private set; } = ""; + + public async ValueTask InitializeAsync() => + (Output, Errors) = await BuildOutcomeTests.Consumer.Build(_directory, new Dictionary { ["Declarations.cs"] = Declarations }); + + public ValueTask DisposeAsync() + { + if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true); + return ValueTask.CompletedTask; + } + } +} diff --git a/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticParityTests.cs b/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticParityTests.cs new file mode 100644 index 00000000..451f2146 --- /dev/null +++ b/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticParityTests.cs @@ -0,0 +1,186 @@ +using System.Globalization; +using System.Numerics; +using CodoMetis.TypeKit.ValueObjects; + +namespace CodoMetis.TypeKit.Generators.Tests; + +/// +/// The arithmetic operator interfaces, each with the value object as both operands and the result, declared +/// through one interface of the tests' own: the generators read what a value object declares through another +/// interface too. +/// +public interface IArithmetic : + IAdditionOperators, + ISubtractionOperators, + IMultiplyOperators, + IDivisionOperators, + IModulusOperators, + IUnaryNegationOperators, + IUnaryPlusOperators + where TSelf : IArithmetic; + +// One value object per numeric wrapped type, a record struct each, and record classes over two of them. +public readonly partial record struct SByteAmount : IValue, IArithmetic; +public readonly partial record struct ByteAmount : IValue, IArithmetic; +public readonly partial record struct Int16Amount : IValue, IArithmetic; +public readonly partial record struct UInt16Amount : IValue, IArithmetic; +public readonly partial record struct Int32Amount : IValue, IArithmetic; +public readonly partial record struct UInt32Amount : IValue, IArithmetic; +public readonly partial record struct Int64Amount : IValue, IArithmetic; +public readonly partial record struct UInt64Amount : IValue, IArithmetic; +public readonly partial record struct Int128Amount : IValue, IArithmetic; +public readonly partial record struct UInt128Amount : IValue, IArithmetic; +public readonly partial record struct IntPtrAmount : IValue, IArithmetic; +public readonly partial record struct UIntPtrAmount : IValue, IArithmetic; +public readonly partial record struct BigIntegerAmount : IValue, IArithmetic; + +/// implements its arithmetic interfaces explicitly: a + b on two chars is an . +public readonly partial record struct CharAmount : IValue, IArithmetic; + +public readonly partial record struct HalfAmount : IValue, IArithmetic; +public readonly partial record struct SingleAmount : IValue, IArithmetic; +public readonly partial record struct DoubleAmount : IValue, IArithmetic; +public readonly partial record struct DecimalAmount : IValue, IArithmetic; + +public sealed partial record Int32AmountClass : IValue, IArithmetic; +public sealed partial record DoubleAmountClass : IValue, IArithmetic; + +/// Nested in another type. +public static class Ledger +{ + public readonly partial record struct Entry : IValue, IArithmetic; +} + +/// Internal, with its generated operators public, as a type's own operators must be. +internal readonly partial record struct InternalAmount : IValue, IArithmetic; + +/// +/// Every generated arithmetic operator is the wrapped type's: for each numeric wrapped type, each record shape +/// and each pair of a catalogue of values, the value object's +, -, *, /, %, +/// unary - and +, unchecked and checked, give what the wrapped type's own give, value or +/// exception. +/// +/// +/// +/// The oracle is the wrapped type's implementation of the same interface, called by the same generic code in +/// the same context. Written as left.Value + right.Value, two s were added as +/// s and 's explicit operators could not be reached; without its checked +/// form, a value object over wrapped around in a checked context where an +/// throws. +/// +/// +/// The catalogue holds zero, one, minus one where the type is signed, the type's bounds, and for the +/// floating-point types NaN, an infinity and the smallest positive value: the rows where overflow, division by +/// zero and the checked context decide the answer. +/// +/// +public sealed class ArithmeticParityTests +{ + public static TheoryData Types => [.. Sweeps.Keys]; + + private static readonly IReadOnlyDictionary>> Sweeps = new Dictionary>> + { + ["sbyte"] = () => Disagreements([0, 1, -1, 7, sbyte.MinValue, sbyte.MaxValue]), + ["byte"] = () => Disagreements([0, 1, 7, byte.MaxValue]), + ["short"] = () => Disagreements([0, 1, -1, 7, short.MinValue, short.MaxValue]), + ["ushort"] = () => Disagreements([0, 1, 7, ushort.MaxValue]), + ["int"] = () => Disagreements([0, 1, -1, 7, int.MinValue, int.MaxValue]), + ["uint"] = () => Disagreements([0, 1, 7, uint.MaxValue]), + ["long"] = () => Disagreements([0, 1, -1, 7, long.MinValue, long.MaxValue]), + ["ulong"] = () => Disagreements([0, 1, 7, ulong.MaxValue]), + ["Int128"] = () => Disagreements([0, 1, -1, 7, Int128.MinValue, Int128.MaxValue]), + ["UInt128"] = () => Disagreements([0, 1, 7, UInt128.MaxValue]), + ["nint"] = () => Disagreements([0, 1, -1, 7, nint.MinValue, nint.MaxValue]), + ["nuint"] = () => Disagreements([0, 1, 7, nuint.MaxValue]), + ["BigInteger"] = () => Disagreements([0, 1, -1, 7, BigInteger.Pow(2, 100), -BigInteger.Pow(10, 30)]), + ["char"] = () => Disagreements([(char)0, (char)1, 'a', char.MaxValue]), + ["Half"] = () => Disagreements([Half.Zero, Half.One, Half.NegativeOne, Half.MinValue, Half.MaxValue, Half.Epsilon, Half.NaN, Half.PositiveInfinity]), + ["float"] = () => Disagreements([0, 1, -1, 0.1f, float.MinValue, float.MaxValue, float.Epsilon, float.NaN, float.PositiveInfinity]), + ["double"] = () => Disagreements([0, 1, -1, 0.1, double.MinValue, double.MaxValue, double.Epsilon, double.NaN, double.PositiveInfinity]), + ["decimal"] = () => Disagreements([0, 1, -1, 0.1m, decimal.MinValue, decimal.MaxValue]), + ["int, record class"] = () => Disagreements([0, 1, -1, 7, int.MinValue, int.MaxValue]), + ["double, record class"] = () => Disagreements([0, 1, -1, double.MaxValue, double.NaN]), + ["long, nested"] = () => Disagreements([0, 1, -1, long.MinValue, long.MaxValue]), + ["int, internal"] = () => Disagreements([0, 1, -1, int.MinValue, int.MaxValue]), + }; + + [Theory] + [MemberData(nameof(Types))] + public void Every_operator_is_the_wrapped_types(string type) + { + var disagreements = Sweeps[type](); + + disagreements.ShouldBeEmpty($"{disagreements.Count} rows of {type} disagree:{Environment.NewLine}{string.Join(Environment.NewLine, disagreements.Take(25))}"); + } + + /// The floor: a sweep that compared nothing would pass everything. + [Fact] + public void The_sweep_covers_every_numeric_type_and_both_record_shapes() => + Sweeps.Count.ShouldBeGreaterThanOrEqualTo(22); + + private static IReadOnlyList Disagreements(T[] values) + where TSelf : IArithmetic, IPlainValueObject, IValueObject + where T : notnull, INumberBase, IModulusOperators + { + List found = []; + var rows = 0; + + void Compare(string operation, Func wrapped, Func unwrapped) + { + rows++; + var expected = Outcome(unwrapped); + var actual = Outcome(() => wrapped().Value); + + if (!Equals(expected, actual)) + found.Add($"{typeof(TSelf).Name}: {operation}: the wrapped type gives {Show(expected)}, the value object {Show(actual)}"); + } + + foreach (var a in values) + { + var x = TSelf.From(a); + + Compare($"unchecked(-{Show(a)})", () => unchecked(-x), () => unchecked(-a)); + Compare($"checked(-{Show(a)})", () => checked(-x), () => checked(-a)); + Compare($"+{Show(a)}", () => +x, () => +a); + + foreach (var b in values) + { + var y = TSelf.From(b); + + Compare($"unchecked({Show(a)} + {Show(b)})", () => unchecked(x + y), () => unchecked(a + b)); + Compare($"checked({Show(a)} + {Show(b)})", () => checked(x + y), () => checked(a + b)); + Compare($"unchecked({Show(a)} - {Show(b)})", () => unchecked(x - y), () => unchecked(a - b)); + Compare($"checked({Show(a)} - {Show(b)})", () => checked(x - y), () => checked(a - b)); + Compare($"unchecked({Show(a)} * {Show(b)})", () => unchecked(x * y), () => unchecked(a * b)); + Compare($"checked({Show(a)} * {Show(b)})", () => checked(x * y), () => checked(a * b)); + Compare($"unchecked({Show(a)} / {Show(b)})", () => unchecked(x / y), () => unchecked(a / b)); + Compare($"checked({Show(a)} / {Show(b)})", () => checked(x / y), () => checked(a / b)); + Compare($"{Show(a)} % {Show(b)}", () => x % y, () => a % b); + } + } + + rows.ShouldBe(values.Length * 3 + values.Length * values.Length * 9, typeof(TSelf).Name); + return found; + } + + /// The value, or the type of the exception thrown instead. + private static object Outcome(Func operation) where T : notnull + { + try + { + return operation(); + } + catch (Exception exception) + { + return exception.GetType(); + } + } + + private static string Show(object value) => value switch + { + char c => $"'\\u{(int)c:X4}'", + Type exception => exception.Name, + IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), + _ => value.ToString() ?? "" + }; +} diff --git a/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticTests.cs b/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticTests.cs new file mode 100644 index 00000000..d4a1cb8d --- /dev/null +++ b/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticTests.cs @@ -0,0 +1,213 @@ +using System.Numerics; +using System.Reflection; +using CodoMetis.TypeKit.Generators.Probes; +using CodoMetis.TypeKit.ValueObjects; + +namespace CodoMetis.TypeKit.Generators.Tests; + +/// +/// Declares its own +, which rounds: it is kept, and so is the checked context's +, which is the +/// same one. - is not declared by hand and is generated. +/// +public readonly partial record struct RoundedMoney : IValue, + IAdditionOperators, + ISubtractionOperators +{ + public static RoundedMoney operator +(RoundedMoney left, RoundedMoney right) => From(Math.Round(left.Value + right.Value, 2, MidpointRounding.AwayFromZero)); +} + +/// Implements + explicitly, as code written for 2.3 could: kept, and no public one is generated beside it. +public readonly partial record struct ExplicitlyAdded : IValue, IAdditionOperators +{ + static ExplicitlyAdded IAdditionOperators.operator +(ExplicitlyAdded left, ExplicitlyAdded right) => + From(left.Value + right.Value + 1_000); +} + +/// A base record that implements + for the value object derived from it, explicitly. +public abstract record AddingBase : IAdditionOperators where TSelf : AddingBase +{ + static TSelf IAdditionOperators.operator +(TSelf left, TSelf right) => throw new NotSupportedException(nameof(AddingBase<>)); +} + +/// Its + is its base record's, which every caller through the interface reaches: none is generated. +public sealed partial record InheritsAddition : AddingBase, IValue; + +/// A validated value object with the + it had to write by hand before 2.4, which still compiles. +public readonly partial record struct Score : IValidatedValue, IAdditionOperators +{ + public static Result Create(int value) => value is >= 0 and <= 100 ? new Score(value) : Result.Error(ScoreFault.OutOfRange); + + public static Score operator +(Score left, Score right) => FromKnownGood(Math.Min(left.Value + right.Value, 100)); +} + +public enum ScoreFault { OutOfRange } + +/// +/// A point in time of the tests' own: a later one is a point plus an , with a checked form +/// of its own, and two points' difference is an , implemented explicitly. A +/// implements neither interface, so a value object over one gets neither. +/// +public readonly record struct Tick(long Count) : IAdditionOperators, ISubtractionOperators +{ + public static Tick operator +(Tick left, Elapsed right) => new(unchecked(left.Count + right.Count)); + + public static Tick operator checked +(Tick left, Elapsed right) => new(checked(left.Count + right.Count)); + + static Elapsed ISubtractionOperators.operator -(Tick left, Tick right) => new(left.Count - right.Count); +} + +/// What lies between two s. +public readonly record struct Elapsed(long Count); + +/// Another type as the operand and as the result, as its wrapped type has them. +public readonly partial record struct Stamp : IValue, + IAdditionOperators, + ISubtractionOperators; + +/// +/// The arithmetic operators a plain value object declares, as a caller writes them: on the type itself, in +/// and out of a checked context, across an assembly boundary, with another operand type; and an operator +/// written by hand, which is kept with its checked form. +/// +/// +/// compares every operator with the wrapped type's through generic code; +/// these call the operators the way most code does, on the concrete type. +/// +public sealed class ArithmeticTests +{ + [Fact] + public void A_declared_operator_is_generated_on_the_type() + { + var price = ProbeMoney.From(10.50m); + + (price + ProbeMoney.From(0.25m)).ShouldBe(ProbeMoney.From(10.75m)); + (price - ProbeMoney.From(0.50m)).ShouldBe(ProbeMoney.From(10m)); + (-price).ShouldBe(ProbeMoney.From(-10.50m)); + (+price).ShouldBe(price); + } + + [Fact] + public void The_wrapped_type_as_the_right_operand_scales() + { + var price = ProbeMoney.From(10m); + + (price * 3m).ShouldBe(ProbeMoney.From(30m)); + (price / 4m).ShouldBe(ProbeMoney.From(2.5m)); + (price % 3m).ShouldBe(ProbeMoney.From(1m)); + } + + [Fact] + public void A_result_of_the_wrapped_type_is_not_wrapped() + { + decimal ratio = ProbeMoney.From(3m) / ProbeMoney.From(4m); + + ratio.ShouldBe(0.75m); + } + + [Fact] + public void Another_operand_type_is_the_wrapped_types_own() + { + var start = Stamp.From(new Tick(10)); + + (start + new Elapsed(5)).Value.ShouldBe(new Tick(15)); + (start + new Elapsed(5) - start).ShouldBe(new Elapsed(5)); + unchecked(Stamp.From(new Tick(long.MaxValue)) + new Elapsed(1)).Value.ShouldBe(new Tick(long.MinValue)); + Should.Throw(() => checked(Stamp.From(new Tick(long.MaxValue)) + new Elapsed(1))); + } + + /// The checked context picks the checked operator, as it does for an . + [Fact] + public void The_checked_context_is_the_wrapped_types() + { + var largest = Int32Amount.From(int.MaxValue); + var one = Int32Amount.From(1); + + unchecked(largest + one).Value.ShouldBe(int.MinValue); + Should.Throw(() => checked(largest + one)); + Should.Throw(() => checked(-Int32Amount.From(int.MinValue))); + Should.Throw(() => one / Int32Amount.From(0)); + } + + /// Two bytes are added as bytes, as of adds them. + [Fact] + public void A_small_integer_wraps_around_at_its_own_width() + { + unchecked(ByteAmount.From(byte.MaxValue) + ByteAmount.From(1)).Value.ShouldBe((byte)0); + Should.Throw(() => checked(ByteAmount.From(byte.MaxValue) + ByteAmount.From(1))); + } + + [Fact] + public void A_null_operand_of_a_record_class_is_refused_by_name() + { + Int32AmountClass? missing = null; + + Should.Throw(() => missing! + Int32AmountClass.From(1)).ParamName.ShouldBe("left"); + Should.Throw(() => Int32AmountClass.From(1) - missing!).ParamName.ShouldBe("right"); + Should.Throw(() => -missing!).ParamName.ShouldBe("value"); + } + + [Fact] + public void A_hand_written_operator_is_kept_with_its_checked_form() + { + var sum = RoundedMoney.From(0.004m) + RoundedMoney.From(0.001m); + + sum.ShouldBe(RoundedMoney.From(0.01m)); + checked(RoundedMoney.From(0.004m) + RoundedMoney.From(0.001m)).ShouldBe(RoundedMoney.From(0.01m)); + Add(RoundedMoney.From(0.004m), RoundedMoney.From(0.001m)).ShouldBe(RoundedMoney.From(0.01m)); + CheckedAdd(RoundedMoney.From(0.004m), RoundedMoney.From(0.001m)).ShouldBe(RoundedMoney.From(0.01m)); + Operators(typeof(RoundedMoney), "op_Addition").ShouldHaveSingleItem(); + Operators(typeof(RoundedMoney), "op_CheckedAddition").ShouldBeEmpty(); + } + + [Fact] + public void An_operator_not_written_by_hand_is_generated_beside_one_that_is() + { + (RoundedMoney.From(0.004m) - RoundedMoney.From(0.001m)).ShouldBe(RoundedMoney.From(0.003m)); + Operators(typeof(RoundedMoney), "op_CheckedSubtraction").ShouldHaveSingleItem(); + } + + [Fact] + public void An_explicit_implementation_is_kept_and_nothing_public_is_generated() + { + Add(ExplicitlyAdded.From(1), ExplicitlyAdded.From(2)).ShouldBe(ExplicitlyAdded.From(1_003)); + CheckedAdd(ExplicitlyAdded.From(1), ExplicitlyAdded.From(2)).ShouldBe(ExplicitlyAdded.From(1_003)); + Operators(typeof(ExplicitlyAdded), "op_Addition").ShouldBeEmpty(); + Operators(typeof(ExplicitlyAdded), "op_CheckedAddition").ShouldBeEmpty(); + } + + /// + /// A public + generated here would not implement the interface, which the base record does: generic + /// code would reach the base's and a + b the generated one. + /// + [Fact] + public void A_base_records_explicit_implementation_is_kept() + { + Should.Throw(() => Add(InheritsAddition.From(1), InheritsAddition.From(2))); + Operators(typeof(InheritsAddition), "op_Addition").ShouldBeEmpty(); + Operators(typeof(InheritsAddition), "op_CheckedAddition").ShouldBeEmpty(); + } + + [Fact] + public void A_validated_value_object_keeps_its_own_operator_and_gets_none() + { + (Score.FromKnownGood(70) + Score.FromKnownGood(50)).ShouldBe(Score.FromKnownGood(100)); + Operators(typeof(Score), "op_Addition").ShouldHaveSingleItem(); + Operators(typeof(Score), "op_CheckedAddition").ShouldBeEmpty(); + } + + /// No interface declared, nothing generated: the arithmetic is opt-in. + [Fact] + public void A_value_object_that_declares_no_operator_interface_gets_none() + { + typeof(ProbeCount).GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(method => method.Name is "op_Addition" or "op_Subtraction" or "op_Multiply" or "op_Division" or "op_Modulus" or "op_UnaryNegation" or "op_UnaryPlus") + .ShouldBeEmpty(); + } + + private static T Add(T left, T right) where T : IAdditionOperators => unchecked(left + right); + + private static T CheckedAdd(T left, T right) where T : IAdditionOperators => checked(left + right); + + private static IEnumerable Operators(Type type, string name) => + type.GetMethods(BindingFlags.Public | BindingFlags.Static).Where(method => method.Name == name); +} diff --git a/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt b/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt index 14f02dab..19837f84 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt +++ b/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt @@ -901,6 +901,80 @@ ProbeMoment static op_LessThanOrEqual(ProbeMoment, ProbeMoment) : Boolean extension static ProbeMomentExtensions.GetValue(ProbeMoment) : DateTimeOffset extension static ProbeMomentExtensions.ValueOrNull(ProbeMoment?) : DateTimeOffset? +ProbeMoney + CompareTo(Object) : Int32 + CompareTo(ProbeMoney) : Int32 + Equals(Object) : Boolean + Equals(ProbeMoney) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Decimal + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ProbeMoneyJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ProbeMoneyTypeConverter : TypeConverter + new() + static From(Decimal) : ProbeMoney + static MaxValue : ProbeMoney + static MinValue : ProbeMoney + static Parse(ReadOnlySpan, IFormatProvider) : ProbeMoney + static Parse(ReadOnlySpan, IFormatProvider) : ProbeMoney + static Parse(String, IFormatProvider) : ProbeMoney + static TryParse(ReadOnlySpan, IFormatProvider, out ProbeMoney) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ProbeMoney) : Boolean + static TryParse(String, IFormatProvider, out ProbeMoney) : Boolean + static op_Addition(ProbeMoney, ProbeMoney) : ProbeMoney + static op_CheckedAddition(ProbeMoney, ProbeMoney) : ProbeMoney + static op_CheckedDivision(ProbeMoney, Decimal) : ProbeMoney + static op_CheckedDivision(ProbeMoney, ProbeMoney) : Decimal + static op_CheckedMultiply(ProbeMoney, Decimal) : ProbeMoney + static op_CheckedSubtraction(ProbeMoney, ProbeMoney) : ProbeMoney + static op_CheckedUnaryNegation(ProbeMoney) : ProbeMoney + static op_Division(ProbeMoney, Decimal) : ProbeMoney + static op_Division(ProbeMoney, ProbeMoney) : Decimal + static op_Equality(ProbeMoney, ProbeMoney) : Boolean + static op_GreaterThan(ProbeMoney, ProbeMoney) : Boolean + static op_GreaterThanOrEqual(ProbeMoney, ProbeMoney) : Boolean + static op_Inequality(ProbeMoney, ProbeMoney) : Boolean + static op_LessThan(ProbeMoney, ProbeMoney) : Boolean + static op_LessThanOrEqual(ProbeMoney, ProbeMoney) : Boolean + static op_Modulus(ProbeMoney, Decimal) : ProbeMoney + static op_Multiply(ProbeMoney, Decimal) : ProbeMoney + static op_Subtraction(ProbeMoney, ProbeMoney) : ProbeMoney + static op_UnaryNegation(ProbeMoney) : ProbeMoney + static op_UnaryPlus(ProbeMoney) : ProbeMoney + extension static ProbeMoneyExtensions.GetValue(ProbeMoney) : Decimal + extension static ProbeMoneyExtensions.ValueOrNull(ProbeMoney?) : Decimal? ProbeName CompareTo(Object) : Int32 CompareTo(ProbeName) : Int32 @@ -1583,6 +1657,290 @@ ProbeWeekday extension static ProbeWeekdayExtensions.GetValue(ProbeWeekday) : DayOfWeek extension static ProbeWeekdayExtensions.ValueOrNull(ProbeWeekday?) : DayOfWeek? # CodoMetis.TypeKit.Generators.Tests +BigIntegerAmount + CompareTo(BigIntegerAmount) : Int32 + CompareTo(Object) : Int32 + Equals(BigIntegerAmount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : BigInteger + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested BigIntegerAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested BigIntegerAmountTypeConverter : TypeConverter + new() + static From(BigInteger) : BigIntegerAmount + static Parse(ReadOnlySpan, IFormatProvider) : BigIntegerAmount + static Parse(ReadOnlySpan, IFormatProvider) : BigIntegerAmount + static Parse(String, IFormatProvider) : BigIntegerAmount + static TryParse(ReadOnlySpan, IFormatProvider, out BigIntegerAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out BigIntegerAmount) : Boolean + static TryParse(String, IFormatProvider, out BigIntegerAmount) : Boolean + static op_Addition(BigIntegerAmount, BigIntegerAmount) : BigIntegerAmount + static op_CheckedAddition(BigIntegerAmount, BigIntegerAmount) : BigIntegerAmount + static op_CheckedDivision(BigIntegerAmount, BigIntegerAmount) : BigIntegerAmount + static op_CheckedMultiply(BigIntegerAmount, BigIntegerAmount) : BigIntegerAmount + static op_CheckedSubtraction(BigIntegerAmount, BigIntegerAmount) : BigIntegerAmount + static op_CheckedUnaryNegation(BigIntegerAmount) : BigIntegerAmount + static op_Division(BigIntegerAmount, BigIntegerAmount) : BigIntegerAmount + static op_Equality(BigIntegerAmount, BigIntegerAmount) : Boolean + static op_GreaterThan(BigIntegerAmount, BigIntegerAmount) : Boolean + static op_GreaterThanOrEqual(BigIntegerAmount, BigIntegerAmount) : Boolean + static op_Inequality(BigIntegerAmount, BigIntegerAmount) : Boolean + static op_LessThan(BigIntegerAmount, BigIntegerAmount) : Boolean + static op_LessThanOrEqual(BigIntegerAmount, BigIntegerAmount) : Boolean + static op_Modulus(BigIntegerAmount, BigIntegerAmount) : BigIntegerAmount + static op_Multiply(BigIntegerAmount, BigIntegerAmount) : BigIntegerAmount + static op_Subtraction(BigIntegerAmount, BigIntegerAmount) : BigIntegerAmount + static op_UnaryNegation(BigIntegerAmount) : BigIntegerAmount + static op_UnaryPlus(BigIntegerAmount) : BigIntegerAmount + extension static BigIntegerAmountExtensions.GetValue(BigIntegerAmount) : BigInteger + extension static BigIntegerAmountExtensions.ValueOrNull(BigIntegerAmount?) : BigInteger? +ByteAmount + CompareTo(ByteAmount) : Int32 + CompareTo(Object) : Int32 + Equals(ByteAmount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Byte + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ByteAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ByteAmountTypeConverter : TypeConverter + new() + static From(Byte) : ByteAmount + static MaxValue : ByteAmount + static MinValue : ByteAmount + static Parse(ReadOnlySpan, IFormatProvider) : ByteAmount + static Parse(ReadOnlySpan, IFormatProvider) : ByteAmount + static Parse(String, IFormatProvider) : ByteAmount + static TryParse(ReadOnlySpan, IFormatProvider, out ByteAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ByteAmount) : Boolean + static TryParse(String, IFormatProvider, out ByteAmount) : Boolean + static op_Addition(ByteAmount, ByteAmount) : ByteAmount + static op_CheckedAddition(ByteAmount, ByteAmount) : ByteAmount + static op_CheckedDivision(ByteAmount, ByteAmount) : ByteAmount + static op_CheckedMultiply(ByteAmount, ByteAmount) : ByteAmount + static op_CheckedSubtraction(ByteAmount, ByteAmount) : ByteAmount + static op_CheckedUnaryNegation(ByteAmount) : ByteAmount + static op_Division(ByteAmount, ByteAmount) : ByteAmount + static op_Equality(ByteAmount, ByteAmount) : Boolean + static op_GreaterThan(ByteAmount, ByteAmount) : Boolean + static op_GreaterThanOrEqual(ByteAmount, ByteAmount) : Boolean + static op_Inequality(ByteAmount, ByteAmount) : Boolean + static op_LessThan(ByteAmount, ByteAmount) : Boolean + static op_LessThanOrEqual(ByteAmount, ByteAmount) : Boolean + static op_Modulus(ByteAmount, ByteAmount) : ByteAmount + static op_Multiply(ByteAmount, ByteAmount) : ByteAmount + static op_Subtraction(ByteAmount, ByteAmount) : ByteAmount + static op_UnaryNegation(ByteAmount) : ByteAmount + static op_UnaryPlus(ByteAmount) : ByteAmount + extension static ByteAmountExtensions.GetValue(ByteAmount) : Byte + extension static ByteAmountExtensions.ValueOrNull(ByteAmount?) : Byte? +CharAmount + CompareTo(CharAmount) : Int32 + CompareTo(Object) : Int32 + Equals(CharAmount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Char + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested CharAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested CharAmountTypeConverter : TypeConverter + new() + static From(Char) : CharAmount + static MaxValue : CharAmount + static MinValue : CharAmount + static Parse(ReadOnlySpan, IFormatProvider) : CharAmount + static Parse(ReadOnlySpan, IFormatProvider) : CharAmount + static Parse(String, IFormatProvider) : CharAmount + static TryParse(ReadOnlySpan, IFormatProvider, out CharAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out CharAmount) : Boolean + static TryParse(String, IFormatProvider, out CharAmount) : Boolean + static op_Addition(CharAmount, CharAmount) : CharAmount + static op_CheckedAddition(CharAmount, CharAmount) : CharAmount + static op_CheckedDivision(CharAmount, CharAmount) : CharAmount + static op_CheckedMultiply(CharAmount, CharAmount) : CharAmount + static op_CheckedSubtraction(CharAmount, CharAmount) : CharAmount + static op_CheckedUnaryNegation(CharAmount) : CharAmount + static op_Division(CharAmount, CharAmount) : CharAmount + static op_Equality(CharAmount, CharAmount) : Boolean + static op_GreaterThan(CharAmount, CharAmount) : Boolean + static op_GreaterThanOrEqual(CharAmount, CharAmount) : Boolean + static op_Inequality(CharAmount, CharAmount) : Boolean + static op_LessThan(CharAmount, CharAmount) : Boolean + static op_LessThanOrEqual(CharAmount, CharAmount) : Boolean + static op_Modulus(CharAmount, CharAmount) : CharAmount + static op_Multiply(CharAmount, CharAmount) : CharAmount + static op_Subtraction(CharAmount, CharAmount) : CharAmount + static op_UnaryNegation(CharAmount) : CharAmount + static op_UnaryPlus(CharAmount) : CharAmount + extension static CharAmountExtensions.GetValue(CharAmount) : Char + extension static CharAmountExtensions.ValueOrNull(CharAmount?) : Char? +DecimalAmount + CompareTo(DecimalAmount) : Int32 + CompareTo(Object) : Int32 + Equals(DecimalAmount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Decimal + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested DecimalAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested DecimalAmountTypeConverter : TypeConverter + new() + static From(Decimal) : DecimalAmount + static MaxValue : DecimalAmount + static MinValue : DecimalAmount + static Parse(ReadOnlySpan, IFormatProvider) : DecimalAmount + static Parse(ReadOnlySpan, IFormatProvider) : DecimalAmount + static Parse(String, IFormatProvider) : DecimalAmount + static TryParse(ReadOnlySpan, IFormatProvider, out DecimalAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out DecimalAmount) : Boolean + static TryParse(String, IFormatProvider, out DecimalAmount) : Boolean + static op_Addition(DecimalAmount, DecimalAmount) : DecimalAmount + static op_CheckedAddition(DecimalAmount, DecimalAmount) : DecimalAmount + static op_CheckedDivision(DecimalAmount, DecimalAmount) : DecimalAmount + static op_CheckedMultiply(DecimalAmount, DecimalAmount) : DecimalAmount + static op_CheckedSubtraction(DecimalAmount, DecimalAmount) : DecimalAmount + static op_CheckedUnaryNegation(DecimalAmount) : DecimalAmount + static op_Division(DecimalAmount, DecimalAmount) : DecimalAmount + static op_Equality(DecimalAmount, DecimalAmount) : Boolean + static op_GreaterThan(DecimalAmount, DecimalAmount) : Boolean + static op_GreaterThanOrEqual(DecimalAmount, DecimalAmount) : Boolean + static op_Inequality(DecimalAmount, DecimalAmount) : Boolean + static op_LessThan(DecimalAmount, DecimalAmount) : Boolean + static op_LessThanOrEqual(DecimalAmount, DecimalAmount) : Boolean + static op_Modulus(DecimalAmount, DecimalAmount) : DecimalAmount + static op_Multiply(DecimalAmount, DecimalAmount) : DecimalAmount + static op_Subtraction(DecimalAmount, DecimalAmount) : DecimalAmount + static op_UnaryNegation(DecimalAmount) : DecimalAmount + static op_UnaryPlus(DecimalAmount) : DecimalAmount + extension static DecimalAmountExtensions.GetValue(DecimalAmount) : Decimal + extension static DecimalAmountExtensions.ValueOrNull(DecimalAmount?) : Decimal? DefaultedAddress Equals(DefaultedAddress) : Boolean Equals(Object) : Boolean @@ -1685,6 +2043,150 @@ DocumentKind static Parse(String, Boolean) : DocumentKind static TryParse(String, Boolean, out DocumentKind) : Boolean static TryParse(String, out DocumentKind) : Boolean +DoubleAmount + CompareTo(DoubleAmount) : Int32 + CompareTo(Object) : Int32 + Equals(DoubleAmount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Double + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested DoubleAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested DoubleAmountTypeConverter : TypeConverter + new() + static From(Double) : DoubleAmount + static MaxValue : DoubleAmount + static MinValue : DoubleAmount + static Parse(ReadOnlySpan, IFormatProvider) : DoubleAmount + static Parse(ReadOnlySpan, IFormatProvider) : DoubleAmount + static Parse(String, IFormatProvider) : DoubleAmount + static TryParse(ReadOnlySpan, IFormatProvider, out DoubleAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out DoubleAmount) : Boolean + static TryParse(String, IFormatProvider, out DoubleAmount) : Boolean + static op_Addition(DoubleAmount, DoubleAmount) : DoubleAmount + static op_CheckedAddition(DoubleAmount, DoubleAmount) : DoubleAmount + static op_CheckedDivision(DoubleAmount, DoubleAmount) : DoubleAmount + static op_CheckedMultiply(DoubleAmount, DoubleAmount) : DoubleAmount + static op_CheckedSubtraction(DoubleAmount, DoubleAmount) : DoubleAmount + static op_CheckedUnaryNegation(DoubleAmount) : DoubleAmount + static op_Division(DoubleAmount, DoubleAmount) : DoubleAmount + static op_Equality(DoubleAmount, DoubleAmount) : Boolean + static op_GreaterThan(DoubleAmount, DoubleAmount) : Boolean + static op_GreaterThanOrEqual(DoubleAmount, DoubleAmount) : Boolean + static op_Inequality(DoubleAmount, DoubleAmount) : Boolean + static op_LessThan(DoubleAmount, DoubleAmount) : Boolean + static op_LessThanOrEqual(DoubleAmount, DoubleAmount) : Boolean + static op_Modulus(DoubleAmount, DoubleAmount) : DoubleAmount + static op_Multiply(DoubleAmount, DoubleAmount) : DoubleAmount + static op_Subtraction(DoubleAmount, DoubleAmount) : DoubleAmount + static op_UnaryNegation(DoubleAmount) : DoubleAmount + static op_UnaryPlus(DoubleAmount) : DoubleAmount + extension static DoubleAmountExtensions.GetValue(DoubleAmount) : Double + extension static DoubleAmountExtensions.ValueOrNull(DoubleAmount?) : Double? +DoubleAmountClass + $() : DoubleAmountClass + CompareTo(DoubleAmountClass) : Int32 + CompareTo(Object) : Int32 + Equals(DoubleAmountClass) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Double + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested DoubleAmountClassJsonConverter : JsonConverter, IStoredJsonConverterSource + nested DoubleAmountClassTypeConverter : TypeConverter + static From(Double) : DoubleAmountClass + static MaxValue : DoubleAmountClass + static MinValue : DoubleAmountClass + static Parse(ReadOnlySpan, IFormatProvider) : DoubleAmountClass + static Parse(ReadOnlySpan, IFormatProvider) : DoubleAmountClass + static Parse(String, IFormatProvider) : DoubleAmountClass + static TryParse(ReadOnlySpan, IFormatProvider, out DoubleAmountClass) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out DoubleAmountClass) : Boolean + static TryParse(String, IFormatProvider, out DoubleAmountClass) : Boolean + static op_Addition(DoubleAmountClass, DoubleAmountClass) : DoubleAmountClass + static op_CheckedAddition(DoubleAmountClass, DoubleAmountClass) : DoubleAmountClass + static op_CheckedDivision(DoubleAmountClass, DoubleAmountClass) : DoubleAmountClass + static op_CheckedMultiply(DoubleAmountClass, DoubleAmountClass) : DoubleAmountClass + static op_CheckedSubtraction(DoubleAmountClass, DoubleAmountClass) : DoubleAmountClass + static op_CheckedUnaryNegation(DoubleAmountClass) : DoubleAmountClass + static op_Division(DoubleAmountClass, DoubleAmountClass) : DoubleAmountClass + static op_Equality(DoubleAmountClass, DoubleAmountClass) : Boolean + static op_GreaterThan(DoubleAmountClass, DoubleAmountClass) : Boolean + static op_GreaterThanOrEqual(DoubleAmountClass, DoubleAmountClass) : Boolean + static op_Inequality(DoubleAmountClass, DoubleAmountClass) : Boolean + static op_LessThan(DoubleAmountClass, DoubleAmountClass) : Boolean + static op_LessThanOrEqual(DoubleAmountClass, DoubleAmountClass) : Boolean + static op_Modulus(DoubleAmountClass, DoubleAmountClass) : DoubleAmountClass + static op_Multiply(DoubleAmountClass, DoubleAmountClass) : DoubleAmountClass + static op_Subtraction(DoubleAmountClass, DoubleAmountClass) : DoubleAmountClass + static op_UnaryNegation(DoubleAmountClass) : DoubleAmountClass + static op_UnaryPlus(DoubleAmountClass) : DoubleAmountClass + extension static DoubleAmountClassExtensions.GetValue(DoubleAmountClass) : Double + extension static DoubleAmountClassExtensions.ValueOrNull(DoubleAmountClass) : Double? EarlyAllStatus Match(Func) : TResult Name : String @@ -1829,6 +2331,760 @@ EarlyTryParseStatus static Parse(String, Boolean) : EarlyTryParseStatus static TryParse(String, Boolean, out EarlyTryParseStatus) : Boolean static TryParse(String, out EarlyTryParseStatus) : Boolean +ExplicitlyAdded + CompareTo(ExplicitlyAdded) : Int32 + CompareTo(Object) : Int32 + Equals(ExplicitlyAdded) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested ExplicitlyAddedJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ExplicitlyAddedTypeConverter : TypeConverter + new() + static From(Int32) : ExplicitlyAdded + static MaxValue : ExplicitlyAdded + static MinValue : ExplicitlyAdded + static Parse(ReadOnlySpan, IFormatProvider) : ExplicitlyAdded + static Parse(ReadOnlySpan, IFormatProvider) : ExplicitlyAdded + static Parse(String, IFormatProvider) : ExplicitlyAdded + static TryParse(ReadOnlySpan, IFormatProvider, out ExplicitlyAdded) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out ExplicitlyAdded) : Boolean + static TryParse(String, IFormatProvider, out ExplicitlyAdded) : Boolean + static op_Equality(ExplicitlyAdded, ExplicitlyAdded) : Boolean + static op_GreaterThan(ExplicitlyAdded, ExplicitlyAdded) : Boolean + static op_GreaterThanOrEqual(ExplicitlyAdded, ExplicitlyAdded) : Boolean + static op_Inequality(ExplicitlyAdded, ExplicitlyAdded) : Boolean + static op_LessThan(ExplicitlyAdded, ExplicitlyAdded) : Boolean + static op_LessThanOrEqual(ExplicitlyAdded, ExplicitlyAdded) : Boolean + extension static ExplicitlyAddedExtensions.GetValue(ExplicitlyAdded) : Int32 + extension static ExplicitlyAddedExtensions.ValueOrNull(ExplicitlyAdded?) : Int32? +HalfAmount + CompareTo(HalfAmount) : Int32 + CompareTo(Object) : Int32 + Equals(HalfAmount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Half + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested HalfAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested HalfAmountTypeConverter : TypeConverter + new() + static From(Half) : HalfAmount + static MaxValue : HalfAmount + static MinValue : HalfAmount + static Parse(ReadOnlySpan, IFormatProvider) : HalfAmount + static Parse(ReadOnlySpan, IFormatProvider) : HalfAmount + static Parse(String, IFormatProvider) : HalfAmount + static TryParse(ReadOnlySpan, IFormatProvider, out HalfAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out HalfAmount) : Boolean + static TryParse(String, IFormatProvider, out HalfAmount) : Boolean + static op_Addition(HalfAmount, HalfAmount) : HalfAmount + static op_CheckedAddition(HalfAmount, HalfAmount) : HalfAmount + static op_CheckedDivision(HalfAmount, HalfAmount) : HalfAmount + static op_CheckedMultiply(HalfAmount, HalfAmount) : HalfAmount + static op_CheckedSubtraction(HalfAmount, HalfAmount) : HalfAmount + static op_CheckedUnaryNegation(HalfAmount) : HalfAmount + static op_Division(HalfAmount, HalfAmount) : HalfAmount + static op_Equality(HalfAmount, HalfAmount) : Boolean + static op_GreaterThan(HalfAmount, HalfAmount) : Boolean + static op_GreaterThanOrEqual(HalfAmount, HalfAmount) : Boolean + static op_Inequality(HalfAmount, HalfAmount) : Boolean + static op_LessThan(HalfAmount, HalfAmount) : Boolean + static op_LessThanOrEqual(HalfAmount, HalfAmount) : Boolean + static op_Modulus(HalfAmount, HalfAmount) : HalfAmount + static op_Multiply(HalfAmount, HalfAmount) : HalfAmount + static op_Subtraction(HalfAmount, HalfAmount) : HalfAmount + static op_UnaryNegation(HalfAmount) : HalfAmount + static op_UnaryPlus(HalfAmount) : HalfAmount + extension static HalfAmountExtensions.GetValue(HalfAmount) : Half + extension static HalfAmountExtensions.ValueOrNull(HalfAmount?) : Half? +InheritsAddition + $() : InheritsAddition + CompareTo(InheritsAddition) : Int32 + CompareTo(Object) : Int32 + Equals(AddingBase) : Boolean + Equals(InheritsAddition) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable> + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable> + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested InheritsAdditionJsonConverter : JsonConverter, IStoredJsonConverterSource + nested InheritsAdditionTypeConverter : TypeConverter + static From(Int32) : InheritsAddition + static MaxValue : InheritsAddition + static MinValue : InheritsAddition + static Parse(ReadOnlySpan, IFormatProvider) : InheritsAddition + static Parse(ReadOnlySpan, IFormatProvider) : InheritsAddition + static Parse(String, IFormatProvider) : InheritsAddition + static TryParse(ReadOnlySpan, IFormatProvider, out InheritsAddition) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out InheritsAddition) : Boolean + static TryParse(String, IFormatProvider, out InheritsAddition) : Boolean + static op_Equality(InheritsAddition, InheritsAddition) : Boolean + static op_GreaterThan(InheritsAddition, InheritsAddition) : Boolean + static op_GreaterThanOrEqual(InheritsAddition, InheritsAddition) : Boolean + static op_Inequality(InheritsAddition, InheritsAddition) : Boolean + static op_LessThan(InheritsAddition, InheritsAddition) : Boolean + static op_LessThanOrEqual(InheritsAddition, InheritsAddition) : Boolean + extension static InheritsAdditionExtensions.GetValue(InheritsAddition) : Int32 + extension static InheritsAdditionExtensions.ValueOrNull(InheritsAddition) : Int32? +Int128Amount + CompareTo(Int128Amount) : Int32 + CompareTo(Object) : Int32 + Equals(Int128Amount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int128 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested Int128AmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested Int128AmountTypeConverter : TypeConverter + new() + static From(Int128) : Int128Amount + static MaxValue : Int128Amount + static MinValue : Int128Amount + static Parse(ReadOnlySpan, IFormatProvider) : Int128Amount + static Parse(ReadOnlySpan, IFormatProvider) : Int128Amount + static Parse(String, IFormatProvider) : Int128Amount + static TryParse(ReadOnlySpan, IFormatProvider, out Int128Amount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out Int128Amount) : Boolean + static TryParse(String, IFormatProvider, out Int128Amount) : Boolean + static op_Addition(Int128Amount, Int128Amount) : Int128Amount + static op_CheckedAddition(Int128Amount, Int128Amount) : Int128Amount + static op_CheckedDivision(Int128Amount, Int128Amount) : Int128Amount + static op_CheckedMultiply(Int128Amount, Int128Amount) : Int128Amount + static op_CheckedSubtraction(Int128Amount, Int128Amount) : Int128Amount + static op_CheckedUnaryNegation(Int128Amount) : Int128Amount + static op_Division(Int128Amount, Int128Amount) : Int128Amount + static op_Equality(Int128Amount, Int128Amount) : Boolean + static op_GreaterThan(Int128Amount, Int128Amount) : Boolean + static op_GreaterThanOrEqual(Int128Amount, Int128Amount) : Boolean + static op_Inequality(Int128Amount, Int128Amount) : Boolean + static op_LessThan(Int128Amount, Int128Amount) : Boolean + static op_LessThanOrEqual(Int128Amount, Int128Amount) : Boolean + static op_Modulus(Int128Amount, Int128Amount) : Int128Amount + static op_Multiply(Int128Amount, Int128Amount) : Int128Amount + static op_Subtraction(Int128Amount, Int128Amount) : Int128Amount + static op_UnaryNegation(Int128Amount) : Int128Amount + static op_UnaryPlus(Int128Amount) : Int128Amount + extension static Int128AmountExtensions.GetValue(Int128Amount) : Int128 + extension static Int128AmountExtensions.ValueOrNull(Int128Amount?) : Int128? +Int16Amount + CompareTo(Int16Amount) : Int32 + CompareTo(Object) : Int32 + Equals(Int16Amount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int16 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested Int16AmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested Int16AmountTypeConverter : TypeConverter + new() + static From(Int16) : Int16Amount + static MaxValue : Int16Amount + static MinValue : Int16Amount + static Parse(ReadOnlySpan, IFormatProvider) : Int16Amount + static Parse(ReadOnlySpan, IFormatProvider) : Int16Amount + static Parse(String, IFormatProvider) : Int16Amount + static TryParse(ReadOnlySpan, IFormatProvider, out Int16Amount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out Int16Amount) : Boolean + static TryParse(String, IFormatProvider, out Int16Amount) : Boolean + static op_Addition(Int16Amount, Int16Amount) : Int16Amount + static op_CheckedAddition(Int16Amount, Int16Amount) : Int16Amount + static op_CheckedDivision(Int16Amount, Int16Amount) : Int16Amount + static op_CheckedMultiply(Int16Amount, Int16Amount) : Int16Amount + static op_CheckedSubtraction(Int16Amount, Int16Amount) : Int16Amount + static op_CheckedUnaryNegation(Int16Amount) : Int16Amount + static op_Division(Int16Amount, Int16Amount) : Int16Amount + static op_Equality(Int16Amount, Int16Amount) : Boolean + static op_GreaterThan(Int16Amount, Int16Amount) : Boolean + static op_GreaterThanOrEqual(Int16Amount, Int16Amount) : Boolean + static op_Inequality(Int16Amount, Int16Amount) : Boolean + static op_LessThan(Int16Amount, Int16Amount) : Boolean + static op_LessThanOrEqual(Int16Amount, Int16Amount) : Boolean + static op_Modulus(Int16Amount, Int16Amount) : Int16Amount + static op_Multiply(Int16Amount, Int16Amount) : Int16Amount + static op_Subtraction(Int16Amount, Int16Amount) : Int16Amount + static op_UnaryNegation(Int16Amount) : Int16Amount + static op_UnaryPlus(Int16Amount) : Int16Amount + extension static Int16AmountExtensions.GetValue(Int16Amount) : Int16 + extension static Int16AmountExtensions.ValueOrNull(Int16Amount?) : Int16? +Int32Amount + CompareTo(Int32Amount) : Int32 + CompareTo(Object) : Int32 + Equals(Int32Amount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested Int32AmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested Int32AmountTypeConverter : TypeConverter + new() + static From(Int32) : Int32Amount + static MaxValue : Int32Amount + static MinValue : Int32Amount + static Parse(ReadOnlySpan, IFormatProvider) : Int32Amount + static Parse(ReadOnlySpan, IFormatProvider) : Int32Amount + static Parse(String, IFormatProvider) : Int32Amount + static TryParse(ReadOnlySpan, IFormatProvider, out Int32Amount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out Int32Amount) : Boolean + static TryParse(String, IFormatProvider, out Int32Amount) : Boolean + static op_Addition(Int32Amount, Int32Amount) : Int32Amount + static op_CheckedAddition(Int32Amount, Int32Amount) : Int32Amount + static op_CheckedDivision(Int32Amount, Int32Amount) : Int32Amount + static op_CheckedMultiply(Int32Amount, Int32Amount) : Int32Amount + static op_CheckedSubtraction(Int32Amount, Int32Amount) : Int32Amount + static op_CheckedUnaryNegation(Int32Amount) : Int32Amount + static op_Division(Int32Amount, Int32Amount) : Int32Amount + static op_Equality(Int32Amount, Int32Amount) : Boolean + static op_GreaterThan(Int32Amount, Int32Amount) : Boolean + static op_GreaterThanOrEqual(Int32Amount, Int32Amount) : Boolean + static op_Inequality(Int32Amount, Int32Amount) : Boolean + static op_LessThan(Int32Amount, Int32Amount) : Boolean + static op_LessThanOrEqual(Int32Amount, Int32Amount) : Boolean + static op_Modulus(Int32Amount, Int32Amount) : Int32Amount + static op_Multiply(Int32Amount, Int32Amount) : Int32Amount + static op_Subtraction(Int32Amount, Int32Amount) : Int32Amount + static op_UnaryNegation(Int32Amount) : Int32Amount + static op_UnaryPlus(Int32Amount) : Int32Amount + extension static Int32AmountExtensions.GetValue(Int32Amount) : Int32 + extension static Int32AmountExtensions.ValueOrNull(Int32Amount?) : Int32? +Int32AmountClass + $() : Int32AmountClass + CompareTo(Int32AmountClass) : Int32 + CompareTo(Object) : Int32 + Equals(Int32AmountClass) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested Int32AmountClassJsonConverter : JsonConverter, IStoredJsonConverterSource + nested Int32AmountClassTypeConverter : TypeConverter + static From(Int32) : Int32AmountClass + static MaxValue : Int32AmountClass + static MinValue : Int32AmountClass + static Parse(ReadOnlySpan, IFormatProvider) : Int32AmountClass + static Parse(ReadOnlySpan, IFormatProvider) : Int32AmountClass + static Parse(String, IFormatProvider) : Int32AmountClass + static TryParse(ReadOnlySpan, IFormatProvider, out Int32AmountClass) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out Int32AmountClass) : Boolean + static TryParse(String, IFormatProvider, out Int32AmountClass) : Boolean + static op_Addition(Int32AmountClass, Int32AmountClass) : Int32AmountClass + static op_CheckedAddition(Int32AmountClass, Int32AmountClass) : Int32AmountClass + static op_CheckedDivision(Int32AmountClass, Int32AmountClass) : Int32AmountClass + static op_CheckedMultiply(Int32AmountClass, Int32AmountClass) : Int32AmountClass + static op_CheckedSubtraction(Int32AmountClass, Int32AmountClass) : Int32AmountClass + static op_CheckedUnaryNegation(Int32AmountClass) : Int32AmountClass + static op_Division(Int32AmountClass, Int32AmountClass) : Int32AmountClass + static op_Equality(Int32AmountClass, Int32AmountClass) : Boolean + static op_GreaterThan(Int32AmountClass, Int32AmountClass) : Boolean + static op_GreaterThanOrEqual(Int32AmountClass, Int32AmountClass) : Boolean + static op_Inequality(Int32AmountClass, Int32AmountClass) : Boolean + static op_LessThan(Int32AmountClass, Int32AmountClass) : Boolean + static op_LessThanOrEqual(Int32AmountClass, Int32AmountClass) : Boolean + static op_Modulus(Int32AmountClass, Int32AmountClass) : Int32AmountClass + static op_Multiply(Int32AmountClass, Int32AmountClass) : Int32AmountClass + static op_Subtraction(Int32AmountClass, Int32AmountClass) : Int32AmountClass + static op_UnaryNegation(Int32AmountClass) : Int32AmountClass + static op_UnaryPlus(Int32AmountClass) : Int32AmountClass + extension static Int32AmountClassExtensions.GetValue(Int32AmountClass) : Int32 + extension static Int32AmountClassExtensions.ValueOrNull(Int32AmountClass) : Int32? +Int64Amount + CompareTo(Int64Amount) : Int32 + CompareTo(Object) : Int32 + Equals(Int64Amount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int64 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested Int64AmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested Int64AmountTypeConverter : TypeConverter + new() + static From(Int64) : Int64Amount + static MaxValue : Int64Amount + static MinValue : Int64Amount + static Parse(ReadOnlySpan, IFormatProvider) : Int64Amount + static Parse(ReadOnlySpan, IFormatProvider) : Int64Amount + static Parse(String, IFormatProvider) : Int64Amount + static TryParse(ReadOnlySpan, IFormatProvider, out Int64Amount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out Int64Amount) : Boolean + static TryParse(String, IFormatProvider, out Int64Amount) : Boolean + static op_Addition(Int64Amount, Int64Amount) : Int64Amount + static op_CheckedAddition(Int64Amount, Int64Amount) : Int64Amount + static op_CheckedDivision(Int64Amount, Int64Amount) : Int64Amount + static op_CheckedMultiply(Int64Amount, Int64Amount) : Int64Amount + static op_CheckedSubtraction(Int64Amount, Int64Amount) : Int64Amount + static op_CheckedUnaryNegation(Int64Amount) : Int64Amount + static op_Division(Int64Amount, Int64Amount) : Int64Amount + static op_Equality(Int64Amount, Int64Amount) : Boolean + static op_GreaterThan(Int64Amount, Int64Amount) : Boolean + static op_GreaterThanOrEqual(Int64Amount, Int64Amount) : Boolean + static op_Inequality(Int64Amount, Int64Amount) : Boolean + static op_LessThan(Int64Amount, Int64Amount) : Boolean + static op_LessThanOrEqual(Int64Amount, Int64Amount) : Boolean + static op_Modulus(Int64Amount, Int64Amount) : Int64Amount + static op_Multiply(Int64Amount, Int64Amount) : Int64Amount + static op_Subtraction(Int64Amount, Int64Amount) : Int64Amount + static op_UnaryNegation(Int64Amount) : Int64Amount + static op_UnaryPlus(Int64Amount) : Int64Amount + extension static Int64AmountExtensions.GetValue(Int64Amount) : Int64 + extension static Int64AmountExtensions.ValueOrNull(Int64Amount?) : Int64? +IntPtrAmount + CompareTo(IntPtrAmount) : Int32 + CompareTo(Object) : Int32 + Equals(IntPtrAmount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : IntPtr + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested IntPtrAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested IntPtrAmountTypeConverter : TypeConverter + new() + static From(IntPtr) : IntPtrAmount + static MaxValue : IntPtrAmount + static MinValue : IntPtrAmount + static Parse(ReadOnlySpan, IFormatProvider) : IntPtrAmount + static Parse(ReadOnlySpan, IFormatProvider) : IntPtrAmount + static Parse(String, IFormatProvider) : IntPtrAmount + static TryParse(ReadOnlySpan, IFormatProvider, out IntPtrAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out IntPtrAmount) : Boolean + static TryParse(String, IFormatProvider, out IntPtrAmount) : Boolean + static op_Addition(IntPtrAmount, IntPtrAmount) : IntPtrAmount + static op_CheckedAddition(IntPtrAmount, IntPtrAmount) : IntPtrAmount + static op_CheckedDivision(IntPtrAmount, IntPtrAmount) : IntPtrAmount + static op_CheckedMultiply(IntPtrAmount, IntPtrAmount) : IntPtrAmount + static op_CheckedSubtraction(IntPtrAmount, IntPtrAmount) : IntPtrAmount + static op_CheckedUnaryNegation(IntPtrAmount) : IntPtrAmount + static op_Division(IntPtrAmount, IntPtrAmount) : IntPtrAmount + static op_Equality(IntPtrAmount, IntPtrAmount) : Boolean + static op_GreaterThan(IntPtrAmount, IntPtrAmount) : Boolean + static op_GreaterThanOrEqual(IntPtrAmount, IntPtrAmount) : Boolean + static op_Inequality(IntPtrAmount, IntPtrAmount) : Boolean + static op_LessThan(IntPtrAmount, IntPtrAmount) : Boolean + static op_LessThanOrEqual(IntPtrAmount, IntPtrAmount) : Boolean + static op_Modulus(IntPtrAmount, IntPtrAmount) : IntPtrAmount + static op_Multiply(IntPtrAmount, IntPtrAmount) : IntPtrAmount + static op_Subtraction(IntPtrAmount, IntPtrAmount) : IntPtrAmount + static op_UnaryNegation(IntPtrAmount) : IntPtrAmount + static op_UnaryPlus(IntPtrAmount) : IntPtrAmount + extension static IntPtrAmountExtensions.GetValue(IntPtrAmount) : IntPtr + extension static IntPtrAmountExtensions.ValueOrNull(IntPtrAmount?) : IntPtr? +InternalAmount (internal) + CompareTo(InternalAmount) : Int32 + CompareTo(Object) : Int32 + Equals(InternalAmount) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested InternalAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested InternalAmountTypeConverter : TypeConverter + new() + static From(Int32) : InternalAmount + static MaxValue : InternalAmount + static MinValue : InternalAmount + static Parse(ReadOnlySpan, IFormatProvider) : InternalAmount + static Parse(ReadOnlySpan, IFormatProvider) : InternalAmount + static Parse(String, IFormatProvider) : InternalAmount + static TryParse(ReadOnlySpan, IFormatProvider, out InternalAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out InternalAmount) : Boolean + static TryParse(String, IFormatProvider, out InternalAmount) : Boolean + static op_Addition(InternalAmount, InternalAmount) : InternalAmount + static op_CheckedAddition(InternalAmount, InternalAmount) : InternalAmount + static op_CheckedDivision(InternalAmount, InternalAmount) : InternalAmount + static op_CheckedMultiply(InternalAmount, InternalAmount) : InternalAmount + static op_CheckedSubtraction(InternalAmount, InternalAmount) : InternalAmount + static op_CheckedUnaryNegation(InternalAmount) : InternalAmount + static op_Division(InternalAmount, InternalAmount) : InternalAmount + static op_Equality(InternalAmount, InternalAmount) : Boolean + static op_GreaterThan(InternalAmount, InternalAmount) : Boolean + static op_GreaterThanOrEqual(InternalAmount, InternalAmount) : Boolean + static op_Inequality(InternalAmount, InternalAmount) : Boolean + static op_LessThan(InternalAmount, InternalAmount) : Boolean + static op_LessThanOrEqual(InternalAmount, InternalAmount) : Boolean + static op_Modulus(InternalAmount, InternalAmount) : InternalAmount + static op_Multiply(InternalAmount, InternalAmount) : InternalAmount + static op_Subtraction(InternalAmount, InternalAmount) : InternalAmount + static op_UnaryNegation(InternalAmount) : InternalAmount + static op_UnaryPlus(InternalAmount) : InternalAmount + extension internal static InternalAmountExtensions.GetValue(InternalAmount) : Int32 + extension internal static InternalAmountExtensions.ValueOrNull(InternalAmount?) : Int32? +Ledger.Entry + CompareTo(Ledger.Entry) : Int32 + CompareTo(Object) : Int32 + Equals(Ledger.Entry) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int64 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested EntryJsonConverter : JsonConverter, IStoredJsonConverterSource + nested EntryTypeConverter : TypeConverter + new() + static From(Int64) : Ledger.Entry + static MaxValue : Ledger.Entry + static MinValue : Ledger.Entry + static Parse(ReadOnlySpan, IFormatProvider) : Ledger.Entry + static Parse(ReadOnlySpan, IFormatProvider) : Ledger.Entry + static Parse(String, IFormatProvider) : Ledger.Entry + static TryParse(ReadOnlySpan, IFormatProvider, out Ledger.Entry) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out Ledger.Entry) : Boolean + static TryParse(String, IFormatProvider, out Ledger.Entry) : Boolean + static op_Addition(Ledger.Entry, Ledger.Entry) : Ledger.Entry + static op_CheckedAddition(Ledger.Entry, Ledger.Entry) : Ledger.Entry + static op_CheckedDivision(Ledger.Entry, Ledger.Entry) : Ledger.Entry + static op_CheckedMultiply(Ledger.Entry, Ledger.Entry) : Ledger.Entry + static op_CheckedSubtraction(Ledger.Entry, Ledger.Entry) : Ledger.Entry + static op_CheckedUnaryNegation(Ledger.Entry) : Ledger.Entry + static op_Division(Ledger.Entry, Ledger.Entry) : Ledger.Entry + static op_Equality(Ledger.Entry, Ledger.Entry) : Boolean + static op_GreaterThan(Ledger.Entry, Ledger.Entry) : Boolean + static op_GreaterThanOrEqual(Ledger.Entry, Ledger.Entry) : Boolean + static op_Inequality(Ledger.Entry, Ledger.Entry) : Boolean + static op_LessThan(Ledger.Entry, Ledger.Entry) : Boolean + static op_LessThanOrEqual(Ledger.Entry, Ledger.Entry) : Boolean + static op_Modulus(Ledger.Entry, Ledger.Entry) : Ledger.Entry + static op_Multiply(Ledger.Entry, Ledger.Entry) : Ledger.Entry + static op_Subtraction(Ledger.Entry, Ledger.Entry) : Ledger.Entry + static op_UnaryNegation(Ledger.Entry) : Ledger.Entry + static op_UnaryPlus(Ledger.Entry) : Ledger.Entry + extension static LedgerEntryExtensions.GetValue(Ledger.Entry) : Int64 + extension static LedgerEntryExtensions.ValueOrNull(Ledger.Entry?) : Int64? LocalOrder.Stage Match(Func, Func) : TResult Name : String @@ -3824,6 +5080,188 @@ ParityUShort static op_LessThanOrEqual(ParityUShort, ParityUShort) : Boolean extension static ParityUShortExtensions.GetValue(ParityUShort) : UInt16 extension static ParityUShortExtensions.ValueOrNull(ParityUShort?) : UInt16? +RoundedMoney + CompareTo(Object) : Int32 + CompareTo(RoundedMoney) : Int32 + Equals(Object) : Boolean + Equals(RoundedMoney) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Decimal + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested RoundedMoneyJsonConverter : JsonConverter, IStoredJsonConverterSource + nested RoundedMoneyTypeConverter : TypeConverter + new() + static From(Decimal) : RoundedMoney + static MaxValue : RoundedMoney + static MinValue : RoundedMoney + static Parse(ReadOnlySpan, IFormatProvider) : RoundedMoney + static Parse(ReadOnlySpan, IFormatProvider) : RoundedMoney + static Parse(String, IFormatProvider) : RoundedMoney + static TryParse(ReadOnlySpan, IFormatProvider, out RoundedMoney) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out RoundedMoney) : Boolean + static TryParse(String, IFormatProvider, out RoundedMoney) : Boolean + static op_Addition(RoundedMoney, RoundedMoney) : RoundedMoney + static op_CheckedSubtraction(RoundedMoney, RoundedMoney) : RoundedMoney + static op_Equality(RoundedMoney, RoundedMoney) : Boolean + static op_GreaterThan(RoundedMoney, RoundedMoney) : Boolean + static op_GreaterThanOrEqual(RoundedMoney, RoundedMoney) : Boolean + static op_Inequality(RoundedMoney, RoundedMoney) : Boolean + static op_LessThan(RoundedMoney, RoundedMoney) : Boolean + static op_LessThanOrEqual(RoundedMoney, RoundedMoney) : Boolean + static op_Subtraction(RoundedMoney, RoundedMoney) : RoundedMoney + extension static RoundedMoneyExtensions.GetValue(RoundedMoney) : Decimal + extension static RoundedMoneyExtensions.ValueOrNull(RoundedMoney?) : Decimal? +SByteAmount + CompareTo(Object) : Int32 + CompareTo(SByteAmount) : Int32 + Equals(Object) : Boolean + Equals(SByteAmount) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : SByte + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested SByteAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested SByteAmountTypeConverter : TypeConverter + new() + static From(SByte) : SByteAmount + static MaxValue : SByteAmount + static MinValue : SByteAmount + static Parse(ReadOnlySpan, IFormatProvider) : SByteAmount + static Parse(ReadOnlySpan, IFormatProvider) : SByteAmount + static Parse(String, IFormatProvider) : SByteAmount + static TryParse(ReadOnlySpan, IFormatProvider, out SByteAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out SByteAmount) : Boolean + static TryParse(String, IFormatProvider, out SByteAmount) : Boolean + static op_Addition(SByteAmount, SByteAmount) : SByteAmount + static op_CheckedAddition(SByteAmount, SByteAmount) : SByteAmount + static op_CheckedDivision(SByteAmount, SByteAmount) : SByteAmount + static op_CheckedMultiply(SByteAmount, SByteAmount) : SByteAmount + static op_CheckedSubtraction(SByteAmount, SByteAmount) : SByteAmount + static op_CheckedUnaryNegation(SByteAmount) : SByteAmount + static op_Division(SByteAmount, SByteAmount) : SByteAmount + static op_Equality(SByteAmount, SByteAmount) : Boolean + static op_GreaterThan(SByteAmount, SByteAmount) : Boolean + static op_GreaterThanOrEqual(SByteAmount, SByteAmount) : Boolean + static op_Inequality(SByteAmount, SByteAmount) : Boolean + static op_LessThan(SByteAmount, SByteAmount) : Boolean + static op_LessThanOrEqual(SByteAmount, SByteAmount) : Boolean + static op_Modulus(SByteAmount, SByteAmount) : SByteAmount + static op_Multiply(SByteAmount, SByteAmount) : SByteAmount + static op_Subtraction(SByteAmount, SByteAmount) : SByteAmount + static op_UnaryNegation(SByteAmount) : SByteAmount + static op_UnaryPlus(SByteAmount) : SByteAmount + extension static SByteAmountExtensions.GetValue(SByteAmount) : SByte + extension static SByteAmountExtensions.ValueOrNull(SByteAmount?) : SByte? +Score + CompareTo(Object) : Int32 + CompareTo(Score) : Int32 + Equals(Object) : Boolean + Equals(Score) : Boolean + GetHashCode() : Int32 + Revalidate() : Result + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IParsable + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValidatedValue + implements IValueObject + implements IValueObjectMaterializer + nested ScoreJsonConverter : JsonConverter, IStoredJsonConverterSource + nested ScoreTypeConverter : TypeConverter + new() + static Create(Int32) : Result + static FromKnownGood(Int32, String) : Score + static Parse(ReadOnlySpan, IFormatProvider) : Score + static Parse(ReadOnlySpan, IFormatProvider) : Score + static Parse(String, IFormatProvider) : Score + static TryFrom(Int32) : Option + static TryParse(ReadOnlySpan, IFormatProvider, out Score) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out Score) : Boolean + static TryParse(String, IFormatProvider, out Score) : Boolean + static op_Addition(Score, Score) : Score + static op_Equality(Score, Score) : Boolean + static op_GreaterThan(Score, Score) : Boolean + static op_GreaterThanOrEqual(Score, Score) : Boolean + static op_Inequality(Score, Score) : Boolean + static op_LessThan(Score, Score) : Boolean + static op_LessThanOrEqual(Score, Score) : Boolean + extension static ScoreExtensions.GetValue(Score) : Int32 + extension static ScoreExtensions.ValueOrNull(Score?) : Int32? ShippableStatus Equals(Object) : Boolean Equals(ShippableStatus) : Boolean @@ -3855,6 +5293,78 @@ ShippableStatus static op_Inequality(ShippableStatus, ShippableStatus) : Boolean extension static ShippableStatusExtensions.GetValue(ShippableStatus) : LocalStatus extension static ShippableStatusExtensions.ValueOrNull(ShippableStatus?) : LocalStatus +SingleAmount + CompareTo(Object) : Int32 + CompareTo(SingleAmount) : Int32 + Equals(Object) : Boolean + Equals(SingleAmount) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Single + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested SingleAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested SingleAmountTypeConverter : TypeConverter + new() + static From(Single) : SingleAmount + static MaxValue : SingleAmount + static MinValue : SingleAmount + static Parse(ReadOnlySpan, IFormatProvider) : SingleAmount + static Parse(ReadOnlySpan, IFormatProvider) : SingleAmount + static Parse(String, IFormatProvider) : SingleAmount + static TryParse(ReadOnlySpan, IFormatProvider, out SingleAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out SingleAmount) : Boolean + static TryParse(String, IFormatProvider, out SingleAmount) : Boolean + static op_Addition(SingleAmount, SingleAmount) : SingleAmount + static op_CheckedAddition(SingleAmount, SingleAmount) : SingleAmount + static op_CheckedDivision(SingleAmount, SingleAmount) : SingleAmount + static op_CheckedMultiply(SingleAmount, SingleAmount) : SingleAmount + static op_CheckedSubtraction(SingleAmount, SingleAmount) : SingleAmount + static op_CheckedUnaryNegation(SingleAmount) : SingleAmount + static op_Division(SingleAmount, SingleAmount) : SingleAmount + static op_Equality(SingleAmount, SingleAmount) : Boolean + static op_GreaterThan(SingleAmount, SingleAmount) : Boolean + static op_GreaterThanOrEqual(SingleAmount, SingleAmount) : Boolean + static op_Inequality(SingleAmount, SingleAmount) : Boolean + static op_LessThan(SingleAmount, SingleAmount) : Boolean + static op_LessThanOrEqual(SingleAmount, SingleAmount) : Boolean + static op_Modulus(SingleAmount, SingleAmount) : SingleAmount + static op_Multiply(SingleAmount, SingleAmount) : SingleAmount + static op_Subtraction(SingleAmount, SingleAmount) : SingleAmount + static op_UnaryNegation(SingleAmount) : SingleAmount + static op_UnaryPlus(SingleAmount) : SingleAmount + extension static SingleAmountExtensions.GetValue(SingleAmount) : Single + extension static SingleAmountExtensions.ValueOrNull(SingleAmount?) : Single? StageOfOrder Equals(Object) : Boolean Equals(StageOfOrder) : Boolean @@ -3884,6 +5394,36 @@ StageOfOrder static op_Inequality(StageOfOrder, StageOfOrder) : Boolean extension static StageOfOrderExtensions.GetValue(StageOfOrder) : LocalOrder.Stage extension static StageOfOrderExtensions.ValueOrNull(StageOfOrder?) : LocalOrder.Stage +Stamp + Equals(Object) : Boolean + Equals(Stamp) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + Value : Tick + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + implements IAdditionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IPlainValueObject + implements ISubtractionOperators + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested StampJsonConverter : JsonConverter, IStoredJsonConverterSource + new() + static From(Tick) : Stamp + static op_Addition(Stamp, Elapsed) : Stamp + static op_CheckedAddition(Stamp, Elapsed) : Stamp + static op_CheckedSubtraction(Stamp, Stamp) : Elapsed + static op_Equality(Stamp, Stamp) : Boolean + static op_Inequality(Stamp, Stamp) : Boolean + static op_Subtraction(Stamp, Stamp) : Elapsed + extension static StampExtensions.GetValue(Stamp) : Tick + extension static StampExtensions.ValueOrNull(Stamp?) : Tick? StatusLabel $() : StatusLabel Equals(Object) : Boolean @@ -4196,3 +5736,361 @@ ThrowingSku static op_Inequality(ThrowingSku, ThrowingSku) : Boolean extension static ThrowingSkuExtensions.GetValue(ThrowingSku) : ProbeSkuText extension static ThrowingSkuExtensions.ValueOrNull(ThrowingSku?) : ProbeSkuText +UInt128Amount + CompareTo(Object) : Int32 + CompareTo(UInt128Amount) : Int32 + Equals(Object) : Boolean + Equals(UInt128Amount) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : UInt128 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested UInt128AmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested UInt128AmountTypeConverter : TypeConverter + new() + static From(UInt128) : UInt128Amount + static MaxValue : UInt128Amount + static MinValue : UInt128Amount + static Parse(ReadOnlySpan, IFormatProvider) : UInt128Amount + static Parse(ReadOnlySpan, IFormatProvider) : UInt128Amount + static Parse(String, IFormatProvider) : UInt128Amount + static TryParse(ReadOnlySpan, IFormatProvider, out UInt128Amount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out UInt128Amount) : Boolean + static TryParse(String, IFormatProvider, out UInt128Amount) : Boolean + static op_Addition(UInt128Amount, UInt128Amount) : UInt128Amount + static op_CheckedAddition(UInt128Amount, UInt128Amount) : UInt128Amount + static op_CheckedDivision(UInt128Amount, UInt128Amount) : UInt128Amount + static op_CheckedMultiply(UInt128Amount, UInt128Amount) : UInt128Amount + static op_CheckedSubtraction(UInt128Amount, UInt128Amount) : UInt128Amount + static op_CheckedUnaryNegation(UInt128Amount) : UInt128Amount + static op_Division(UInt128Amount, UInt128Amount) : UInt128Amount + static op_Equality(UInt128Amount, UInt128Amount) : Boolean + static op_GreaterThan(UInt128Amount, UInt128Amount) : Boolean + static op_GreaterThanOrEqual(UInt128Amount, UInt128Amount) : Boolean + static op_Inequality(UInt128Amount, UInt128Amount) : Boolean + static op_LessThan(UInt128Amount, UInt128Amount) : Boolean + static op_LessThanOrEqual(UInt128Amount, UInt128Amount) : Boolean + static op_Modulus(UInt128Amount, UInt128Amount) : UInt128Amount + static op_Multiply(UInt128Amount, UInt128Amount) : UInt128Amount + static op_Subtraction(UInt128Amount, UInt128Amount) : UInt128Amount + static op_UnaryNegation(UInt128Amount) : UInt128Amount + static op_UnaryPlus(UInt128Amount) : UInt128Amount + extension static UInt128AmountExtensions.GetValue(UInt128Amount) : UInt128 + extension static UInt128AmountExtensions.ValueOrNull(UInt128Amount?) : UInt128? +UInt16Amount + CompareTo(Object) : Int32 + CompareTo(UInt16Amount) : Int32 + Equals(Object) : Boolean + Equals(UInt16Amount) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : UInt16 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested UInt16AmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested UInt16AmountTypeConverter : TypeConverter + new() + static From(UInt16) : UInt16Amount + static MaxValue : UInt16Amount + static MinValue : UInt16Amount + static Parse(ReadOnlySpan, IFormatProvider) : UInt16Amount + static Parse(ReadOnlySpan, IFormatProvider) : UInt16Amount + static Parse(String, IFormatProvider) : UInt16Amount + static TryParse(ReadOnlySpan, IFormatProvider, out UInt16Amount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out UInt16Amount) : Boolean + static TryParse(String, IFormatProvider, out UInt16Amount) : Boolean + static op_Addition(UInt16Amount, UInt16Amount) : UInt16Amount + static op_CheckedAddition(UInt16Amount, UInt16Amount) : UInt16Amount + static op_CheckedDivision(UInt16Amount, UInt16Amount) : UInt16Amount + static op_CheckedMultiply(UInt16Amount, UInt16Amount) : UInt16Amount + static op_CheckedSubtraction(UInt16Amount, UInt16Amount) : UInt16Amount + static op_CheckedUnaryNegation(UInt16Amount) : UInt16Amount + static op_Division(UInt16Amount, UInt16Amount) : UInt16Amount + static op_Equality(UInt16Amount, UInt16Amount) : Boolean + static op_GreaterThan(UInt16Amount, UInt16Amount) : Boolean + static op_GreaterThanOrEqual(UInt16Amount, UInt16Amount) : Boolean + static op_Inequality(UInt16Amount, UInt16Amount) : Boolean + static op_LessThan(UInt16Amount, UInt16Amount) : Boolean + static op_LessThanOrEqual(UInt16Amount, UInt16Amount) : Boolean + static op_Modulus(UInt16Amount, UInt16Amount) : UInt16Amount + static op_Multiply(UInt16Amount, UInt16Amount) : UInt16Amount + static op_Subtraction(UInt16Amount, UInt16Amount) : UInt16Amount + static op_UnaryNegation(UInt16Amount) : UInt16Amount + static op_UnaryPlus(UInt16Amount) : UInt16Amount + extension static UInt16AmountExtensions.GetValue(UInt16Amount) : UInt16 + extension static UInt16AmountExtensions.ValueOrNull(UInt16Amount?) : UInt16? +UInt32Amount + CompareTo(Object) : Int32 + CompareTo(UInt32Amount) : Int32 + Equals(Object) : Boolean + Equals(UInt32Amount) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : UInt32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested UInt32AmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested UInt32AmountTypeConverter : TypeConverter + new() + static From(UInt32) : UInt32Amount + static MaxValue : UInt32Amount + static MinValue : UInt32Amount + static Parse(ReadOnlySpan, IFormatProvider) : UInt32Amount + static Parse(ReadOnlySpan, IFormatProvider) : UInt32Amount + static Parse(String, IFormatProvider) : UInt32Amount + static TryParse(ReadOnlySpan, IFormatProvider, out UInt32Amount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out UInt32Amount) : Boolean + static TryParse(String, IFormatProvider, out UInt32Amount) : Boolean + static op_Addition(UInt32Amount, UInt32Amount) : UInt32Amount + static op_CheckedAddition(UInt32Amount, UInt32Amount) : UInt32Amount + static op_CheckedDivision(UInt32Amount, UInt32Amount) : UInt32Amount + static op_CheckedMultiply(UInt32Amount, UInt32Amount) : UInt32Amount + static op_CheckedSubtraction(UInt32Amount, UInt32Amount) : UInt32Amount + static op_CheckedUnaryNegation(UInt32Amount) : UInt32Amount + static op_Division(UInt32Amount, UInt32Amount) : UInt32Amount + static op_Equality(UInt32Amount, UInt32Amount) : Boolean + static op_GreaterThan(UInt32Amount, UInt32Amount) : Boolean + static op_GreaterThanOrEqual(UInt32Amount, UInt32Amount) : Boolean + static op_Inequality(UInt32Amount, UInt32Amount) : Boolean + static op_LessThan(UInt32Amount, UInt32Amount) : Boolean + static op_LessThanOrEqual(UInt32Amount, UInt32Amount) : Boolean + static op_Modulus(UInt32Amount, UInt32Amount) : UInt32Amount + static op_Multiply(UInt32Amount, UInt32Amount) : UInt32Amount + static op_Subtraction(UInt32Amount, UInt32Amount) : UInt32Amount + static op_UnaryNegation(UInt32Amount) : UInt32Amount + static op_UnaryPlus(UInt32Amount) : UInt32Amount + extension static UInt32AmountExtensions.GetValue(UInt32Amount) : UInt32 + extension static UInt32AmountExtensions.ValueOrNull(UInt32Amount?) : UInt32? +UInt64Amount + CompareTo(Object) : Int32 + CompareTo(UInt64Amount) : Int32 + Equals(Object) : Boolean + Equals(UInt64Amount) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : UInt64 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested UInt64AmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested UInt64AmountTypeConverter : TypeConverter + new() + static From(UInt64) : UInt64Amount + static MaxValue : UInt64Amount + static MinValue : UInt64Amount + static Parse(ReadOnlySpan, IFormatProvider) : UInt64Amount + static Parse(ReadOnlySpan, IFormatProvider) : UInt64Amount + static Parse(String, IFormatProvider) : UInt64Amount + static TryParse(ReadOnlySpan, IFormatProvider, out UInt64Amount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out UInt64Amount) : Boolean + static TryParse(String, IFormatProvider, out UInt64Amount) : Boolean + static op_Addition(UInt64Amount, UInt64Amount) : UInt64Amount + static op_CheckedAddition(UInt64Amount, UInt64Amount) : UInt64Amount + static op_CheckedDivision(UInt64Amount, UInt64Amount) : UInt64Amount + static op_CheckedMultiply(UInt64Amount, UInt64Amount) : UInt64Amount + static op_CheckedSubtraction(UInt64Amount, UInt64Amount) : UInt64Amount + static op_CheckedUnaryNegation(UInt64Amount) : UInt64Amount + static op_Division(UInt64Amount, UInt64Amount) : UInt64Amount + static op_Equality(UInt64Amount, UInt64Amount) : Boolean + static op_GreaterThan(UInt64Amount, UInt64Amount) : Boolean + static op_GreaterThanOrEqual(UInt64Amount, UInt64Amount) : Boolean + static op_Inequality(UInt64Amount, UInt64Amount) : Boolean + static op_LessThan(UInt64Amount, UInt64Amount) : Boolean + static op_LessThanOrEqual(UInt64Amount, UInt64Amount) : Boolean + static op_Modulus(UInt64Amount, UInt64Amount) : UInt64Amount + static op_Multiply(UInt64Amount, UInt64Amount) : UInt64Amount + static op_Subtraction(UInt64Amount, UInt64Amount) : UInt64Amount + static op_UnaryNegation(UInt64Amount) : UInt64Amount + static op_UnaryPlus(UInt64Amount) : UInt64Amount + extension static UInt64AmountExtensions.GetValue(UInt64Amount) : UInt64 + extension static UInt64AmountExtensions.ValueOrNull(UInt64Amount?) : UInt64? +UIntPtrAmount + CompareTo(Object) : Int32 + CompareTo(UIntPtrAmount) : Int32 + Equals(Object) : Boolean + Equals(UIntPtrAmount) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : UIntPtr + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IArithmetic + implements IComparable + implements IComparable + implements IComparisonOperators + implements IDivisionOperators + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IModulusOperators + implements IMultiplyOperators + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUnaryNegationOperators + implements IUnaryPlusOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested UIntPtrAmountJsonConverter : JsonConverter, IStoredJsonConverterSource + nested UIntPtrAmountTypeConverter : TypeConverter + new() + static From(UIntPtr) : UIntPtrAmount + static MaxValue : UIntPtrAmount + static MinValue : UIntPtrAmount + static Parse(ReadOnlySpan, IFormatProvider) : UIntPtrAmount + static Parse(ReadOnlySpan, IFormatProvider) : UIntPtrAmount + static Parse(String, IFormatProvider) : UIntPtrAmount + static TryParse(ReadOnlySpan, IFormatProvider, out UIntPtrAmount) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out UIntPtrAmount) : Boolean + static TryParse(String, IFormatProvider, out UIntPtrAmount) : Boolean + static op_Addition(UIntPtrAmount, UIntPtrAmount) : UIntPtrAmount + static op_CheckedAddition(UIntPtrAmount, UIntPtrAmount) : UIntPtrAmount + static op_CheckedDivision(UIntPtrAmount, UIntPtrAmount) : UIntPtrAmount + static op_CheckedMultiply(UIntPtrAmount, UIntPtrAmount) : UIntPtrAmount + static op_CheckedSubtraction(UIntPtrAmount, UIntPtrAmount) : UIntPtrAmount + static op_CheckedUnaryNegation(UIntPtrAmount) : UIntPtrAmount + static op_Division(UIntPtrAmount, UIntPtrAmount) : UIntPtrAmount + static op_Equality(UIntPtrAmount, UIntPtrAmount) : Boolean + static op_GreaterThan(UIntPtrAmount, UIntPtrAmount) : Boolean + static op_GreaterThanOrEqual(UIntPtrAmount, UIntPtrAmount) : Boolean + static op_Inequality(UIntPtrAmount, UIntPtrAmount) : Boolean + static op_LessThan(UIntPtrAmount, UIntPtrAmount) : Boolean + static op_LessThanOrEqual(UIntPtrAmount, UIntPtrAmount) : Boolean + static op_Modulus(UIntPtrAmount, UIntPtrAmount) : UIntPtrAmount + static op_Multiply(UIntPtrAmount, UIntPtrAmount) : UIntPtrAmount + static op_Subtraction(UIntPtrAmount, UIntPtrAmount) : UIntPtrAmount + static op_UnaryNegation(UIntPtrAmount) : UIntPtrAmount + static op_UnaryPlus(UIntPtrAmount) : UIntPtrAmount + extension static UIntPtrAmountExtensions.GetValue(UIntPtrAmount) : UIntPtr + extension static UIntPtrAmountExtensions.ValueOrNull(UIntPtrAmount?) : UIntPtr? diff --git a/test/CodoMetis.TypeKit.Samples/Docs/ValueObjectsSamples.cs b/test/CodoMetis.TypeKit.Samples/Docs/ValueObjectsSamples.cs index af021a8c..a47c634f 100644 --- a/test/CodoMetis.TypeKit.Samples/Docs/ValueObjectsSamples.cs +++ b/test/CodoMetis.TypeKit.Samples/Docs/ValueObjectsSamples.cs @@ -78,6 +78,14 @@ public static Result Create(int value) } // end sample +// sample: value-objects/arithmetic +public readonly partial record struct Money : IValue, + IAdditionOperators, + ISubtractionOperators, + IMultiplyOperators, // Money * 3m + IDivisionOperators; // two amounts make a ratio +// end sample + // sample: value-objects/other-wrapped-types public readonly partial record struct Weekday : IValue; @@ -170,6 +178,17 @@ public static (bool Widest, string Shown, bool Ordered) Numbers() return (widest, shown, ordered); } + public static (Money Total, Money Tripled, decimal Share) Arithmetic() + { + // sample: value-objects/arithmetic-use + Money total = Money.From(10.50m) + Money.From(4.50m); // 15.00 + Money tripled = total * 3m; // 45.00 + decimal share = Money.From(3m) / Money.From(4m); // 0.75 + // end sample + + return (total, tripled, share); + } + public static (Weekday Day, Weekday Fifth, DueOn Due, bool Sooner, bool Sortable) OtherWrappedTypes() { // sample: value-objects/other-wrapped-types-use @@ -298,6 +317,18 @@ public void Numbers_are_what_the_comments_say() Weight.From(1.5m).ToString().ShouldBe("1.5"); } + [Fact] + public void Arithmetic_is_what_the_comments_say() + { + var (total, tripled, share) = Usage.Arithmetic(); + + (Money.From(10.50m) + Money.From(4.50m)).ShouldBeShownIn(Readme, money => money.ToString()); + (total * 3m).ShouldBeShownIn(Readme, money => money.ToString()); + (Money.From(3m) / Money.From(4m)).ShouldBeShownIn(Readme, ratio => ratio.ToString(System.Globalization.CultureInfo.InvariantCulture)); + (total, tripled, share).ShouldBe((Money.From(15.00m), Money.From(45.00m), 0.75m)); + Should.Throw(() => unchecked(Money.From(decimal.MaxValue) + Money.From(1m)), "a decimal overflows in either context"); + } + [Fact] public void Other_wrapped_types_parse_and_compare_as_the_comments_say() { diff --git a/test/oracles/axes.md b/test/oracles/axes.md index 05e1cbff..c1beeda6 100644 --- a/test/oracles/axes.md +++ b/test/oracles/axes.md @@ -20,6 +20,9 @@ footprint, it runs with the suite. key the wrapped type's generation. `MappingTests`, `KeyGenerationVariantTests`, `ColumnFacetVariantTests`. - **One set of rules (differential).** `TryFrom`, `Parse`, the JSON converter, the type converter and the binders refuse what `Create` refuses. `EntryPointValidationTests`. +- **Arithmetic parity (differential).** A value object's generated arithmetic operator gives what the wrapped + type's own gives, value or exception, unchecked and checked, for every numeric wrapped type and both record + shapes. `ArithmeticParityTests`. - **No refusal quotes the input (exception).** A sentinel in refused input never reaches a message. `UnreadableInputTests`. - **Off the wire (exception).** `Option` and `Result` refuse System.Text.Json in both directions. `NotWireTypeTests`. @@ -40,6 +43,7 @@ footprint, it runs with the suite. | EF | the query | `QueryShapes` | `QueryTranslationVariantTests` | | | Analyzers | where a type argument is written, and what it is | `TypeArgumentSites`, `TypeArgumentKinds` (`test/CodoMetis.TypeKit.Analyzers.Tests/Variants/`) | `TypeArgumentConstraintVariantTests` | | | Generation | the declaration's shape | record struct, record class, struct, class; sealed or not; over each wrapped kind | `BuildOutcomeTests`, `GeneratedSurfaceTests` | | +| Generation | the operator interfaces a plain value object declares | each arithmetic interface, the other operand and the result as the value object or another type, declared directly or through another interface, written by hand (public, explicit, a base record's explicit) or not; over every numeric wrapped type and both record shapes | `ArithmeticParityTests`, `ArithmeticTests`, `ArithmeticBuildTests` | an operator interface beside a hand-written `CompareTo` | An empty last column means nobody has looked yet, not that nothing is missing. Fill it when a feature lands or a finding names a combination no test varied. diff --git a/test/oracles/claims.json b/test/oracles/claims.json index 47a89c5b..22158fe5 100644 --- a/test/oracles/claims.json +++ b/test/oracles/claims.json @@ -1595,6 +1595,140 @@ "InlineValueObjectsSettingTests" ] }, + { + "id": "C28-checked-context-is-the-underlying-types", + "source": "docs/value-objects.md#add-subtract-and-scale", + "quote": "`checked(a + b)` uses the checked operator, as it would for an `int`.", + "claim": "`checked(a + b)` uses the checked operator, as it would for an `int`.", + "oracle": "differential", + "surfaces": [ + "GeneratedArithmetic.*" + ], + "heldBy": [ + "ArithmeticParityTests", + "ArithmeticTests" + ] + }, + { + "id": "C28-checked-operator-not-completed", + "source": "src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members", + "quote": "A `checked` operator you write needs the plain one beside it, as C# requires (CS9025), and TypeKit doesn't write that one for you either.", + "claim": "A `checked` operator you write needs the plain one beside it, as C# requires (CS9025), and TypeKit doesn't write that one for you either.", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*" + ], + "heldBy": [ + "ArithmeticBuildTests" + ] + }, + { + "id": "C28-dates-get-no-operator", + "source": "docs/value-objects.md#add-subtract-and-scale", + "quote": "`DateTime`, `TimeSpan`, `DateOnly` and `Guid` implement none of them, so a value object over one gets no operator, and the interface you declared fails the build (CS0535).", + "claim": "`DateTime`, `TimeSpan`, `DateOnly` and `Guid` implement none of them, so a value object over one gets no operator, and the interface you declared fails the build (CS0535).", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*" + ], + "heldBy": [ + "ArithmeticBuildTests" + ] + }, + { + "id": "C28-ef-between-value-objects", + "source": "docs/value-objects.md#add-subtract-and-scale", + "quote": "An operator between two value objects runs in the database, as it would for the underlying type: `o.Price + o.Tax > limit` keeps the rows memory keeps.", + "claim": "An operator between two value objects runs in the database, as it would for the underlying type: `o.Price + o.Tax > limit` keeps the rows memory keeps.", + "oracle": "differential", + "surfaces": [ + "GeneratedArithmetic.*" + ], + "heldBy": [ + "ArithmeticQueryTests" + ] + }, + { + "id": "C28-nothing-unless-declared", + "source": "docs/value-objects.md#add-subtract-and-scale", + "quote": "Nothing is generated unless you declare it.", + "claim": "Nothing is generated unless you declare it.", + "oracle": "claim", + "surfaces": [ + "IPlainValueObject.*" + ], + "heldBy": [ + "ArithmeticTests" + ] + }, + { + "id": "C28-one-operator-per-signature", + "source": "src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members", + "quote": "only the first is generated, and the second fails the build (CS0738) until you implement it explicitly.", + "claim": "only the first is generated, and the second fails the build (CS0738) until you implement it explicitly.", + "oracle": "exception", + "surfaces": [ + "IPlainValueObject.*" + ], + "heldBy": [ + "ArithmeticBuildTests" + ] + }, + { + "id": "C28-own-operator-kept", + "source": "docs/value-objects.md#add-subtract-and-scale", + "quote": "Write one yourself, or implement the interface explicitly, and TypeKit keeps it, with its checked form.", + "claim": "Write one yourself, or implement the interface explicitly, and TypeKit keeps it, with its checked form.", + "oracle": "claim", + "surfaces": [ + "IPlainValueObject.*" + ], + "heldBy": [ + "ArithmeticTests" + ] + }, + { + "id": "C28-record-class-null-operand", + "source": "src/CodoMetis.TypeKit.Generators/README.md#what-is-generated", + "quote": "A `null` operand of a record class throws `ArgumentNullException`.", + "claim": "A `null` operand of a record class throws `ArgumentNullException`.", + "oracle": "exception", + "surfaces": [ + "GeneratedArithmetic.*" + ], + "heldBy": [ + "ArithmeticTests" + ] + }, + { + "id": "C28-the-underlying-types-operator", + "source": "docs/value-objects.md#add-subtract-and-scale", + "quote": "Each operator does what the underlying type's operator does: the same result, the same overflow, the same checked context.", + "claim": "Each operator does what the underlying type's operator does: the same result, the same overflow, the same checked context.", + "oracle": "differential", + "surfaces": [ + "GeneratedArithmetic.*", + "IPlainValueObject.*" + ], + "heldBy": [ + "ArithmeticParityTests", + "ArithmeticTests" + ] + }, + { + "id": "C28-validated-gets-none", + "source": "docs/value-objects.md#add-subtract-and-scale", + "quote": "A validated value object gets none.", + "claim": "A validated value object gets none.", + "oracle": "exception", + "surfaces": [ + "IValidatedValue.*" + ], + "heldBy": [ + "ArithmeticBuildTests", + "ArithmeticTests" + ] + }, { "id": "E01-column-type", "source": "docs/ef-core.md#the-model", diff --git a/test/oracles/oracles.json b/test/oracles/oracles.json index 26396de8..401bd38c 100644 --- a/test/oracles/oracles.json +++ b/test/oracles/oracles.json @@ -5,6 +5,12 @@ "kind": "completeness", "against": "every public member of the base package that returns a task, by reflection: a callback runs where the task it waited for completed, and nothing resumes on the caller's context" }, + { + "class": "ArithmeticParityTests", + "project": "CodoMetis.TypeKit.Generators.Tests", + "kind": "differential", + "against": "the wrapped type's own operator of the same interface, called by the same generic code in the same checked or unchecked context, for every numeric wrapped type and both record shapes" + }, { "class": "JsonParityTests", "project": "CodoMetis.TypeKit.Generators.Tests", diff --git a/test/oracles/seeds/arithmetic-checked-forms-not-generated.patch b/test/oracles/seeds/arithmetic-checked-forms-not-generated.patch new file mode 100644 index 00000000..f1fbc1e5 --- /dev/null +++ b/test/oracles/seeds/arithmetic-checked-forms-not-generated.patch @@ -0,0 +1,17 @@ +Seed: The checked forms of the arithmetic operators are not generated, so a checked context reaches the interface's default, the unchecked operator +Oracles: ArithmeticParityTests +Fails: every checked row of an integer type whose sum, difference, product or negation leaves the type's range, where the wrapped type throws OverflowException and the value object wraps around + +diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs +index 5a9acb8..ce47b03 100644 +--- a/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs ++++ b/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs +@@ -133,8 +133,6 @@ internal sealed partial class ValueObjectArithmeticAspect : TypeAspect + + Introduce(@operator.Kind, @operator.Helper); + +- if (@operator is { CheckedKind: { } checkedKind, CheckedHelper: { } checkedHelper }) +- Introduce(checkedKind, checkedHelper); + + void Introduce(OperatorKind kind, string helperName) + { diff --git a/tools/ConsumerSmokeTest/Consumers/aot/Domain/Shop.cs b/tools/ConsumerSmokeTest/Consumers/aot/Domain/Shop.cs index 381956e9..5e22ec85 100644 --- a/tools/ConsumerSmokeTest/Consumers/aot/Domain/Shop.cs +++ b/tools/ConsumerSmokeTest/Consumers/aot/Domain/Shop.cs @@ -32,9 +32,10 @@ public enum Rule { Reserved } // One per JSON strategy: a reader method (Guid), the serializer's contract for a number, the // fallback with a built-in converter (Uri, an enum), a string, and parsing through a TypeConverter. +// Quantity and Amount declare arithmetic, whose operators call the wrapped type's through a type parameter. public readonly partial record struct OrderId : IValue; -public readonly partial record struct Quantity : IValue; -public readonly partial record struct Amount : IValue; +public readonly partial record struct Quantity : IValue, System.Numerics.IAdditionOperators; +public readonly partial record struct Amount : IValue, System.Numerics.IAdditionOperators, System.Numerics.IMultiplyOperators; public readonly partial record struct Link : IValue; public readonly partial record struct Weekday : IValue; public readonly partial record struct Name : IValue; diff --git a/tools/ConsumerSmokeTest/Consumers/aot/Web/Program.cs b/tools/ConsumerSmokeTest/Consumers/aot/Web/Program.cs index a8ff2494..62e4d65a 100644 --- a/tools/ConsumerSmokeTest/Consumers/aot/Web/Program.cs +++ b/tools/ConsumerSmokeTest/Consumers/aot/Web/Program.cs @@ -66,6 +66,19 @@ await Check("set-json-read", async () => return $"{ReferenceEquals(tracking.Status, OrderStatus.Open)} {tracking.Counts[OrderStatus.Shipped]}"; }); +// Arithmetic a value object declares: the wrapped type's operators, called directly, through generic code, +// and in a checked context, which picks the checked operator as it would for an int. +await Check("arithmetic", async () => +{ + var total = (Amount.From(1.5m) + Amount.From(2m)) * 2m; + string overflow; + try { _ = checked(Quantity.From(int.MaxValue) + Quantity.From(1)); overflow = "wrapped around"; } + catch (OverflowException) { overflow = "OverflowException"; } + return $"{total} {Twice(Quantity.From(21))} {overflow}"; +}); + +static T Twice(T value) where T : System.Numerics.IAdditionOperators => value + value; + // Parsing, including through a TypeConverter (NodaTime's LocalDate). await Check("parse", async () => $"{Quantity.Parse("42", null).Value} {Weekday.Parse("Monday", null).Value} {Link.Parse("https://p.test/", null).Value} {Day.Parse("2026-09-27", null).Value:yyyy-MM-dd}"); await Check("tryparse-refused", async () => Code.TryParse("abc", null, out _).ToString()); diff --git a/tools/ConsumerSmokeTest/Program.cs b/tools/ConsumerSmokeTest/Program.cs index 6365952a..9ed7cd87 100644 --- a/tools/ConsumerSmokeTest/Program.cs +++ b/tools/ConsumerSmokeTest/Program.cs @@ -423,6 +423,7 @@ private void Aot() AssertContains("""decimal-factory="7.8" string {"Price":"7.8"} string string""", "aot.out", "a factory in the options and one a property names are asked for the converter they make, and the stored-JSON factory keeps the generated description"); AssertContains("openapi-decimal=\"string\" \"decimal\"", "aot.out", "the document describes a value object over a decimal as the described converter writes it"); AssertContains("option-refused=THREW NotSupportedException: Option is not a wire type", "aot.out", "Option refuses JSON with its own message"); + AssertContains("arithmetic=7.0 42 OverflowException", "aot.out", "the operators a value object declares are the wrapped type's, through generic code and in a checked context"); AssertContains("parse=42 Monday https://p.test/ 2026-09-27", "aot.out", "the generated Parse, including through a TypeConverter"); AssertContains("parse-after-lookup=True 2026-09-27T12:00:00Z", "aot.out", "and after TypeDescriptor described the wrapped type unregistered"); AssertContains("tryparse-refused=False", "aot.out", "the generated TryParse applies Create"); From 5b0723add978d4c95dc0462b59c588d73e098692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Gro=C3=9F?= Date: Fri, 30 Oct 2026 14:39:34 +0100 Subject: [PATCH 2/3] fix: an interface's explicit implementation of an arithmetic operator is kept An interface can implement a generic-math operator explicitly, and every type that declares it reaches that implementation until it declares a public operator of its own. The arithmetic aspect looked for one on the value object and its base records only, so it generated a public `+` beside `IAddsLeft`'s, which returns the left operand: generic addition read 3 where the interface's answered 1, and code that compiled on 2.3 changed behaviour on 2.4 (found in an outside review of #188). The preservation check now reads the interfaces the value object implements too, its base records' included, and keeps that operator with its checked form, as it keeps the type's own. Input class: where an existing implementation of the operator comes from. ArithmeticTests now covers a generic interface over TSelf, one two interfaces down, an interface of a base record, and an interface for a single value object, beside which `-` is still generated. Reverting the check fails the test with 3 for 1. Measured on the way, outside this change: the generated IMinMaxValue and IFormattable members replace an interface's explicit implementation the same way; that predates 2.4 and is filed on its own. Defect-Class: GTS Found-By: OR Co-Authored-By: Claude Opus 5.5 --- ...opt-in-and-a-hand-written-one-is-a-seam.md | 6 +- design/aspects.md | 2 +- docs/value-objects.md | 4 +- src/CodoMetis.TypeKit.Generators/README.md | 2 +- .../ValueObjectArithmeticAspect.cs | 2 +- .../ValueObjectDeclaration.cs | 15 +- .../ArithmeticTests.cs | 56 +++++ .../GeneratedSurface.verified.txt | 223 ++++++++++++++++++ test/oracles/axes.md | 2 +- test/oracles/claims.json | 4 +- tools/BuildVerdict/build-verdict-remedies.tsv | 1 + 11 files changed, 303 insertions(+), 14 deletions(-) diff --git a/design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md b/design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md index b8f6bbf2..8cbb33ad 100644 --- a/design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md +++ b/design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md @@ -36,7 +36,7 @@ the same. An operand or a result of another type passes through as it is. `DateTime`, `TimeSpan`, `DateOnly` and `Guid` implement none of the operator interfaces (measured in the .NET 10 reference assemblies: they were dropped before .NET 7 shipped). - **A hand-written operator is a seam.** One the value object declares for the same operands, of either kind, - whatever it returns, or an explicit implementation of the interface, its own or a base record's, is kept, + whatever it returns, or an explicit implementation of the interface, its own, a base record's or an interface's, is kept, and neither form of that operator is generated; the operators not written are. Decision 28 would make it CMTK1011, but code that implemented these interfaces by hand on 2.3 must build unchanged on 2.4, and decision 34 lets no minor report more under an Error. Nothing generated depends on an arithmetic operator, @@ -46,7 +46,9 @@ the same. An operand or a result of another type passes through as it is. generating the `+` would have built it, adding by the wrapped type's rules outside a checked context and the user's inside one; it is kept failing. A public operator generated beside a base record's explicit implementation would not implement the interface, which the base does: generic code would reach the base's, `a + b` - ours. + ours. One beside an interface's explicit implementation, the `+` of an `IMoneyMath` that every type + declaring it reaches by default, replaced it: generic addition read 3 where the interface's answered 1 (an + outside review of #188). - **One operator per signature.** C# allows one `operator /(Money, Money)`, so of two interfaces that ask for it, the first declared gets it and the second fails the build (CS0738), to be implemented explicitly by hand. - **A record class's operand of its own type** is checked for `null` and throws `ArgumentNullException` diff --git a/design/aspects.md b/design/aspects.md index c36016ab..81691214 100644 --- a/design/aspects.md +++ b/design/aspects.md @@ -16,7 +16,7 @@ declares, where the wrapped type implements the same interface with the value object replaced by itself, each calling the wrapped type's through `GeneratedArithmetic`, a constrained generic call with its context stated. A hand-written operator for the same operands, or an explicit implementation of the interface, its - own or a base record's, is a seam: kept with its `checked` form, never CMTK1011, so code that wrote them on + own, a base record's or an interface's, is a seam: kept with its `checked` form, never CMTK1011, so code that wrote them on 2.3 builds unchanged. - **Declarations that cannot be generated** are errors, so no type is left half-generated: CMTK1000/1001/1002 (not `partial`, not a record, a struct not `readonly`), CMTK1003 (more than one diff --git a/docs/value-objects.md b/docs/value-objects.md index 82aadc85..3d1371f2 100644 --- a/docs/value-objects.md +++ b/docs/value-objects.md @@ -228,8 +228,8 @@ object or another type, as long as the underlying type has that operator itself. - **Plain value objects only.** A validated value object gets none. An operator can't return a `Result`, and a sum outside the range `Create` accepts would have to throw. The interface you declared fails the build (CS0535) until you write the operator yourself. -- **Your own operator is kept.** Write one yourself, or implement the interface explicitly, and TypeKit - keeps it, with its checked form. It generates only the operators you haven't written. +- **Your own operator is kept.** Write one yourself, or implement the interface explicitly, in the type, a + base record or an interface of yours, and TypeKit keeps it, with its checked form. It generates only the operators you haven't written. - **The checked context.** `checked(a + b)` uses the checked operator, as it would for an `int`. A value object over an `int` throws `OverflowException` there, and wraps around outside it. - **In an EF Core query.** An operator between two value objects runs in the database, as it would for diff --git a/src/CodoMetis.TypeKit.Generators/README.md b/src/CodoMetis.TypeKit.Generators/README.md index 438acf9b..f317d985 100644 --- a/src/CodoMetis.TypeKit.Generators/README.md +++ b/src/CodoMetis.TypeKit.Generators/README.md @@ -217,7 +217,7 @@ A value object may declare its own `TryFrom`, `FromKnownGood`, `Revalidate`, `Co `ToString()`. Each one is kept, and whatever depends on it is built from it. An arithmetic operator you write is kept as well, and so is an explicit implementation of its -interface, yours or a base record's. Its `checked` form is then yours too: TypeKit generates neither +interface, yours, a base record's or one an interface of yours supplies. Its `checked` form is then yours too: TypeKit generates neither form of that operator, and generates the ones you didn't write. A `checked` operator you write needs the plain one beside it, as C# requires (CS9025), and TypeKit doesn't write that one for you either. Code that wrote these operators by hand before 2.4 builds unchanged. C# allows one operator per pair of operand types, so of two interfaces diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs index 5a9acb86..1e7a9d39 100644 --- a/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs +++ b/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs @@ -83,7 +83,7 @@ internal sealed class ArithmeticOperatorArguments /// /// /// An operator the value object already has is kept, with its checked form: one it declares for the -/// same operands, or an explicit implementation of the interface, its own or a base record's. Code that +/// same operands, or an explicit implementation of the interface, its own, a base record's or an interface's. Code that /// implemented these interfaces by hand before compiles unchanged, so this is no refusal (decision 121). /// /// diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs index 8578ae5c..36cd8375 100644 --- a/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs +++ b/src/CodoMetis.TypeKit.Generators/ValueObjectDeclaration.cs @@ -340,12 +340,19 @@ internal static IEnumerable ExplicitImplementationsOf(INamedType target, } /// - /// or a base record implements a member of - /// explicitly, compared as the instance it is, type arguments included: what a generic base implements - /// for IAdditionOperators<TSelf, TSelf, TSelf> is read for the value object it derives. + /// , a base record or an interface it implements implements a member of + /// explicitly, compared as the instance it is, type arguments included: what a + /// generic base implements for IAdditionOperators<TSelf, TSelf, TSelf> is read for the value + /// object it derives. /// + /// + /// An interface's explicit implementation is the default every implementing type reaches until it declares + /// its own public member: a generated + replaced IMoneyMath<TSelf>'s, and generic + /// addition read 3 where the interface's answered 1. + /// internal static bool ImplementsExplicitly(INamedType target, INamedType @interface) => - SelfAndBases(target).Any(type => type.Methods.Any(method => method is { IsImplicitlyDeclared: false, IsExplicitInterfaceImplementation: true } + SelfAndBases(target).Concat(target.AllImplementedInterfaces) + .Any(type => type.Methods.Any(method => method is { IsImplicitlyDeclared: false, IsExplicitInterfaceImplementation: true } && method.ExplicitInterfaceImplementations.Any(implemented => implemented.DeclaringType.Equals(@interface)))); /// diff --git a/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticTests.cs b/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticTests.cs index d4a1cb8d..7c62676b 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticTests.cs +++ b/test/CodoMetis.TypeKit.Generators.Tests/ArithmeticTests.cs @@ -32,6 +32,38 @@ public abstract record AddingBase : IAdditionOperatorsIts + is its base record's, which every caller through the interface reaches: none is generated. public sealed partial record InheritsAddition : AddingBase, IValue; +/// An interface that implements + explicitly, the default of every type that declares it: the left operand. +public interface IAddsLeft : IAdditionOperators where TSelf : IAddsLeft +{ + static TSelf IAdditionOperators.operator +(TSelf left, TSelf right) => left; +} + +/// 's +, one interface further down. +public interface IAddsLeftToo : IAddsLeft where TSelf : IAddsLeftToo; + +/// Its + is its interface's: none is generated. +public readonly partial record struct AddsThroughInterface : IValue, IAddsLeft; + +/// Its + is an interface's that its own interface derives from. +public readonly partial record struct AddsThroughInheritedInterface : IValue, IAddsLeftToo; + +/// A base record whose interface implements +. +public abstract record AddsThroughBase : IAddsLeft where TSelf : AddsThroughBase; + +/// Its + is its base record's interface's. +public sealed partial record AddsThroughBaseInterface : AddsThroughBase, IValue; + +/// An interface of one value object's, not generic, that adds a fee of one. +#pragma warning disable CA2260 // TSelf is the value object, not the interface: the shape under test. +public interface IAddsFee : IAdditionOperators +#pragma warning restore CA2260 +{ + static FeeMoney IAdditionOperators.operator +(FeeMoney left, FeeMoney right) => FeeMoney.From(left.Value + right.Value + 1m); +} + +/// Its + is 's; its -, which no interface implements, is generated. +public readonly partial record struct FeeMoney : IValue, IAddsFee, ISubtractionOperators; + /// A validated value object with the + it had to write by hand before 2.4, which still compiles. public readonly partial record struct Score : IValidatedValue, IAdditionOperators { @@ -187,6 +219,30 @@ public void A_base_records_explicit_implementation_is_kept() Operators(typeof(InheritsAddition), "op_CheckedAddition").ShouldBeEmpty(); } + /// + /// An interface's explicit implementation is what every type that declares the interface reaches until it + /// declares a public operator of its own: one generated here replaced it, and generic addition read 3 where the + /// interface's answered 1. Through a generic interface, one further down, one of a base record and one of a single + /// value object's. + /// + [Fact] + public void An_interfaces_explicit_implementation_is_kept() + { + Add(AddsThroughInterface.From(1), AddsThroughInterface.From(2)).ShouldBe(AddsThroughInterface.From(1)); + CheckedAdd(AddsThroughInterface.From(1), AddsThroughInterface.From(2)).ShouldBe(AddsThroughInterface.From(1)); + Add(AddsThroughInheritedInterface.From(1), AddsThroughInheritedInterface.From(2)).ShouldBe(AddsThroughInheritedInterface.From(1)); + Add(AddsThroughBaseInterface.From(1), AddsThroughBaseInterface.From(2)).ShouldBe(AddsThroughBaseInterface.From(1)); + Add(FeeMoney.From(1m), FeeMoney.From(2m)).ShouldBe(FeeMoney.From(4m)); + + foreach (var type in new[] { typeof(AddsThroughInterface), typeof(AddsThroughInheritedInterface), typeof(AddsThroughBaseInterface), typeof(FeeMoney) }) + { + Operators(type, "op_Addition").ShouldBeEmpty(type.Name); + Operators(type, "op_CheckedAddition").ShouldBeEmpty(type.Name); + } + + (FeeMoney.From(3m) - FeeMoney.From(1m)).ShouldBe(FeeMoney.From(2m)); + } + [Fact] public void A_validated_value_object_keeps_its_own_operator_and_gets_none() { diff --git a/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt b/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt index 19837f84..cfaff2bd 100644 --- a/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt +++ b/test/CodoMetis.TypeKit.Generators.Tests/GeneratedSurface.verified.txt @@ -1657,6 +1657,172 @@ ProbeWeekday extension static ProbeWeekdayExtensions.GetValue(ProbeWeekday) : DayOfWeek extension static ProbeWeekdayExtensions.ValueOrNull(ProbeWeekday?) : DayOfWeek? # CodoMetis.TypeKit.Generators.Tests +AddsThroughBaseInterface + $() : AddsThroughBaseInterface + CompareTo(AddsThroughBaseInterface) : Int32 + CompareTo(Object) : Int32 + Equals(AddsThroughBase) : Boolean + Equals(AddsThroughBaseInterface) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IAddsLeft + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable> + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements IReimplementedEquatable> + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested AddsThroughBaseInterfaceJsonConverter : JsonConverter, IStoredJsonConverterSource + nested AddsThroughBaseInterfaceTypeConverter : TypeConverter + static From(Int32) : AddsThroughBaseInterface + static MaxValue : AddsThroughBaseInterface + static MinValue : AddsThroughBaseInterface + static Parse(ReadOnlySpan, IFormatProvider) : AddsThroughBaseInterface + static Parse(ReadOnlySpan, IFormatProvider) : AddsThroughBaseInterface + static Parse(String, IFormatProvider) : AddsThroughBaseInterface + static TryParse(ReadOnlySpan, IFormatProvider, out AddsThroughBaseInterface) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out AddsThroughBaseInterface) : Boolean + static TryParse(String, IFormatProvider, out AddsThroughBaseInterface) : Boolean + static op_Equality(AddsThroughBaseInterface, AddsThroughBaseInterface) : Boolean + static op_GreaterThan(AddsThroughBaseInterface, AddsThroughBaseInterface) : Boolean + static op_GreaterThanOrEqual(AddsThroughBaseInterface, AddsThroughBaseInterface) : Boolean + static op_Inequality(AddsThroughBaseInterface, AddsThroughBaseInterface) : Boolean + static op_LessThan(AddsThroughBaseInterface, AddsThroughBaseInterface) : Boolean + static op_LessThanOrEqual(AddsThroughBaseInterface, AddsThroughBaseInterface) : Boolean + extension static AddsThroughBaseInterfaceExtensions.GetValue(AddsThroughBaseInterface) : Int32 + extension static AddsThroughBaseInterfaceExtensions.ValueOrNull(AddsThroughBaseInterface) : Int32? +AddsThroughInheritedInterface + CompareTo(AddsThroughInheritedInterface) : Int32 + CompareTo(Object) : Int32 + Equals(AddsThroughInheritedInterface) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IAddsLeft + implements IAddsLeftToo + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested AddsThroughInheritedInterfaceJsonConverter : JsonConverter, IStoredJsonConverterSource + nested AddsThroughInheritedInterfaceTypeConverter : TypeConverter + new() + static From(Int32) : AddsThroughInheritedInterface + static MaxValue : AddsThroughInheritedInterface + static MinValue : AddsThroughInheritedInterface + static Parse(ReadOnlySpan, IFormatProvider) : AddsThroughInheritedInterface + static Parse(ReadOnlySpan, IFormatProvider) : AddsThroughInheritedInterface + static Parse(String, IFormatProvider) : AddsThroughInheritedInterface + static TryParse(ReadOnlySpan, IFormatProvider, out AddsThroughInheritedInterface) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out AddsThroughInheritedInterface) : Boolean + static TryParse(String, IFormatProvider, out AddsThroughInheritedInterface) : Boolean + static op_Equality(AddsThroughInheritedInterface, AddsThroughInheritedInterface) : Boolean + static op_GreaterThan(AddsThroughInheritedInterface, AddsThroughInheritedInterface) : Boolean + static op_GreaterThanOrEqual(AddsThroughInheritedInterface, AddsThroughInheritedInterface) : Boolean + static op_Inequality(AddsThroughInheritedInterface, AddsThroughInheritedInterface) : Boolean + static op_LessThan(AddsThroughInheritedInterface, AddsThroughInheritedInterface) : Boolean + static op_LessThanOrEqual(AddsThroughInheritedInterface, AddsThroughInheritedInterface) : Boolean + extension static AddsThroughInheritedInterfaceExtensions.GetValue(AddsThroughInheritedInterface) : Int32 + extension static AddsThroughInheritedInterfaceExtensions.ValueOrNull(AddsThroughInheritedInterface?) : Int32? +AddsThroughInterface + CompareTo(AddsThroughInterface) : Int32 + CompareTo(Object) : Int32 + Equals(AddsThroughInterface) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Int32 + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IAddsLeft + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested AddsThroughInterfaceJsonConverter : JsonConverter, IStoredJsonConverterSource + nested AddsThroughInterfaceTypeConverter : TypeConverter + new() + static From(Int32) : AddsThroughInterface + static MaxValue : AddsThroughInterface + static MinValue : AddsThroughInterface + static Parse(ReadOnlySpan, IFormatProvider) : AddsThroughInterface + static Parse(ReadOnlySpan, IFormatProvider) : AddsThroughInterface + static Parse(String, IFormatProvider) : AddsThroughInterface + static TryParse(ReadOnlySpan, IFormatProvider, out AddsThroughInterface) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out AddsThroughInterface) : Boolean + static TryParse(String, IFormatProvider, out AddsThroughInterface) : Boolean + static op_Equality(AddsThroughInterface, AddsThroughInterface) : Boolean + static op_GreaterThan(AddsThroughInterface, AddsThroughInterface) : Boolean + static op_GreaterThanOrEqual(AddsThroughInterface, AddsThroughInterface) : Boolean + static op_Inequality(AddsThroughInterface, AddsThroughInterface) : Boolean + static op_LessThan(AddsThroughInterface, AddsThroughInterface) : Boolean + static op_LessThanOrEqual(AddsThroughInterface, AddsThroughInterface) : Boolean + extension static AddsThroughInterfaceExtensions.GetValue(AddsThroughInterface) : Int32 + extension static AddsThroughInterfaceExtensions.ValueOrNull(AddsThroughInterface?) : Int32? BigIntegerAmount CompareTo(BigIntegerAmount) : Int32 CompareTo(Object) : Int32 @@ -2384,6 +2550,63 @@ ExplicitlyAdded static op_LessThanOrEqual(ExplicitlyAdded, ExplicitlyAdded) : Boolean extension static ExplicitlyAddedExtensions.GetValue(ExplicitlyAdded) : Int32 extension static ExplicitlyAddedExtensions.ValueOrNull(ExplicitlyAdded?) : Int32? +FeeMoney + CompareTo(FeeMoney) : Int32 + CompareTo(Object) : Int32 + Equals(FeeMoney) : Boolean + Equals(Object) : Boolean + GetHashCode() : Int32 + ToString() : String + ToString(String, IFormatProvider) : String + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + TryFormat(Span, out Int32, ReadOnlySpan, IFormatProvider) : Boolean + Value : Decimal + attribute CompilerGeneratedAttribute + attribute GeneratedValueObjectAttribute + attribute JsonConverterAttribute + attribute TypeConverterAttribute + implements IAdditionOperators + implements IAddsFee + implements IComparable + implements IComparable + implements IComparisonOperators + implements IConvertible + implements IEqualityOperators + implements IEquatable + implements IFormattable + implements IMinMaxValue + implements IParsable + implements IPlainValueObject + implements ISpanFormattable + implements ISpanParsable + implements ISubtractionOperators + implements IUtf8SpanFormattable + implements IUtf8SpanParsable + implements IValue + implements IValueObject + implements IValueObjectMaterializer + nested FeeMoneyJsonConverter : JsonConverter, IStoredJsonConverterSource + nested FeeMoneyTypeConverter : TypeConverter + new() + static From(Decimal) : FeeMoney + static MaxValue : FeeMoney + static MinValue : FeeMoney + static Parse(ReadOnlySpan, IFormatProvider) : FeeMoney + static Parse(ReadOnlySpan, IFormatProvider) : FeeMoney + static Parse(String, IFormatProvider) : FeeMoney + static TryParse(ReadOnlySpan, IFormatProvider, out FeeMoney) : Boolean + static TryParse(ReadOnlySpan, IFormatProvider, out FeeMoney) : Boolean + static TryParse(String, IFormatProvider, out FeeMoney) : Boolean + static op_CheckedSubtraction(FeeMoney, FeeMoney) : FeeMoney + static op_Equality(FeeMoney, FeeMoney) : Boolean + static op_GreaterThan(FeeMoney, FeeMoney) : Boolean + static op_GreaterThanOrEqual(FeeMoney, FeeMoney) : Boolean + static op_Inequality(FeeMoney, FeeMoney) : Boolean + static op_LessThan(FeeMoney, FeeMoney) : Boolean + static op_LessThanOrEqual(FeeMoney, FeeMoney) : Boolean + static op_Subtraction(FeeMoney, FeeMoney) : FeeMoney + extension static FeeMoneyExtensions.GetValue(FeeMoney) : Decimal + extension static FeeMoneyExtensions.ValueOrNull(FeeMoney?) : Decimal? HalfAmount CompareTo(HalfAmount) : Int32 CompareTo(Object) : Int32 diff --git a/test/oracles/axes.md b/test/oracles/axes.md index c1beeda6..c353a5f5 100644 --- a/test/oracles/axes.md +++ b/test/oracles/axes.md @@ -43,7 +43,7 @@ footprint, it runs with the suite. | EF | the query | `QueryShapes` | `QueryTranslationVariantTests` | | | Analyzers | where a type argument is written, and what it is | `TypeArgumentSites`, `TypeArgumentKinds` (`test/CodoMetis.TypeKit.Analyzers.Tests/Variants/`) | `TypeArgumentConstraintVariantTests` | | | Generation | the declaration's shape | record struct, record class, struct, class; sealed or not; over each wrapped kind | `BuildOutcomeTests`, `GeneratedSurfaceTests` | | -| Generation | the operator interfaces a plain value object declares | each arithmetic interface, the other operand and the result as the value object or another type, declared directly or through another interface, written by hand (public, explicit, a base record's explicit) or not; over every numeric wrapped type and both record shapes | `ArithmeticParityTests`, `ArithmeticTests`, `ArithmeticBuildTests` | an operator interface beside a hand-written `CompareTo` | +| Generation | the operator interfaces a plain value object declares | each arithmetic interface, the other operand and the result as the value object or another type, declared directly or through another interface, written by hand (public, explicit, a base record's explicit, an interface's explicit) or not; over every numeric wrapped type and both record shapes | `ArithmeticParityTests`, `ArithmeticTests`, `ArithmeticBuildTests` | an operator interface beside a hand-written `CompareTo` | An empty last column means nobody has looked yet, not that nothing is missing. Fill it when a feature lands or a finding names a combination no test varied. diff --git a/test/oracles/claims.json b/test/oracles/claims.json index 22158fe5..e85e7b23 100644 --- a/test/oracles/claims.json +++ b/test/oracles/claims.json @@ -1677,8 +1677,8 @@ { "id": "C28-own-operator-kept", "source": "docs/value-objects.md#add-subtract-and-scale", - "quote": "Write one yourself, or implement the interface explicitly, and TypeKit keeps it, with its checked form.", - "claim": "Write one yourself, or implement the interface explicitly, and TypeKit keeps it, with its checked form.", + "quote": "Write one yourself, or implement the interface explicitly, in the type, a base record or an interface of yours, and TypeKit keeps it, with its checked form.", + "claim": "Write one yourself, or implement the interface explicitly, in the type, a base record or an interface of yours, and TypeKit keeps it, with its checked form.", "oracle": "claim", "surfaces": [ "IPlainValueObject.*" diff --git a/tools/BuildVerdict/build-verdict-remedies.tsv b/tools/BuildVerdict/build-verdict-remedies.tsv index 600a2da5..d7027433 100644 --- a/tools/BuildVerdict/build-verdict-remedies.tsv +++ b/tools/BuildVerdict/build-verdict-remedies.tsv @@ -39,4 +39,5 @@ exit:test:5 MTP refused the command line and ran nothing An option the test host exit:test:8 The filter matched no test --filter-method and --filter-class match the fully-qualified name: use Namespace.Class.Method or a wildcard (*ProbeTests). A bare method name is a valid filter that matches nothing. exit:test:filter-mix xunit.v3 refused a mix of filter kinds and the host crashed Use one kind per run: the simple filters (--filter-class, --filter-method, ...), --filter-query, or VSTest's --filter. Mixing them throws before any test runs (measured: exit 134). CA1416 A call to an API a platform lacks, reachable on that platform Guard the call with the platform check the analyzer reads (if (!OperatingSystem.IsWindows()) ...) or mark the member [SupportedOSPlatform]; a test skip such as Assert.SkipWhen is not read as one. +CA2260 A generic-math interface whose TSelf is not the type that implements it Pass the implementing type as TSelf (IAdditionOperators on Money, or TSelf through a generic interface). An interface that implements one value object's operator on purpose suppresses CA2260 around its declaration. ASP0020 A minimal-API parameter of a value object declared in the same project looks unparsable ASP.NET's analyzer reads the value object before the generators add IParsable; the running application binds it through the generated TryParse. Suppress it at the endpoint with #pragma warning disable ASP0020, as the AspNetCore README's "A route parameter declared in the same project" shows. From fce86253fdd3bf4d157b56ba133266dce9e5ab89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Gro=C3=9F?= Date: Fri, 30 Oct 2026 17:23:08 +0100 Subject: [PATCH 3/3] docs: renumber the arithmetic decision record from 0121 to 0122 2.3.1 takes 0121 for #191's record (epic #198), since 2.3.1 stops at 0120 and DesignRecordTests wants the numbers without a gap. The arithmetic record becomes 0122: its file and heading, its index row, design/aspects.md's link and the aspect's doc comment. Until 2.3.1's 0121 reaches release/2.4.0, The_records_are_numbered_without_a_gap fails on this branch, the only conventions test that does. Refs #184 Co-Authored-By: Claude Opus 5.5 --- ...ic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md} | 2 +- design/adr/README.md | 2 +- design/aspects.md | 2 +- src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename design/adr/{0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md => 0122-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md} (98%) diff --git a/design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md b/design/adr/0122-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md similarity index 98% rename from design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md rename to design/adr/0122-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md index 8cbb33ad..edcf6870 100644 --- a/design/adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md +++ b/design/adr/0122-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md @@ -1,4 +1,4 @@ -# 0121. Arithmetic operators are opt-in by their interfaces, and a hand-written one is a seam +# 0122. Arithmetic operators are opt-in by their interfaces, and a hand-written one is a seam Date: 2026-10-30 Status: accepted diff --git a/design/adr/README.md b/design/adr/README.md index 64063bdd..b68b1ea7 100644 --- a/design/adr/README.md +++ b/design/adr/README.md @@ -127,4 +127,4 @@ per area ([design](../plan.md)) link to the records they rest on. | [0118](0118-an-error-may-learn-a-form-only-new-api-can-write.md) | An Error may report, in a minor version, a form that only an API new in that version can write, since no build that worked before contains it: CMTK0003's dropped assertion of `CodoMetis.TypeKit.Testing` on a `Task` or `ValueTask` in 2.3.0, its `ConfigureAwait`, `WaitAsync` and branch forms (#152) included, known by the package's assembly; the forms freeze at the tag; refines decision 34; one exception the maintainer took into 2.3.0, a dropped `Task.WhenAny` of tasks of results (#159) | 2026-10-29 | accepted; widened by [0119](0119-a-gap-found-before-the-tag-ships-in-that-release.md) | | [0119](0119-a-gap-found-before-the-tag-ships-in-that-release.md) | A form an Error misses, found before its version is tagged, is reported in that version when it loses what the rule protects in every build, though code built on the previous minor can contain it: 2.3.0's CMTK0003 on a `Task.WhenAny` of results (#159), `Wait()` on a branch (#160), an array in `Task.WaitAll`/`WaitAny` (#161), a dropped `Task.WhenEach` (#162), and a `WhenAny` of held tasks silent; the rule stops at the tag, after which decision 34 holds; refines 34 and 118 | 2026-10-29 | accepted | | [0120](0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md) | A `net11.0` target joins a 2.x minor when a feature needs it, and a dependency floor may rise in a minor, on every target or on `net11.0` alone, if it is documented (the Semantic Versioning FAQ calls a dependency update with an unchanged public API compatible; a project that references the dependency lower gets NU1605, measured on SDK 11 RC1, so the changelog names the floor, who it reaches and both ways out, and the smoke test shows it); a union new on `net11.0` is an addition and may join a 2.x minor (a `LangVersion` 14 consumer who matches on it gets CS9327, and on Metalama 2026.1 CS8652, measured); Metalama is one version for every target and TypeKit follows the maintained Metalama year in a minor: 2.3 and 2.4 stay on 2026.1 and 2.5.0 moves both targets to 2027.0 once stable (a direct 2026.1 reference gets NU1605 and stays on 2.4.x; a woven `net11.0` project goes from C# 14 to 15; conditions: a license-free CI and clean-container build, the ceiling and the 2.4.x line documented); the SDK 11 spike comes first (#170); 3.0.0 stays the `net10.0` drop and a change of an existing contract, timed to .NET 12 LTS, and .NET 11 is no reason for a major; refines decision 44 | 2026-10-29 | accepted | -| [0121](0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md) | A plain value object gets the arithmetic operators (`+`, `-`, `*`, `/`, `%`, unary `-` and `+`, and their `checked` forms) of the operator interfaces it declares, where the wrapped type implements the same interface with the value object replaced by itself; each calls the wrapped type's through `GeneratedArithmetic`, a constrained generic call with its context stated; never by default and never on a validated value object, which keeps CS0535; a hand-written operator for the same operands, or an explicit implementation of the interface, its own or a base record's, is a seam, kept with its `checked` form, so 2.3 code builds unchanged; refines decision 28 | 2026-10-30 | accepted | +| [0122](0122-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md) | A plain value object gets the arithmetic operators (`+`, `-`, `*`, `/`, `%`, unary `-` and `+`, and their `checked` forms) of the operator interfaces it declares, where the wrapped type implements the same interface with the value object replaced by itself; each calls the wrapped type's through `GeneratedArithmetic`, a constrained generic call with its context stated; never by default and never on a validated value object, which keeps CS0535; a hand-written operator for the same operands, or an explicit implementation of the interface, its own or a base record's, is a seam, kept with its `checked` form, so 2.3 code builds unchanged; refines decision 28 | 2026-10-30 | accepted | diff --git a/design/aspects.md b/design/aspects.md index 81691214..4d5b09e5 100644 --- a/design/aspects.md +++ b/design/aspects.md @@ -11,7 +11,7 @@ `IValueObjectMaterializer.Materialize`, by contract. `MinValue`/`MaxValue` are only generated for a plain value object, and so are arithmetic operators, which wrap their result through the private constructor as `From` does. -- **Arithmetic is opt-in (decided 2026-10-30, [decision 121](adr/0121-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md)).** +- **Arithmetic is opt-in (decided 2026-10-30, [decision 122](adr/0122-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md)).** `ValueObjectArithmeticAspect` generates the operators of the operator interfaces a plain value object declares, where the wrapped type implements the same interface with the value object replaced by itself, each calling the wrapped type's through `GeneratedArithmetic`, a constrained generic call with its context diff --git a/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs b/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs index 1e7a9d39..0f295146 100644 --- a/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs +++ b/src/CodoMetis.TypeKit.Generators/ValueObjectArithmeticAspect.cs @@ -84,7 +84,7 @@ internal sealed class ArithmeticOperatorArguments /// /// An operator the value object already has is kept, with its checked form: one it declares for the /// same operands, or an explicit implementation of the interface, its own, a base record's or an interface's. Code that -/// implemented these interfaces by hand before compiles unchanged, so this is no refusal (decision 121). +/// implemented these interfaces by hand before compiles unchanged, so this is no refusal (decision 122). /// /// internal sealed partial class ValueObjectArithmeticAspect : TypeAspect