Repository navigation
v6.3.0
6.3.0 — 2026-08-16
Three additions that close gaps in the existing surface rather than extending the model. No
breaking changes.
Added
-
RangeSet.IsInfinity()andRangeSet.IsFinite()— the two shape predicates a range had and
its multirange counterpart did not.IsFinite()is true for a non-empty set bounded at both
ends;IsInfinity()is true only for the set covering the whole domain.IsInfinity()is deliberately notIsUnboundedStart() && IsUnboundedEnd(). That equivalence
holds for a single range, because a range is contiguous, and fails for a set:
{(,5],[10,)}is unbounded at both ends and does not contain 7. The EF translation reflects
the same distinction — the range predicate maps tolower_inf(x) AND upper_inf(x), the set
predicate to equality against the infinite multirange literal, which is exact because
PostgreSQL canonicalizes multiranges the way the model does.IsFinite()maps to
NOT lower_inf AND NOT upper_inf AND NOT isemptyfor both. -
Collection expressions for
RangeSet<TRange, T>—RangeSet<Int32Range, int> set = [a, b];,
matching what the nineteen value set types and arities already supported. Elements are normalized exactly as
Fromnormalizes them: empties dropped, sorted by lower bound, overlapping and adjacent
neighbours merged, any infinity collapsing the set. AFrom(params ReadOnlySpan<TRange>)
overload comes with it, alongside the existingFrom(IEnumerable<TRange>).The builder is exposed as a non-generic
RangeSet.Create<TRange, T>, because a
[CollectionBuilder]target cannot be generic. Prefer the collection expression: C# does not
infer type arguments from constraints, so a direct call has to name bothTRangeandT, which
is longer thanRangeSet<TRange, T>.Fromalready was. -
ISpanParsable<T>on every parsable type — the eleven range types,RangeSet<TRange, T>, and
all nineteen value set types and arities, with publicParse/TryParseoverloads over
ReadOnlySpan<char>beside the existingstringones. The literal grammars were already parsed
over spans internally, so this exposes the parser that was always there and lets a caller parse
a slice of a larger buffer without allocating a substring first.IParsable<T>remains
satisfied —ISpanParsable<T>extends it.One consequence worth knowing if you write generic code over these types: where a type parameter
is constrained toIRangeFactory/IValueSetFactory, bothParseoverloads are now visible, and
astringargument binds to the span overload through the implicit conversion. Every type's two
overloads are the same call, so results do not change. -
Lengthon every range type — the measure of what it covers. A discrete domain counts its
values inclusive of both ends ([2024-01-01, 2024-01-31]measures 31 days,[1, 10]measures
10 integers), a continuous one measures the span between the bounds. The empty range measures
zero and every unbounded shape measuresnull: the two are different answers and stay
distinguishable. The type follows the domain —long?forInt32Range/Int64Range,int?
days forDateRange,TimeSpan?for the timestamp ranges,decimal?forDecimalRange, and
Duration?/Period?for the NodaTime ranges, which distinguish elapsed time from a calendar
quantity. Client-side only; it does not translate to SQL. -
Values()on the discrete range types — enumerates the contained values ascending,
inclusive of both bounds. Declared only byInt32Range,Int64Range,DateRange,
LocalDateRangeandYearMonthRange, so asking a continuous range for its values is a compile
error rather than a runtime failure. An unbounded range throws immediately rather than at the
first iteration, so the failure points at the call rather than at theforeach. -
A bridge between the value sets and the range sets over the same discrete domain:
Int32Set/Int64Set/DateSet(plusLocalDateSet/YearMonthSetin the NodaTime satellite)
gainToRangeSet(), which collapses runs of consecutive values —{1,2,3,7}becomes
{[1,3],[7,7]}— and the matchingToInt32Set()/ToDateSet()/… expand back. The two shapes
describe the same membership; which to store is a question of density, and a thousand
consecutive dates are better served by onedaterangethan by a thousand-element array. Both
directions are client-side: PostgreSQL converts between arrays and multiranges only through
unnestand a custom aggregate. -
Clamp(value)on every range — the contained value nearest the argument, ornullfor the
empty range. An unbounded side never constrains. -
An indexer on the value set types,
set[0], matching whatRangeSetalready offered. -
IRangeFactory<TRange, T>.IsDiscrete— a defaulted virtual static reporting whether the
domain has a step, for generic code that cannot see which concrete type it holds. It cannot be
derived fromNextValueAfter, which returnsnullboth for a continuous domain and for the
last value of a discrete one; a convention test now holds the two to agreement.
Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.