Repository navigation
Closed
Labels
area/value-objectsValue-object contracts and what the aspects generate for themValue-object contracts and what the aspects generate for themenhancementNew feature or requestNew feature or requestpackage/generatorsAffects CodoMetis.TypeKit.GeneratorsAffects CodoMetis.TypeKit.Generators
Milestone
Description
Problem
A plain value object over a number can't be added, subtracted or scaled without unwrapping it: Money.From(a.Value + b.Value). You can declare IAdditionOperators<Money, Money, Money> and write the operator yourself, but that is boilerplate the generator already writes for its neighbours: it generates IEqualityOperators, IComparisonOperators and IMinMaxValue<TSelf> so a value object satisfies generic-math constraints. Other value-object libraries generate arithmetic (EricksonLopez.DomainPrimitives on every numeric primitive, KB-055).
Change
- Opt in by declaring the interface, not by an attribute and never by default: the generator implements the operators of the generic-math operator interfaces a plain
IValue<T>declares, when the wrappedTimplements the matching interface itself:Candidates:public readonly partial record struct Money : IValue<decimal>, IAdditionOperators<Money, Money, Money>, ISubtractionOperators<Money, Money, Money>;
IAdditionOperators,ISubtractionOperators,IUnaryNegationOperators,IUnaryPlusOperators, andIMultiplyOperators/IDivisionOperatorswith the wrapped type as the right operand (Money * 3m). Which ones, and whetherTSelf * TSelfis allowed, is decided in the PR. The checked operators follow the wrapped type's. - Plain value objects only. A validated value object gets none: an operator can't return a
Result, and a sum outsideCreate's range would have to throw. Declared on one, the interface stays unimplemented and the build fails with CS0535, as today; the docs say so. - A hand-written operator is kept, and only the missing ones are generated. Code that implements these interfaces by hand on 2.3 must compile unchanged on 2.4, so this is not a CMTK1011 refusal (a minor may not report more under an Error, decision 34).
- The Generators README's table of generated members,
docs/value-objects.md("Numbers") and the changelog name it; a claim intest/oracles/claims.jsonholds it.
Done when
- A plain value object that declares an operator interface gets the operators, with the wrapped type's results (overflow and checked context included), tested over the numeric shapes of the generated-type axes.
- A hand-written operator beside the declaration is kept, shown by a test.
- The Native AOT smoke consumer uses one operator.
Decided (in the PR, decision 122)
- Interfaces:
IAdditionOperators,ISubtractionOperators,IMultiplyOperators,IDivisionOperators,IModulusOperators,IUnaryNegationOperators,IUnaryPlusOperators, each with itscheckedform where the interface has one. Left out:++/--, the bitwise and shift operators. - Operands: the first type argument is the value object; the other operand and the result can each be the value object or any other type, as long as the wrapped type implements the same interface with the value object replaced by itself. So
Money * 3m,Money / Moneyas adecimalratio, andTSelf * TSelfwhen declared. - How: each operator calls the wrapped type's through
GeneratedArithmetic(base package), a constrained generic call, sobytestaysbyteandchar's explicit operators are reached. - Measured on the way:
DateTime,TimeSpan,DateOnlyandGuidimplement none of the operator interfaces, so a value object over them gets none (CS0535). A lone hand-writtencheckedoperator keeps failing (CS9025). In EF Core an operator between two value objects translates; one with another operand throws (EF Core: translate an arithmetic operator whose other operand or result is not the value object #186), and a hand-written one would run as the database's arithmetic (EF Core: a hand-written arithmetic operator in a query runs as the database's arithmetic #187).
Found in review (#188)
- An interface's explicit implementation was replaced. An interface that implements
+explicitly, such asIAddsLeft<TSelf> : IAdditionOperators<TSelf, TSelf, TSelf>, is what every type declaring it reaches by default. The preservation check read the value object and its base records only, so a public+was generated beside it: generic addition read 3 where the interface's answered 1. Kept now, with its checked form, through a generic interface, one further down, a base record's interface and a single value object's (ArithmeticTests.An_interfaces_explicit_implementation_is_kept). The same gap in the members generated since before 2.4,IMinMaxValueandIFormattablemeasured, is An interface's explicit implementation of a generated interface member is replaced without a diagnostic #191.
Metadata
Metadata
Assignees
Labels
area/value-objectsValue-object contracts and what the aspects generate for themValue-object contracts and what the aspects generate for themenhancementNew feature or requestNew feature or requestpackage/generatorsAffects CodoMetis.TypeKit.GeneratorsAffects CodoMetis.TypeKit.Generators
Activity
docs: renumber the arithmetic decision record from 0121 to 0122
CaffeinatedCoder commentedon Oct 31, 2026
Merged into release/2.4.0 by #188 (ded5fa6).