CodoMetis.ValueRanges 10.1.0

Prefix Reserved
dotnet add package CodoMetis.ValueRanges --version 10.1.0
                    
NuGet\Install-Package CodoMetis.ValueRanges -Version 10.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CodoMetis.ValueRanges" Version="10.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CodoMetis.ValueRanges" Version="10.1.0" />
                    
Directory.Packages.props
<PackageReference Include="CodoMetis.ValueRanges" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CodoMetis.ValueRanges --version 10.1.0
                    
#r "nuget: CodoMetis.ValueRanges, 10.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CodoMetis.ValueRanges@10.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CodoMetis.ValueRanges&version=10.1.0
                    
Install as a Cake Addin
#tool nuget:?package=CodoMetis.ValueRanges&version=10.1.0
                    
Install as a Cake Tool

CodoMetis.ValueRanges

Range types and value sets for .NET 10 that work entirely in memory. You get PostgreSQL's complete range and set algebra, with no database dependency.

You get two families of types. Both are immutable, and both are in canonical form from the moment you create them:

  • Range types. The six element types PostgreSQL ships built-in range types for, plus TimeRange, Float64Range and Float32Range. Each range type is a discriminated union of five sealed variants. RangeSet<TRange, T> is the matching multirange, and it is always kept normalized.
  • Value sets (v6). Sets of scalar values, deduplicated and sorted, that PostgreSQL stores as a native array.

Everything runs in your process. The EF Core package, CodoMetis.ValueRanges.EFCore.PostgreSQL, maps both families to PostgreSQL columns and runs the same calls as SQL. So the same code works in memory and in a query.

dotnet add package CodoMetis.ValueRanges

Requires .NET 10 or later.

Native AOT and trimming compatible. Your publish gets no trim or AOT warnings, and a NativeAOT consumer in CI verifies this. For JSON under Native AOT you need a source-generated context (details).

Why not NpgsqlRange<T>?

Because you can't use it in a domain model. NpgsqlRange<T> is declared in NpgsqlTypes, in Npgsql.dll, so a model that uses it references the database driver. The struct also has no interval algebra of its own. Contains, Overlaps, Union and the rest are EF Core extension methods, each documented as "only intended for use via SQL translation as part of an EF Core LINQ query". If you call one from a unit test or a domain service, it throws. So the algebra exists only inside a query, on a type that exists only inside the driver.

These types work the other way round. The algebra runs in your process, with no database dependency. The EF Core package runs the same calls as the same PostgreSQL operators, and the results are verified against a live server.

The other options each cover only part of this:

  • NodaTime's Interval and DateInterval are real domain types, and the Npgsql NodaTime plugin maps them. But they cover only two date/time shapes, and a DateInterval is always closed and bounded. It can never be half-open, unbounded or empty.
  • In-memory range libraries such as FRange have an algebra but no persistence. They have no discrete element types either: [1,10) and [1,9] stay different values, so they can't decide adjacency for integers and dates. And unboundedness stays a runtime fact. Asking an unbounded range for its bound value throws. Here that property doesn't exist, so there is nothing to ask.
  • A PostgreSQL array maps to T[] or List<T>. Those are mutable references. Your code can change an element after loading it, so the domain can't protect a rule once it has handed the value out. And a list is not a set: order and duplicates are part of its value. The value sets here are immutable end to end. Every way of creating one puts it in canonical form, and no member can change it afterwards.

The full comparison is in Why this exists.

Range types

.NET type PostgreSQL equivalent Element type Discrete
Int32Range int4range int ✓
Int64Range int8range long ✓
DecimalRange numrange decimal —
DateRange daterange DateOnly ✓
DateTimeRange tsrange DateTime —
DateTimeOffsetRange tstzrange DateTimeOffset —
TimeRange timerange (custom) TimeOnly —
Float64Range float8range (custom) double, finite only —
Float32Range float4range (custom) float, finite only —

The NodaTime versions are LocalDateRange, LocalDateTimeRange, InstantRange and YearMonthRange, which works at month granularity. They are in the NodaTime package, CodoMetis.ValueRanges.NodaTime.

Unboundedness is a shape, not a bound value

Each range is a discriminated union of five kinds of range: Finite, UnboundedStart, UnboundedEnd, EmptyRange and Infinity. This shape is part of the static type:

  • UnboundedEnd has no End property, so there is nowhere to put a sentinel.
  • Finite has no flag that could disown its End.
  • Neither has a nullable bound.

So invalid states can't be represented. You can't declare your own subtype either, which makes these five the only ranges that can exist. A switch over them is therefore complete in fact. C# can't prove that, though, so a switch expression still needs a discard arm that is never reached (pattern matching).

DateTimeRange.CreateUnboundedEnd(start)               // genuinely open-ended
DateTimeRange.CreateFinite(start, DateTime.MaxValue)  // ends at a specific instant

