Repository navigation
Opt-in strict finite factory: refuse start > end as written, translated to the bare PostgreSQL constructor #429
Description
Summary
CreateFinite returns the empty range when start > end. That's right for computed bounds: "from the later start to the earlier end" should be empty when the inputs don't overlap. But it's silent for swapped arguments or a bad row (To < From), and the empty range then flows into Contains and Overlaps as a plausible false. The literal parser has refused inverted bounds since 10.0.0 (#114), as PostgreSQL's range_in does. The factory has no opt-in way to do the same.
Add an opt-in strict finite factory that refuses start > end, and leave CreateFinite as it is.
Proposal
-
The rule is the parser's (
RangeFormat.Finite): compare the bounds as written, before a discrete type closes them. Onlystart > endis refused.(1,2)over integers and[x,x)stay a legitimateEmpty. That keeps "inverted" (probably a bug) apart from "empty once the bounds are closed" (a correct answer). -
Shape of the API, still to decide:
- a throwing factory (name open, e.g.
CreateFiniteStrictorCreateFiniteOrThrow) that throwsArgumentExceptionnaming both bounds; and/or TryCreateFinite(start, end, startInclusive, endInclusive, out TRange range), returningfalseonly for inverted bounds.
Only the throwing one can be translated: an
outparameter can't appear in an expression tree, soTryCreateFinitewould be client-only. - a throwing factory (name open, e.g.
-
EF translation: the throwing factory becomes PostgreSQL's bare range constructor,
daterange(lower, upper, '[]'), without theCASE WHEN lower <= upperguard thatGuardedFiniteRangeConstructoradds forCreateFinite. PostgreSQL then raisesrange lower bound must be less than or equal to range upper bound, so memory and the server both refuse. Rule (c) still applies: NULL over a NULL element (NullOverNullElement). -
Every range type gets it: the eight core types and the four NodaTime ones.
YearMonthRange(a month-aligneddaterange) and the custom store types (timerange,float8range,float4range) need their constructor behaviour measured, not assumed.
Open questions
- Does it go on
IRangeFactory<TRange,T>?CreateFiniteisabstract staticthere. A newabstract staticmember breaks anyone implementing the interface outside the library. Astatic virtualmember with a default implementation built onCreateFiniteplus the comparison doesn't, and gives generic code the strict path too. - Which exception:
ArgumentException(it's an argument check), orArgumentOutOfRangeExceptiononend? - The float types already throw
ArgumentExceptionforNaNor an infinity as a bound. The strict factory keeps that check.
Done when
- Each range type has the strict factory, and a test reverting the check shows the test fails (AGENTS.md, workflow step 4).
- A live integration test on PostgreSQL 16 and 18 shows the translated factory raising the error for inverted column values, matching memory, and building the same range as
CreateFiniteotherwise. Covers every element type, NULL elements and the domain extremes. - The new public members have a claim in
test/oracles/claims.json(OracleSurfaceCoverageTests). docs/ranges.mdanddocs/efcore.mdsay when to use which factory, with an example, and the changelogs have their rows.- Lands on
mainfirst, then cherry-picked with-xintorelease/10.xas a 10.x minor (it's an addition, notbreaking).
Not in scope: changing CreateFinite's own answer. That would turn a value into an exception (breaking) and penalise the computed-bounds idiom it exists for.
Activity
CaffeinatedCoder commentedon Oct 25, 2026
Decision (2026-10-25): no
Option/Resultin the coreCreateFinitekeeps its return type (TRange).Empty, which is a real value, so there's no failure to report.Option<TRange>would give two ways of saying "nothing":NoneandSome(Empty).CreateFinitetranslates to PostgreSQL's range constructor inside queries (DateRange.CreateFinite(b.From, b.To).Contains(day), docs/efcore.md). A call returningResultneedsMatchorTryGetValueto get the range back, and EF can't translate that.IRangeFactory<TRange,T>.CreateFiniteisabstract staticand returnsTRange. The engines, the literal parser and the EF registry all build ranges through it.No reference to CodoMetis.TypeKit from the core packages, on 10.x or 11.x.
PrivateAssets="none"), so every ValueRanges consumer would get CMTK rules in their build without choosing TypeKit.Option/Resultin ValueRanges' public API, every TypeKit major would become a ValueRanges major. TypeKit versions by API and plans 3.0.0 (C# 15 unions, droppingnet10.0) around .NET 12, while ValueRanges' lines follow .NET.Option/Result, so .NET 11 doesn't change this.A
Result-returning strict factory, if anyone wants one, goes in an optional satellite (CodoMetis.ValueRanges.TypeKit, like the NodaTime one). C# 14 static extension members let it add the factory to the range types without the core knowing about it:It applies the rule this issue defines for the strict factory and is client-only, like
TryCreateFinite. Build it only once a consumer asks for it. Out of scope for this issue.What this issue still covers: the throwing variant, which translates to the bare constructor, and possibly
TryCreateFinite. Both are BCL-only.