Repository navigation
EF Core: translate an arithmetic operator whose other operand or result is not the value object #186
Copy link
Copy link
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/efcoreAffects CodoMetis.TypeKit.EntityFrameworkCoreAffects CodoMetis.TypeKit.EntityFrameworkCore
Milestone
Description
Problem
The arithmetic operators a plain value object declares (#184) translate in an EF Core query only between two value objects: o.Price + o.Tax > limit runs in the database as ef_add("p"."Price", "p"."Tax") on SQLite, as two decimal columns would. An operator with another operand or result type throws instead, though it works in memory:
db.Orders.Where(o => o.Price * 2m > Money.From(10m)).ToList(); // InvalidCastException: Invalid cast from 'System.Decimal' to 'Money'
db.Orders.Select(o => o.Tax / o.Price).ToList(); // InvalidOperationException: No coercion operator is defined between types 'Money' and 'Nullable<Decimal>'EF infers the binary expression's type mapping from the column and applies the value object's converter to the decimal side. o.Price.Value * 2m translates. Measured on SQLite with EF Core 10 (ArithmeticQueryTests, which pins both throws).
Change
- Translate an operator whose other operand or result is not the value object as the wrapped type's arithmetic over the column, in the EF Core package, on SQLite, Npgsql and SQL Server.
docs/value-objects.md("Add, subtract and scale") then drops its.Valueadvice for queries, andArithmeticQueryTestsasserts the rows instead of the throws.
Found while doing it (#190)
.Valueof the database's arithmetic was cast to the column's store type, which rounds on SQL Server:(o.Price / 32m).Valueover 5 read 0.16 where memory reads 0.15625 at the defaultdecimal(18,2)(measured on Azure SQL Edge). Re-typed now, in both directions.- Between two value objects, EF's own translation bypassed decision 56: over a column the application stores with its own converter (
"M:" + value),Price + Tax > 4became"p"."Price" + "p"."Tax" > 'M:4'and kept no row where memory keeps two. Every generated operator is rewritten now, so it is EF's "could not be translated" there. byte,short,charhave no arithmetic in an expression tree; their operators are refused naming the value object.- Composed, the database's arithmetic was still cast (found in review): a
CASE(?:) or aCOALESCE(??) of it was cast whole to the column's store type, so(o.Optional / 32m ?? o.Price / 32m).Valueread[0.09, 0.31, 0.05]where memory reads[0.09375, 0.3125, 0.046875]on SQL Server, a filter on it lost its row, and arithmetic over a?:rounded the same. ACASE, aCOALESCE,MAXandMIN, and a subquery of one value (a navigation's largest quotient, found across the input class) are re-typed branch by branch now. - An optional operand's null threw in memory (found in review): where EF computes the last
Selectin memory, over a column the application's own converter stores,Select(r => r.Amount * 2m)threw "Nullable object must have a value" on a null row. The rewrite checks each optional operand now, as the lifted operator does: oneCASE WHEN … IS NOT NULL THEN … ENDaround the arithmetic, which reads the column once.
Done when
Price * 2m,Price / 2m,Price % 2mandTax / Price(adecimal) in a filter and a projection answer as memory does on the three providers.
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/efcoreAffects CodoMetis.TypeKit.EntityFrameworkCoreAffects CodoMetis.TypeKit.EntityFrameworkCore
Activity
CaffeinatedCoder commentedon Oct 30, 2026
Implemented in #190, stacked on #189 (#187) and #188: every generated operator is rewritten to the wrapped type's arithmetic over
ProviderValue, its value-object resultMaterialized and re-typed. 32 queries answer as memory on SQLite, PostgreSQL 17 and SQL Server, and a precompiled query runs under Native AOT. Decision 124.CaffeinatedCoder commentedon Oct 31, 2026
Merged into release/2.4.0 by #190 (251f016).