These two are not interchangeable, and the compiler won't let you mix them up. A representation built from bounds plus flags has to sort this out at runtime. The difference still matters at the database boundary, where DateTime.MaxValue maps to PostgreSQL infinity. See Unboundedness is a shape, not a bound value for the full reasoning.

Operations

  • Containment, overlap and adjacency: Contains, Overlaps, IsContainedBy, IsAdjacentTo.
  • Directional comparisons: IsStrictlyLeftOf/RightOf, DoesNotExtendLeftOf/RightOf.
  • Set operations: Intersect, Union, Except, Complement, Merge, Gap, Gaps.
  • Arithmetic: Shift moves a range, Expand grows it on both sides.
  • Bound accessors: LowerBound/UpperBound/LowerBoundInclusive/UpperBoundInclusive. They return null exactly where PostgreSQL returns NULL.
  • Aggregates: RangeAgg/RangeIntersectAgg.
  • Calendar ranges: DateRange.Month, Quarter, Year and IsoWeek. Weeks are ISO 8601, Monday to Sunday.
  • Splitting: Split cuts a range into steps or calendar periods; Values(step) skips ahead on a discrete range.

Every one of them returns exactly what PostgreSQL returns, including how PostgreSQL canonicalizes discrete ranges. Ranges and range sets shows each operation with examples.

Why these element types

Interval algebra needs two things from the element type:

  • A total order that the type's own comparisons agree with.
  • A defined step, for adjacency.

double meets the first one only without NaN. .NET sorts NaN below every number, and PostgreSQL sorts it above Infinity, so no comparison involving it could agree on both sides. The float ranges and sets (Float64Range, Float32Range, Float64Set, Float32Set) therefore hold finite numbers only, and creating, parsing or loading NaN or an infinity throws. Limiting the element types to a checked list is what keeps the algebra sound. See Why these element types.

Value sets (v6)

A value set is an immutable set of scalar values in canonical form: deduplicated, sorted, and free of nulls. Two sets with the same values are equal. A set relates to a PostgreSQL array column the way RangeSet<DateRange, DateOnly> relates to a datemultirange. The .NET type is the idea, and the column is how it is stored.

.NET type Element type PostgreSQL column With your own element type
StringSet string text[] StringSet<TElement>
GuidSet Guid uuid[] GuidSet<TElement>
Int16Set short smallint[] Int16Set<TElement>
Int32Set int integer[] Int32Set<TElement>
Int64Set long bigint[] Int64Set<TElement>
DecimalSet decimal numeric[] DecimalSet<TElement>
Float64Set double, finite double precision[] Float64Set<TElement>
Float32Set float, finite real[] Float32Set<TElement>
DateSet DateOnly date[] DateSet<TElement>
TimeSet TimeOnly time[] TimeSet<TElement>
DateTimeSet DateTime timestamp[] DateTimeSet<TElement>
DateTimeOffsetSet DateTimeOffset timestamptz[] DateTimeOffsetSet<TElement>
var tags = StringSet.From("beta", "alpha", "beta");   // {alpha,beta} — deduplicated, sorted
StringSet more = ["gamma", "alpha"];                  // collection expressions work

tags.Contains("alpha");      // true
tags.Overlaps(more);         // true — shares "alpha"
tags.IsProperSubsetOf(more); // false — proper containment excludes equality
tags.Union(more);            // {alpha,beta,gamma}
tags.Count;                  // 2

The full guide is Value sets.

Use your own element types

Every set family has a version for your own element types, such as typed keys generated by Vogen, Metalama or StronglyTypedId, or written by hand. These versions (the wrapper arities) require only standard .NET interfaces, so your types never reference this package. They are StringSet<T>, GuidSet<T>, Int16Set<T>, Int32Set<T>, Int64Set<T>, DecimalSet<T>, Float64Set<T>, Float32Set<T>, DateSet<T>, TimeSet<T>, DateTimeSet<T> and DateTimeOffsetSet<T>. The NodaTime package adds five more.

One rule is a convention, because no constraint can express it: your element's text must be exactly the text of the value it wraps. A wrapper whose ToString(format, provider) passes both arguments on to that value follows the rule. This matters most for the temporal versions. They ask for a round-trip format because the default format drops fractions of a second. See Keep the text form of the underlying type.

Why every set is sorted and deduplicated

Every way of creating a set sorts it and removes duplicates: From, parsing, JSON, and loading it from the database. You get two things from that:

  • Change detection is cheap and exact. EF Core's ValueComparer comes down to a cheap equality check, and it never reports a change that isn't one.
  • SQL = means set equality. Two stored arrays are equal exactly when the sets are.

The sort order is chosen on purpose:

  • String-backed sets sort ordinal, never culture-sensitive. The order is a storage rule that every writer shares, not a display order. With a culture sort, two machines could disagree about the same set.
  • Every other set sorts by the element's own comparison.

