Repository navigation
Per-domain range set types (Int32RangeSet, DateRangeSet, …) replace RangeSet<TRange, T> #275
Description
Problem
RangeSet<TRange, T> makes every consumer spell both type arguments — RangeSet<DateRange, DateOnly> — although T is fully determined by TRange. C# does not infer type arguments from constraints, so the element type has to be named at every declaration: entity properties, DTOs, method signatures. The From remarks and docs/ranges.md already tell consumers to prefer the collection expression because of it.
The quirk is not local to one declaration. Everything around the set exists because an open generic cannot name a closed type:
RangeSetnamesRangeJsonConverterFactoryand registers closed converters throughGenericConverters.Register, because an attribute cannot name a converter closed over the set's own type (AGENTS.md, "AOT").- The EF translators match members by
typeof(RangeSet<,>)andnameof(RangeSet<,>.Union)inValueRangesMethodCallTranslator,ValueRangesQueryExpressionInterceptorandValueRangesAggregateMethodCallTranslator, instead of a registry lookup by CLR type as the ranges do. RangeAgg/RangeIntersectAggover ranges are thirteen per-type overloads for the same reason (docs/architecture.md, "Aggregates").YearMonthRangeSetTypeMappingis a hand-written special case beside the genericValueRangeSetTypeMapping<TRange, T>.
Change
One sealed set type per domain, built as the value-set family is (StringSet, GuidSet, …):
- Core:
Int32RangeSet,Int64RangeSet,DecimalRangeSet,DateRangeSet,DateTimeRangeSet,DateTimeOffsetRangeSet,TimeRangeSet,Float64RangeSet,Float32RangeSet. NodaTime satellite:LocalDateRangeSet,LocalDateTimeRangeSet,InstantRangeSet,YearMonthRangeSet. TheRangeSetsuffix keeps the vocabulary the docs, the claim catalogue andarea/range-setuse;Int32Set(values) againstInt32RangeSet(ranges) reads correctly. - An
IRangeSet<TRange, T>interface and a static-abstractIRangeSetFactory<TSet, TRange, T>, mirroringIValueSet<T>/IValueSetFactory<TSet, T>. The current body ofRangeSet.csmoves into an internal engine (Internals/) the thirteen types delegate to, so the algebra stays in one place and a fix to set semantics is one change. - Generic producers (
Union,Except,Gaps,ToRangeSet, the set aggregates,RangeTypeDefinition<TRange, T>) learn the set type from the range side: a third type parameter onIRangeFactory<TRange, T>or a companion interface withstatic abstract TSet CreateSet(ReadOnlySpan<TRange>). The infrastructure generics then carry three parameters a consumer never writes. Per-type overloads (theRangeAggprecedent) are the fallback, not the plan. - Each set type names its own closed
[JsonConverter]and[TypeConverter]; theGenericConvertersregistration for sets and the factory indirection go. The EF side looks sets up throughRangeTypeRegistryby CLR type (RangeSetClrTypealready exists) and drops thetypeof(RangeSet<,>)matches.YearMonthRangeSetstops being the odd one out. RangeSet<TRange, T>and the non-genericRangeSetbuilder are removed.Int32RangeSet set = [a, b];keeps working through[CollectionBuilder]on each type.- New clone-sweep family (
core range sets,nodatime range sets) inclone-waivers.json; claims inclaims.jsonre-pointed;RangeProbes,RangeSetLiftParityTestsand the EF discovery pick the types up by reflection. docs/migration.mdv10.x section gives the rename as a regex (RangeSet<(\w+)Range, \w+>→$1RangeSet), and the changelog's "what can break you" list carries it.
Why 11.0.0 and not 10.0.0
10.0.0 tags on the commit the gate (#179) passed on 2026-10-19; its changelog opens with "Nothing was removed or renamed", and additions were frozen on 2026-10-18. Epic #147 reserves surface changes for the 11 line, and since #149 (closed unions) turned out non-breaking, this is the one breaking surface change 11.0.0 carries. The cost is that the two lines differ in the set family for the LTS lifetime, so a set-semantics fix on main is a manual port into release/10.x; keeping the algebra in a shared engine keeps that port small. 10.x consumers can alias (using DateRangeSet = CodoMetis.ValueRanges.RangeSet<CodoMetis.ValueRanges.DateRange, System.DateOnly>;) in the meantime; docs/ranges.md should say so.
Footprint (measured 2026-10-19 on main)
| Area | Mentions of RangeSet< |
|---|---|
Shipping source outside RangeSet.cs |
about 90 across core, EF and both satellites |
| Test files | 87 files, 397 mentions |
claims.json |
180 lines mention RangeSet |
Verification
- Discovered tests and the clone sweep list what is missing for each new sibling (
docs/architecture.md#adding-a-range-or-set-type);dotnet run tools/oracles.cs clonesclean or waived. JsonDefaultContractTests,TypeConverterContractTests,OracleSurfaceCoverageTestsandOracleCatalogueTestspass with the thirteen types and noRangeSet<,>.- Every EF translation test asserts the same SQL as before;
ShapeMatrixParityTests,RangeSetLiftParityTests,TemporalStoreParityTestsand the aggregate integration tests pass on PostgreSQL 16 and 18. - The Native AOT consumer in
test/consumer-smoke-test.shdeclares and serializes at least one set type per package. - ApiCompat suppression file lists every removed member; each is in the changelog's "what can break you" list.
Sequence on the 11 line
After 11.0.0-preview.1 (#103, pure retarget) and preview.2 (#149, closed unions): this is the third step before 11.0.0.
Found-By: maintainer, API review of the 10.0.0 surface
Activity