Changelog
All notable changes to CodoMetis.ValueRanges and its packages.
The four packages share one version number (the Version property in Directory.Build.props) and
are released together, so a version appears here even when a package saw no change in it. Each
package also has its own CHANGELOG.md with the entries that affect it. Versions follow
Semantic Versioning. Entries are newest first.
A version's section is the GitHub release's notes. Each line is one change: what happens now, what to do about it, and a link to the guide with the detail. CONTRIBUTING.md has the rules.
[10.5.0]
[10.4.0] — 2026-10-26
Some calls answer differently: Changed answers in 10.4.0.
Changed answers
- EF Core: a
StringSetorStringSet<TElement>property with a collation that may not compare bytes throws when the model is built, and so doesEF.Functions.Collateover one in a query. Under a case-insensitive collation,Contains("a")matched{A}andRemove("a")deletedA, unlike in memory (#496, Migration). - EF Core: on a
DateRange, aLocalDateRangeand their multiranges,LowerBound(),UpperBound(),LowerBoundInclusive()andClampread an exclusiveinfinityor-infinityin a query as the row loads. On[2026-01-01,infinity),UpperBound()returned 9999-12-31 where memory gives 9999-12-30 (#502, Migration). - EF Core: on a
YearMonthRangeand its multirange,UpperBound()andClampcompare with a month in a query as in memory. They read the last day of the month, so on[2026-01,2026-01]UpperBound() == 2026-01wasfalseandUpperBound() > 2026-01wastrue(#511, Migration). - EF Core: a query that compares a
StringSetorStringSet<TElement>with a value whose column has a collation that may not compare bytes, or withEF.Functions.Collate(value, …)under one, throws, because the database compares under it:Contains(g.Owner)matched{A}for"a"on a case-insensitiveOwner, andRemove(g.Owner)inExecuteUpdatedeletedA(#512, Migration).
Added
- EF Core:
Clampruns as SQL on every range type, no longer only onInt32Range,Int64RangeandDecimalRange, and answers as memory does, at the extremes too. ADateTimestored astimestamp with time zoneagainst aDateTimeRangeruns in memory, because PostgreSQL would compare them through the session's time zone (What runs where). - EF Core:
Union,Add,IntersectandExcepton value sets run as SQL for every set type exceptStringSetandStringSet<TElement>. They give the deduplicated, sorted set memory gives, so a query can compare, count, group by and store the result (What runs where). - EF Core: on
StringSetandStringSet<TElement>,Intersect,ExceptandAddrun as SQL, left unsorted, where order and repeats don't change the answer.Countover them and overUnioncounts the distinct elements, and==and!=compare by mutual containment. Ordering, grouping and storing one still throw (Combine sets with Union). - EF Core:
Counton aRangeSetruns as SQL, so a query can filter and sort by how many separate ranges a multirange holds. It counts the ranges memory holds, and anullmultirange givesnull. Near the element type's extremes, a multirange the database computes can count one range more (What runs where).
Changed
- Docs: a value set over your own element type, other than
StringSet<TElement>, needs aCompareTothat orders values as the underlying type does, because the database sorts the underlying values (Value sets).
[10.3.0] — 2026-10-25
Some calls answer differently: Changed answers in 10.3.0.
Changed answers
- EF Core: a value set
Unionwith a NULL set, orRemovewith a NULL set or element, is NULL in a query, asWhere(… == null)already said. ASelectloaded the other set instead, for a nullable column, a missing outer-join child or anullparameter (#395, Migration). - EF Core: any
Compare(x, y)over ranges or sets,RangeLowerBoundComparer.Compareand every otherIComparerincluded, runs in memory only in a query, as the method called. A filter or an ordering fails translation. In SQL it compared in the range order, soCompare(r.Period, p) == 0missed rows with the same lower bound (#396, #452, #464, Migration).
Added
- EF Core:
LengthonInt32RangeandInt64Rangeruns as SQL, so a query can filter and sort by it. It counts the integers as memory does: 0 for the empty range, andnullfor an unbounded side or, onInt64Range, a count larger thanlong.MaxValue(What runs where). - EF Core:
ClamponInt32Range,Int64RangeandDecimalRangeruns as SQL, so a query can filter and sort by the clamped value. It answers as memory does: the value inside the range, otherwise the bound it passes, andnullfor the empty range or anullrange or value (What runs where). - EF Core:
DateRange.Month,Quarter,YearandIsoWeekrun as SQL when they read a column, soDateRange.Month(b.Day)works in aWhereand anOrderBy. They build the range memory builds, the extremes included, and an argument memory rejects fails in PostgreSQL too. The NodaTime forms stay in memory (What runs where). - Docs: the guides, the package READMEs, the changelog and an API reference are published as a documentation site with one set of pages per line. It opens on the stable 10.x line, and the 11.x previews are under
/11.x/. - Docs: a walkthrough follows one booking through a minimal API and PostgreSQL, from the request to the stored row and back, and says what changes at each step. CI runs its sample against the packages, so the responses and SQL on the page stay true.
Changed
- Docs: each package's Project website link on NuGet opens the documentation site, and its README links the site and the walkthrough.
[10.2.0] — 2026-10-25
Some calls answer differently: Changed answers in 10.2.0.
Changed answers
- Ranges:
RangeAgg()andRangeIntersectAgg()skip anullrange or set, asrange_aggandrange_intersect_aggskip a NULL row. With anullafter another range,RangeIntersectAgg()returnedempty, or threw after(,), andRangeAgg()threw unless an input was(,)(#363, Migration). - Ranges:
RangeSet.From, a collection expression andRangeSet.CreatethrowArgumentExceptionfor anullrange, where they built a set holdingnull, whoseIsEmpty()wasfalse.UnionandExceptcalled on anullrange throw too (#357, Migration). - Ranges: a predicate called on a
nullrange, or given anullrange or set, returnsfalse, as a query does over a NULL column.Contains(Empty),IsContainedBy((,))and(,).Contains(null)returnedtrue, andContains(T)and anullset operand threw (#357, Migration). - EF Core:
LowerBound()andUpperBound()in a query areNULLfor an empty range or set and on an unbounded side, as in memory. So== nullselects those rows, where it selected none on a required column, and?? fallbackreturns the fallback, where the row was missing (#354, Migration). - EF Core: a range, multirange or value set predicate over a NULL column is
false, so its negation keeps the row, as EF Core does forstringandstring[].!b.Period.Overlaps(p)dropped the NULL rows, and a projected predicate threw (#357, Migration). - EF Core:
Contains(T)on a range or multirange isfalsein a query when the element is NULL: a column of a missing child in an outer join, or a nullable column read with.Value. Its negation dropped the row, and a projection threw (#387, Migration). - EF Core:
CreateFinite,CreateUnboundedStartandCreateUnboundedEndover a NULL element are NULL in a query, as EF Core's ownAddDaysis. They builtemptyor(,), soContains(v)selected a row without the element (#388, Migration).
Fixed
- Docs: What runs where, the READMEs and the XML docs give each in-memory operation's real reason.
ShiftandExpandkeep a bound atinfinity, so that reason was stale.Length,Values,Split, the bridge conversions,Clamp,IntersectandExceptwould take composed SQL, not a missing operator. In-memory operations run in the finalSelectonly. - Docs: the What runs where tables list every public member a query can call.
Equalsruns as=.Valueson a value set,Countand the indexer on aRangeSet,From,Parse, the NodaTime conversions,ToString,GetHashCodeandGetEnumeratorrun in memory. - Docs: two queries in 10.2.0 still return something other than the same LINQ over the loaded rows, without an error: a value set
UnionorRemoveover NULL in a projection, andRangeLowerBoundComparerin a query. Each has a workaround, and its fix comes in a later 10.x minor (#397, EF Core). - Docs: an ordering by
LowerBound()orUpperBound()puts the empty rows and those without that bound where PostgreSQL puts NULLs, last ascending and first descending, also on a required column. A!= nullkey in front sorts them as memory does (EF Core). - EF Core: on EF Core 10.0.2 and later, an empty captured list of ranges or sets keeps EF Core's ordering column (
_ord, behindFirstOrDefault) an integer, as with elements. It was cast to the range or set type. No answer changed, but comparing it with an integer would have failed for the empty list (Captured lists).
[10.1.0] — 2026-10-19
One call answers differently, on the .NET 11 runtime only: Changed answers in 10.1.0.
Changed answers
- Literals: a time,
DateTimeorDateTimeOffsetelement with an hour of 24 throwsFormatExceptionon the .NET 11 runtime too, where it returned midnight:{24:00:00}read as{00:00:00}andT24:00:00as the next day. On .NET 10 it always threw (#300, Migration).
Fixed
- Value sets:
ToStringon every value set, in the core and NodaTime packages, runs in anet11.0Native AOT consumer, where the process ended withSIGTRAPon the first call (Native AOT). - EF Core:
AnyorCountover an empty captured list of ranges or value sets returnsfalseor0on EF Core 10.0.2 and later, where it failed with42883: operator does not exist: text && int4range. The stopgapAppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue37216", true)also brings back EF Core's own failure on empty lists of other types (EF Core). - EF Core:
RangeSet.Containstranslates on the EF Core 10 patch that carries dotnet/efcore#37081, where every overload threwArgumentExceptioninside EF Core before translation. That patch, planned as 10.0.15, was not published when 10.1.0 shipped. Its stopgapAppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue37081", true)also gives up EF Core's own fix forIReadOnlySetparameters, which inline their values again (EF Core). - EF Core:
ExecuteUpdateof a value set inside a JSON column is refused naming the property ('Doc.Tags') on Npgsql EF 10.0.3 withSetPostgresVersion(16, 0)or higher, where it was refused naming the column ('Doc'), and the refusal no longer says that a value that may benullwritesNULLas the whole document (EF Core). - EF Core:
a == b ? null : aover a value setUnioncomputed on the server, which EF Core writes asNULLIF(a, b), is refused as value set equality, where it was refused naming no operation (EF Core).
[10.0.0] — 2026-10-19
Nothing was removed or renamed. Some calls answer differently, and text PostgreSQL refuses is refused here too: Migration from v9.x.
Breaking
- Ranges:
IsUnboundedStart()andIsUnboundedEnd()returntruefor the infinite range(,), as PostgreSQL andRangeSetdo, where they returnedfalsein memory. MatchIUnboundedStartRange<T>orIUnboundedEndRange<T>for the half-unbounded shape alone (Migration). - Ranges:
RangeSet.Overlapson a continuous domain returnstruewhen the operand ends where a later element starts and shares values with an earlier one, as&&does.{[1,4),[5,8)}.Overlaps([3,5))returnedfalse(Migration). - Ranges: a record derived from a range type throws
InvalidOperationExceptionwhen it is constructed, where it compiled andContainsandOverlapsansweredfalsefor it (Pattern matching). - Value sets: every core wrapper set checks that an element's text form reads back as the same element, and throws if it doesn't, where the element was stored as a different value (Value sets).
- Literals: integer or decimal text with a NUL throws
FormatException, in a range, a set and JSON, where a trailing NUL was ignored and31\0read as 31 (Migration). - Literals:
StringSetandStringSet<T>refuse an element holding a NUL:FromandAddthrowArgumentExceptionand parsing fails, where the set was built and failed when saved (Value sets). - Literals:
GuidSetandGuidSet<T>read PostgreSQL'suuid_ingrammar.{6f96-19ff-8b86-d011-b42d-00c0-4fc9-64ff}parses, where it threw;{(6f9619ff-…)}and whitespace inside a quoted element throwFormatException, where they parsed (Value sets). - Literals: a
DateTimeOffsetoffset other thanZ,±HH,±HH:mmor±HH:mm:ssthrowsFormatException, where+0530and+5:30were read (Migration). - Literals: decimal text with more precision than
decimalholds, such as a 29th significant digit or1e-30, throwsFormatException, where it was rounded (Literals). - Literals: a range or multirange literal whose lower bound is above its upper bound, such as
[5,1), throwsFormatException, where it read asempty.CreateFinitestill returnsEmpty(Literals). - JSON: the integer, decimal and Guid wrapper sets read only the JSON their plain sets read: a quoted number needs
AllowReadingFromString, and a Guid is hyphenated (Value sets). - ASP.NET Core: a minimal-API parameter typed as one shape, such as
DateRange.Finite, answers 400 for a literal of another shape, where it boundnulland ran the handler (Binding). - EF Core: the packages support PostgreSQL 16 and later. 9.x said 14 or later, but nothing older than 16 was tested (Migration).
- EF Core:
AnyandCountover a captured list of ranges return the in-memory answer, where a list with overlapping, adjacent or empty ranges answered differently.EF.Parameter(list)there throws (Migration).
Added
Float64Range,Float64SetandFloat64Set<T>over finitedoublevalues.NaNand the infinities throw, and the text is what PostgreSQL'sfloat8outprints (Why doubles are finite only).Float32Range,Float32SetandFloat32Set<T>over finitefloatvalues, with the textfloat4outprints. There is noHalffamily, as PostgreSQL has no two-byte float (Why doubles are finite only).IComparableon every range type and onRangeSet, in PostgreSQL's range and multirange order: empty first, then by lower bound, then by upper bound (Sort ranges).ParseandTryParseon every shape (Int32Range.Finite.TryParse, …), which accept only a literal of that shape (Binding).- A range with a
RangeSetoperand forContains,Overlaps,IsAdjacentToand the other range operators, andRangeSet.IsContainedBy(Compare a range with a set). RangeAggandRangeIntersectAggover a sequence ofRangeSet, as PostgreSQL'srange_aggandrange_intersect_aggover multiranges (Aggregates).Gapbetween two ranges, andGaps()andGaps(window)on aRangeSet, in memory and in EF Core queries (Ranges).ShiftandExpandon every range type, which move a range by a step or grow it on both sides, in memory only (Ranges).Spliton every range type, by a step or at calendar period boundaries, andValues(step)on the discrete types, in memory only (Ranges).DateRange.Month,Quarter,YearandIsoWeek, the same onLocalDateRange, andQuarterandYearonYearMonthRange. Weeks are ISO 8601 only, whatever the culture (Ranges).- EF Core:
Float64RangeandFloat32Rangemap to customfloat8rangeandfloat4rangetypes throughHasPostgresRangeandEnableUnmappedTypes(), and their sets todouble precision[]andreal[](Custom range types). - EF Core:
CompareToagainst 0 on a range or multirange runs as<,<=,>,>=or=(EF Core). - EF Core: a range with a multirange operand runs as the operator with the range on the left, and
RangeSet.IsContainedByas<@(EF Core). - EF Core:
RangeAggandRangeIntersectAggover a multirange column inside aGroupByprojection (EF Core). - Docs: PostgreSQL 18 temporal keys over a range column (
WITHOUT OVERLAPS,PERIOD), declared throughEFCore.ComplexIndexes.PostgreSQL(Temporal keys).
Fixed
- Literals: integer text reads PostgreSQL 16's
0x,0oand0bprefixes and underscores between digits, where[0x1F,)and{1_000}threwFormatException(Literals). - Literals: value sets read a dimension decoration such as
[0:1]={1,2}, and{{}}as{}as PostgreSQL 17 does, where both threwFormatException(Value sets). - Literals:
DateTimeandDateTimeOffsettext reads an offset up to ±15:59:59, as PostgreSQL does, where+15:00threwFormatException(Literals). - EF Core:
AnyorCountwith a predicate over an empty captured list of value sets, or of ranges beside aContains, returnsfalseor0, where it threwInvalidOperationException(EF Core). - NodaTime:
LocalDateTimeRangeandLocalDateTimeSetreadZor an offset and keep the wall clock, where they threw.InstantRangeandInstantSetrefuse an offset beyond ±15:59:59 (Literals). - Docs: an exclusive
infinitydate bound excludes the date extreme, so[2026-01-01,infinity)is[2026-01-01,9999-12-30](Domain width). - Docs: a
YearMonthRangecolumn holding an exclusiveinfinity)or(-infinitythrows when it loads. Ask other tools to write an absent or inclusive bound (EF Core). - Docs: a temporal literal with more than six fraction digits can be 1 µs from what PostgreSQL reads and the EF Core package stores (Literals).
- Docs: a database query over a
NaNor infinite float another tool stored answers in PostgreSQL's order, and a CHECK constraint keeps such values out (EF Core).
[9.1.0] — 2026-10-17
Added
- EF Core:
wanted.Contains(b.Tags)over a captured list, array or other collection of value sets runs asIN (@p1, @p2, …), one parameter per set, and returns the same rows as in memory. It threwInvalidCastExceptionin Npgsql.EF.Parameter(wanted)throws and says to remove it (EF Core). - EF Core:
wanted.Contains(b.Seats)over a captured list, array or other collection of ranges runs asIN (@p1, @p2, …), one parameter per range, and returns the same rows as in memory. It failed with42809, and withInvalidCastExceptionforTimeRange.EF.Parameter(wanted)there throws and says to remove it (EF Core).
Fixed
- Literals: whitespace at either end of a quoted or escaped range bound is dropped, as in PostgreSQL, so
[" 2026-01-01",2026-02-01)parses, where it threwFormatException(Literals). - Literals: a date or time value set drops whitespace at either end of a quoted or escaped element, as PostgreSQL does, so
DateSet.Parse("{\" 2026-01-01\"}", null)is{2026-01-01}, where it threwFormatException. A string set keeps the whitespace, as before (Value sets). - EF Core:
ExecuteUpdatesetting a value set inside aToJson()column tonullis still refused, now for its own reason: aNULLthere would turn the whole document intoNULL. The message blamed the JSON array a value would write. Set it withSaveChanges, or replace the whole JSON property (JSON columns).
9.0.0 — 2026-10-11
Nothing was removed or renamed. What existing calls write, accept and translate changed, and the compiler can't show you where: Migration from v8.x.
Breaking
- JSON: every range, variant,
RangeSetand value set writes its literal with nothing registered ("[2026-01-01,2026-06-30]",["a","b"]), where it wrote a property dump such as{"Length":181}that nothing could read back.AddRangeConverters()is now redundant, and a converter on the options or on a property still wins (JSON). - JSON: a converter you register on the options for a range type also handles values serialized through
objector declared as a variant, which wrote the literal before (JSON). - JSON: a property declared as
IRange<T>, a shape interface orIValueSet<T>writes its runtime type's literal, where it wrote{}or a property dump. Reading one throwsNotSupportedException, so declare the concrete type (JSON). - Literals:
ParseandTryParseignore the format provider, and formatting without a format string, or with"", writes the invariant literal whatever the provider.th-THread2026-01-01as the year 1483. To read culture-formatted text, parse the bounds yourself and use the factories (Literals). - Literals: interpolating an
Int32Range,Int64RangeorDecimalRangeis invariant under every culture. Underde-DE,$"{range}"wrote[1,5,2), which parsed back as[1,52)(Literals). - Literals: temporal bounds and elements are strict ISO 8601 in
Parse,TryParse, JSON, theTypeConverterand configuration binding. Dotted, slashed and named-month dates, a date alone for a date-time, and aDateTimeOffsetwithout an offset throwFormatException([01.02.2027,05.03.2027)read as January 2 to May 2).HH:mm, a space before the time andinfinitystill parse (Literals). - Literals: a wrapper set parses element text as its underlying type first and hands your element the round-trip form, so a lenient element
Parseno longer sees other text. An element that ignores the format specifier fails on read, where it was truncated (Value sets). - Literals: a closed temporal set reads its JSON elements with the same ISO grammar, unless you register a converter for the element type. System.Text.Json's own converter read an offset-less
DateTimeOffsetin the machine's offset (JSON). - Literals: a
DateTimebound or element with an offset keeps its wall clock asLocal(2026-01-01T09:30+05:30is 09:30 on every machine), where it was converted into the parsing machine's zone. If you mean instants, useDateTimeOffsetRange, or convert withToUniversalTime()first (Migration). - Literals: decimal bounds and elements use PostgreSQL's numeric grammar, so
"1,000","(5)"and currency symbols throwFormatException. This coversDecimalRange,DecimalSet, the numeric wrapper sets and their EF Core bridge (DecimalSet<Price>.Parse("{\"1,5\"}")was{15}) (Literals). - Literals: the grammar is PostgreSQL's. A quoted-empty or blank range bound throws, where it meant unbounded (
["",5)was(,4]). A backslash escapes outside quotes too ({a\,b}is the one elementa,b).{{a,b}},{a}b}and{a"b}throw, and only ASCII whitespace is skipped (Literals). - NodaTime: a bare
LocalDate,LocalDateTimeorYearMonthin a non-ISO calendar throwsArgumentExceptionon write, and so does such an element ofLocalDateSet<T>,LocalDateTimeSet<T>orYearMonthSet<T>, in memory and as an EF Core operand. Both were stored thirteen days off. Convert withWithCalendar(CalendarSystem.Iso)first (NodaTime). - NodaTime:
AddNodaTimeRangeConverters()no longer outranksConfigureForNodaTimewhen registered first. Any other converter for the type wins, in either order (JSON). - EF Core: a query that builds on a server-computed value set
UnionthrowsInvalidOperationException, where it answered differently from memory: equality in a projection,?:or??around a union,GroupBy,Distinct,OrderBy, set operations,Countthrough??, andExecuteUpdateto a union. UseIsSubsetOfandIsSupersetOf, or materialize first (Migration). - EF Core: if you replace
IRelationalParameterBasedSqlProcessorFactoryafterUseValueRanges(), every query with a server-sideUnionfails withvalueranges_union_without_sql_guard. Wrap the registered factory instead (Migration). - EF Core: temporal SQL constants carry the value Npgsql binds, truncated to the microsecond toward 2000-01-01, with the extremes as
-infinityandinfinity. A constant now matches the row a parameter matched, and a temporal set whose elements are less than a microsecond apart is stored with one of them (Migration).
Added
- A
TypeConverteron every type, so configuration binding reads"Period": "[2026-01-01,2026-06-30]", where it boundnullwith no error. It is always invariant, an empty string isnull, and a variant rejects a literal of another shape (TypeConverter). - Native AOT and trimming: the core and NodaTime packages build with
IsAotCompatible, andRangeSetand the wrapper sets no longer fail with "missing native code". A source-generated context lists a value set's element type as well as the set (Native AOT). - Ranges and multiranges as dictionary keys, written as the literal (
{"[2026-01-01,2026-06-30]":181}), where they threwNotSupportedException.DictionaryKeyPolicyis not applied, and two spellings of one key collide; setAllowDuplicateProperties = falseto reject that (Dictionary keys). Deconstructon every bounded variant:var (start, end) = finiteon a discrete type, and(start, end, startInclusive, endInclusive)on a continuous one (Ranges).- NodaTime:
infinityand-infinityparse as the extremes ofInstant,LocalDateandLocalDateTime, the ones Npgsql maps them to (NodaTime). - Docs: an OpenAPI schema transformer to copy into your API project. It describes ranges and multiranges as
type: stringwith a pattern and value sets as arrays, where ASP.NET Core wrote{}(OpenAPI).
Changed
- The range interfaces (
IRange<T>, the shape interfaces andIRangeFactory<TRange, T>) are not an extension point. Implementing them outside the library is unsupported, and they may gain members in a minor version (Architecture).
Fixed
- EF Core: a range, multirange or value set inside a
ToJson()column saves, whereSaveChangesthrewNullReferenceException, and its operators translate (CAST(doc ->> 'Period' AS daterange) @> @day). The document holds the store's text, a persisted format:"{a,b}"for a value set, month-aligned dates for aYearMonthtype. A compiled model works;--nativeaotdoesn't yet (JSON columns). - EF Core:
ExecuteUpdateof a value set inside aToJson()column throws, however the setter reaches it, because EF Core would store a JSON array the row could no longer load from. Setting the whole complex property still works (JSON columns). - EF Core: a range built in a query from values that don't depend on it, such as
r.Period == DateRange.CreateFinite(…), translates, where it threw "does not have a type mapping assigned". - EF Core: a temporal range's element operand gets the range's own mapping.
DateTimeOffsetRange.Contains(dto)threw for any non-UTC offset, andDateTimeRange.Containsfor a UTCDateTime. - EF Core: a
LocalDateRangeorLocalDateTimeRangeelement operand in another calendar is converted to ISO, where it missed its row or threw inside Npgsql. - EF Core: a
DateTimeSet<T>element keeps its wall clock on the way to the column, even one the machine's time zone skips. - NodaTime:
HH:mmparses for aLocalTime, the time of aLocalDateTime, and anInstantwith its offset orZ, as the BCL types parse it. DecimalSet<T>reads exponent notation ({1e3}, a JSON1.5e1), asDecimalSetdoes.
8.0.0 — 2026-08-17
No signature changed. Some existing calls answer differently, and one query that ran now throws: Migration from v7.x.
Breaking
- EF Core:
==,!=andEqualsover a server-computed value setUnionfail translation in a filter, where they returned wrong rows:array_catkeeps duplicates and the operands' order. UseContains,Overlaps,IsSubsetOforIsSupersetOf, or callAsEnumerable()first (Migration). RangeSet.Except(TRange)with an infinite range returns the empty set, where it returned the whole domain, andComplement()of the infinite set does too.YearMonthRange.NextValueAfterandPreviousValueBeforereject a non-ISO calendar, where they stepped in it and returned a value the type's constructors reject.RangeSet.Contains(T)on the infinite set returnstrue, where it threwInvalidOperationException.LocalDateRange's step functions returnnullat a Gregorian9999-12-31, where they threw. Only code that caught these exceptions is affected.
Added
- Docs: a getting-started guide, and the migration guide covers v7 and v8 (Getting started).
Changed
- An
IRange<T>that is none of the five shapes makesIRangeFactory.ToStringthrow, where it was formatted as"empty"and stored as the empty range. - A shape dispatch with no case for a pair of shapes throws
UnreachableExceptionnaming the pair, instead of returning a value. No reachable call changed. - EF Core: the wrapper-set bridge rejects an element type it doesn't know, instead of writing its
ToString().
Fixed
- Docs: a switch over the five range shapes needs a
_arm, which should throw. The docs said the compiler checks the cases, and the example failed to build with CS8509 (Pattern matching). - Docs: the value set table lists a wrapper set for every family, and the EF Core README's links reach
docs/efcore.md.
7.0.0 — 2026-08-17
Nothing was removed or renamed. Three range fixes change what existing calls answer in memory: Migration from v6.x.
Breaking
ContainsandIsContainedBytreat the empty range as contained by every range, as PostgreSQL's@>does, so[1,5].Contains(Int32Range.Empty)istrue. For the old meaning, writeouter.Contains(inner) && !inner.IsEmpty()(Migration).IsStrictlyLeftOfandIsStrictlyRightOfgive PostgreSQL's<<and>>answer for a range unbounded at its start:(,5]is left of[10,20], where it returnedfalse. A one-elementRangeSetchanges with it.Exceptsubtracts when the two ranges are unbounded in opposite directions:(,5].Except([1,))is{(,0]}, where it returned(,5]unchanged.RangeSet.Exceptchanges with it.- JSON:
Int16Set<T>,Int32Set<T>,Int64Set<T>andDecimalSet<T>honourJsonNumberHandling.WriteAsStringand write strings, as their primitive sets do. Reads are unchanged (Migration). DecimalRange.Lengthreturnsnullfor a span wider thandecimal, where it threwOverflowException.
Added
- A wrapper set for every family:
Int16Set<T>,DecimalSet<T>,DateSet<T>,TimeSet<T>,DateTimeSet<T>andDateTimeOffsetSet<T>, and in the NodaTime packageLocalDateSet<T>,LocalDateTimeSet<T>,InstantSet<T>,LocalTimeSet<T>andYearMonthSet<T>, each mapped by EF Core to its array column.DecimalSet<T>writes JSON numbers with their scale (Value sets). SetTypeRegistry.RegisterFamily, for a package that adds wrapper set families.
Changed
- The temporal wrapper sets ask their elements for a round-trip format (
yyyy-MM-ddforDateSet<T>,Ofor the others, the ISO pattern in NodaTime), so a hand-written element must pass itsformatargument on. One that ignores it is rejected when stored (Value sets). - JSON: a temporal wrapper set writes the same value as its primitive set but not the same bytes: seven fraction digits, and
+escaped. Give the element its own[JsonConverter]if the response shape matters (Value sets). - EF Core:
DateTimeSet<T>andDateTimeOffsetSet<T>normalize as their closed sets do:Unspecifiedfortimestamp, UTC fortimestamptz. - EF Core docs:
Counton a value set, like==, assumes every row is canonical. A row another tool stored as{b,a,b}counts 3 in SQL and loads as{a,b}. No behaviour changed (EF Core).
Fixed
- EF Core:
Int64Range.Contains(25)with a constant runs, where PostgreSQL rejected it (42883: operator does not exist: int8range @> integer). The constant now carries::bigint, and no other SQL changed.
6.3.0 — 2026-08-16
Added
RangeSet.IsInfinity()andIsFinite().IsInfinity()is true only for the set that covers the whole domain, which{(,5],[10,)}is not, though it is unbounded at both ends. Both translate (Ranges).- Collection expressions for
RangeSet<TRange, T>(RangeSet<Int32Range, int> set = [a, b];), normalized asFromnormalizes. AlsoFrom(params ReadOnlySpan<TRange>)and the builderRangeSet.Create<TRange, T>(Ranges). ISpanParsable<T>on every range type,RangeSetand every value set:ParseandTryParseover aReadOnlySpan<char>(Literals).Lengthon every range: a count for a discrete type, both ends included, and a span for a continuous one. It is0for the empty range andnullwhen unbounded, and runs in memory only (Ranges).Values()on the discrete ranges (Int32Range,Int64Range,DateRange,LocalDateRange,YearMonthRange) lists the values in order. An unbounded range throws at the call (Ranges).ToRangeSet()onInt32Set,Int64Set,DateSet,LocalDateSetandYearMonthSet, andToInt32Set(),ToDateSet()and the others back:{1,2,3,7}becomes{[1,3],[7,7]}. In memory only (Value sets).Clamp(value)on every range: the contained value nearest the argument, ornullfor the empty range (Ranges).- An indexer on every value set (
set[0]). IRangeFactory<TRange, T>.IsDiscrete, for generic code that needs to know whether the element type has a step.- EF Core:
RangeSet.IsFinite()andIsInfinity()translate, toNOT lower_inf(x) AND NOT upper_inf(x) AND NOT isempty(x)and to equality with'{(,)}'(What runs where).
6.2.1 — 2026-08-16
Breaking
IsAdjacentToreturnstruefor an unbounded range next to its neighbour, as PostgreSQL's-|-does:(,0].IsAdjacentTo([1,3])returnedfalse.RangeSet.FromandUnionnow merge such neighbours, soFrom([(,0], [1,)])is{(,)}(Ranges).- A literal that fails to parse, through
Parseor JSON, quotes at most 64 characters of the input and its length, and no longer chains the element parser's exception. If you match the message or readInnerException, update that. Exception types are unchanged.
6.2.0 — 2026-08-16
Breaking
- JSON: a
nullrange reads back asnull, where it threwJsonException. If you relied on that to reject a null, validate it yourself.nulland"empty"stay different.
Fixed
- EF Core:
CountoverUnion(…).Remove(x)fails translation in a filter and runs in memory in a projection, where it counted shared elements twice.
6.1.0 — 2026-08-15
Breaking
- JSON: a wrapper set whose element has no
[JsonConverter]writes the underlying values (["users.read"],[1,2]), where it wrote objects such as[{"Value":"users.read"}]or[{}]. Reads run the element'sParse, and a registered converter still wins (Value sets).
Added
IValueSetFactory<TSet, T>.ElementJsonConverter, the element converter a set uses when none is registered.RangeVariantJsonConverter<TVariant, TRange, T>.- NodaTime:
AddNodaTimeRangeConverters(), for bare NodaTime values next to a set. It works before or afterConfigureForNodaTime.
Fixed
- JSON: the NodaTime sets write ISO 8601 elements, where they wrote objects and read back
default. - JSON: a
nullrange property is written asnull, where it threwNullReferenceException. Readingnullstill throws; write"empty"for the empty range. - JSON: a range serialized through
objectworks, where it threwArgumentException.
6.0.0 — 2026-08-14
Value sets, a second type family. No breaking changes.
Added
- Value sets:
StringSet,GuidSet,Int16Set,Int32Set,Int64Set,DecimalSet,DateSet,TimeSet,DateTimeSetandDateTimeOffsetSet, deduplicated and sorted, with no nulls. The wrapper setsStringSet<T>,GuidSet<T>,Int32Set<T>andInt64Set<T>hold your own element types and need only BCL interfaces on them (Value sets). - NodaTime value sets:
LocalDateSet,LocalDateTimeSet,InstantSet,LocalTimeSetandYearMonthSet. - Set operations:
Contains,Overlaps,IsSubsetOf,IsSupersetOf,IsProperSubsetOf,IsProperSupersetOf,Union,Remove,CountandIsEmpty, andIntersect,ExceptandAddin memory only. Also array literals, JSON and collection expressions. - EF Core: value set columns as native arrays (
StringSet→text[], …), with wrapper sets found automatically. The operations translate to@>,&&,<@,cardinality,array_catandarray_remove;Containsis@>, so a GIN index serves it.Countover aUnionfails translation (EF Core).
5.0.0 — 2026-08-13
New range types beyond PostgreSQL's built-ins. No breaking changes.
Added
TimeRangeoverTimeOnly, for the customtimerangetype. EF Core needsHasPostgresRangeandEnableUnmappedTypesfor it (TimeRange).- NodaTime:
YearMonthRangeoverYearMonth, stored as a month-aligneddaterange, with conversions to and fromLocalDateRangeandDateInterval. A non-ISO calendar is rejected.
4.1.0 — 2026-08-12
Added
CodoMetis.ValueRanges.NodaTime:LocalDateRange(daterange),LocalDateTimeRange(tsrange) andInstantRange(tstzrange), with conversions to and from NodaTime'sIntervalandDateInterval.CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime: maps them throughNpgsql.EntityFrameworkCore.PostgreSQL.NodaTime, turned on withnpgsql.UseValueRangesNodaTime().
Changed
- EF Core: range types are registered through
RangeTypeRegistry.Register, so other packages can add theirs. Your code doesn't change.
4.0.0 — 2026-08-10
Every PostgreSQL range and multirange operator now runs in memory and in SQL: Migration from v3.x.
Breaking
==and!=onRangeSetcompare values, as the range records and SQL=do. UseReferenceEqualswhere you meant identity.DoesNotExtendRightOfandDoesNotExtendLeftOfmatch PostgreSQL for infinite bounds:+∞ ≤ +∞, so an unbounded range no longer always returnsfalse.
Added
- Bound accessors:
LowerBound()andUpperBound()returnT?,nullwhere PostgreSQL returnsNULL, andLowerBoundInclusive()andUpperBoundInclusive(), on ranges andRangeSet. EF Core translates them tolower,upper,lower_incandupper_inc. Merge: the smallest range covering both, gap included (range_merge).- Aggregates:
RangeAgg()andRangeIntersectAgg(), translated torange_aggandrange_intersect_agginside aGroupBy. RangeSetoperators:Contains,Overlaps,IsAdjacentTo,IsStrictlyLeftOf,IsStrictlyRightOf,DoesNotExtendLeftOf,DoesNotExtendRightOf,IsEmpty(),IsUnboundedStart()andIsUnboundedEnd(), each translated to its multirange operator.
Fixed
RangeSet.Infinite.Contains(range)andOverlaps(range)work for a range with a finite bound, where they threwInvalidOperationException.
3.1.0 — 2026-06-17
Added
RangeSet<TRange, T>.LowerBoundComparer, the set's lower-bound ordering as anIComparer<TRange>, also asRangeLowerBoundComparer<TRange, T>.Instance.
Changed
RangeSetis faster:ContainsandOverlapsare O(log n), andUnion,IntersectandExceptmerge two sorted lists. No result changed.
Fixed
- A quoted range bound unescapes
\"and\\, so an element whose text holds a quote or a backslash round-trips.
3.0.0 — 2026-06-11
Breaking
IsEmpty,IsFinite,IsInfinity,IsUnboundedStartandIsUnboundedEndare methods, so add parentheses at every call. As methods they translate to SQL.
Added
CodoMetis.ValueRanges.EFCore.PostgreSQL: maps the range types to PostgreSQL range and multirange columns and translates the range operations to SQL, turned on withnpgsql.UseValueRanges()(EF Core).
2.0.0 — 2026-06-11
Breaking
ToString()returns the PostgreSQL literal ([1,10]), where it returned the record'sFinite { Start = 1, … }. Update code that logs, displays or compares that text.
Added
ParseandTryParsefor PostgreSQL range literals.
1.0.0 — 2026-06-10
The first release.
Added
- The six PostgreSQL range types as unions of five shapes (
Finite,UnboundedStart,UnboundedEnd,EmptyRange,Infinity), with the range operations andRangeSet<TRange, T>, a multirange that keeps itself normalized.