Good to know:

  • Intersect, Except and Add run in memory only. PostgreSQL's array type has no intersection, no difference and no sorted insert.

Literals, parsing and JSON

Every type implements IParsable<T> and IFormattable with PostgreSQL literal syntax. Every type also carries its own System.Text.Json converter, so it works in ASP.NET Core APIs without any registration:

Int32Range.Parse("[1,10)", null).ToString();   // [1,9] — discrete ranges canonicalize
StringSet.Parse("{alpha,beta}", null);         // {alpha,beta}

JsonSerializer.Serialize(Int32Range.CreateFinite(1, 10));   // "[1,10]"

Literals, parsing, and JSON covers the parsing rules and the JSON options.

Documentation

The project README is the starting point. The guides go into detail:

See also the changelog.

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on CodoMetis.ValueRanges:

Package Downloads
CodoMetis.ValueRanges.EFCore.PostgreSQL

Entity Framework Core (Npgsql) plugin for CodoMetis.ValueRanges: maps the range types to the PostgreSQL range columns (int4range, int8range, numrange, daterange, tsrange, tstzrange, and the custom timerange, float8range and float4range) and RangeSet<TRange, T> to the corresponding multirange columns — no manual value converters required. Translates the full range algebra from LINQ to SQL: Contains, Overlaps, IsContainedBy, IsStrictlyLeftOf/RightOf, DoesNotExtendLeftOf/RightOf, IsAdjacentTo (@>, <@, &&, <<, >>, &<, &>, -|-) on ranges and multiranges, bound accessors (lower, upper, lower_inc, upper_inc), Merge (range_merge), Gap/Gaps, CompareTo against 0 (<, <=, >, >=, =), the RangeAgg/RangeIntersectAgg aggregates (range_agg, range_intersect_agg), Intersect (*), Union and Except (multirange + and -), RangeSet operations and operators including == equality, and the CreateFinite/CreateUnboundedStart/CreateUnboundedEnd factories as range constructor calls. v6 also maps the value set types to native PostgreSQL arrays (StringSet to text[], GuidSet to uuid[], …) by convention — wrapper instantiations like StringSet<YourKey> recognized automatically, nothing to configure — and translates the set algebra to the array operators: Contains as GIN-servable containment (@> ARRAY[value]), Overlaps (&&), IsSubsetOf (<@), IsSupersetOf (@>), Union (array_cat), Count/IsEmpty (cardinality). Enable with one line: options.UseNpgsql(..., npgsql => npgsql.UseValueRanges()).

CodoMetis.ValueRanges.NodaTime

NodaTime range types for CodoMetis.ValueRanges: LocalDateRange (daterange), LocalDateTimeRange (tsrange), InstantRange (tstzrange) and YearMonthRange — a month-granularity range for billing and reporting periods, stored as a month-aligned daterange by the EF Core companion. Each range type is a discriminated union of five sealed variants (Finite, UnboundedStart, UnboundedEnd, EmptyRange, Infinity) with the full interval algebra of the core package: Contains, Overlaps, IsAdjacentTo, Intersect, Union, Except, Merge, bound accessors, RangeAgg/RangeIntersectAgg aggregates, RangeSet<TRange, T> multiranges, PostgreSQL literal parsing/formatting and System.Text.Json support. Includes conversions to and from NodaTime's own Interval and DateInterval. LocalDateTime is wall-clock time by construction and Instant is an instant by construction, so the DateTimeKind and offset-normalization caveats of the BCL-based types do not arise. v6 adds the NodaTime value sets — LocalDateSet (date[]), LocalDateTimeSet (timestamp[]), InstantSet (timestamptz[]), LocalTimeSet (time[]) and the month-granularity YearMonthSet (month-aligned date[]): immutable, canonical (deduplicated, sorted, null-free) sets with the full membership algebra, ISO-calendar normalization at construction, PostgreSQL array literals and JSON support. v6.4 adds a validated-wrapper arity for each of them (LocalDateSet<T>, LocalDateTimeSet<T>, InstantSet<T>, LocalTimeSet<T>, YearMonthSet<T>), constrained only on BCL interfaces, so generator-produced domain values never reference this package.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.1.0 193 10/19/2026
10.0.0 290 10/19/2026
9.1.0 1,558 10/17/2026
9.0.0 372 10/11/2026
8.0.0 461 8/17/2026
7.0.0 220 8/17/2026
6.3.0 164 8/16/2026
6.2.1 179 8/16/2026
6.2.0 243 8/15/2026
6.1.0 150 8/14/2026
6.0.0 153 8/14/2026
5.0.0 149 8/13/2026
4.1.0 148 8/12/2026
4.0.0 137 8/10/2026
3.1.0 172 6/17/2026
3.0.0 152 6/11/2026
2.0.0 143 6/11/2026
1.0.0 139 6/10/2026