CodoMetis.ValueRanges.EFCore.PostgreSQL
10.1.0
Prefix Reserved
dotnet add package CodoMetis.ValueRanges.EFCore.PostgreSQL --version 10.1.0
NuGet\Install-Package CodoMetis.ValueRanges.EFCore.PostgreSQL -Version 10.1.0
<PackageReference Include="CodoMetis.ValueRanges.EFCore.PostgreSQL" Version="10.1.0" />
<PackageVersion Include="CodoMetis.ValueRanges.EFCore.PostgreSQL" Version="10.1.0" />
<PackageReference Include="CodoMetis.ValueRanges.EFCore.PostgreSQL" />
paket add CodoMetis.ValueRanges.EFCore.PostgreSQL --version 10.1.0
#r "nuget: CodoMetis.ValueRanges.EFCore.PostgreSQL, 10.1.0"
#:package CodoMetis.ValueRanges.EFCore.PostgreSQL@10.1.0
#addin nuget:?package=CodoMetis.ValueRanges.EFCore.PostgreSQL&version=10.1.0
#tool nuget:?package=CodoMetis.ValueRanges.EFCore.PostgreSQL&version=10.1.0
CodoMetis.ValueRanges.EFCore.PostgreSQL
This package lets you store the types of
CodoMetis.ValueRanges in PostgreSQL with
Entity Framework Core and Npgsql. A range goes into a range column, a RangeSet<TRange, T> into a
multirange column, and a value set into a native array column. Your LINQ queries over them run in
the database as SQL, and they return the same results as the same code in memory.
dotnet add package CodoMetis.ValueRanges.EFCore.PostgreSQL
It needs PostgreSQL 16 or later. You turn it on with one line. You don't configure value converters, comparers or column types:
options.UseNpgsql(connectionString, npgsql => npgsql.UseValueRanges());
Which column each type gets
EF Core picks the column from the property type:
| Property type | Column type |
|---|---|
Int32Range |
int4range |
RangeSet<Int32Range, int> |
int4multirange |
DateRange |
daterange |
RangeSet<DateRange, DateOnly> |
datemultirange |
TimeRange |
timerange (custom type — see below) |
Float64Range |
float8range (custom type — see below) |
Float32Range |
float4range (custom type — see below) |
Float64Set |
double precision[] |
Float32Set |
real[] |
StringSet |
text[] |
GuidSet |
uuid[] |
| … and so on for every type |
Sets over your own element types work too. A property such as StringSet<AccessRight> is
recognized from the open generic, so there is no registration per element type to forget. Plain
string[] and List<Guid> properties keep Npgsql's own array mapping, and they can sit in the same
model.
Good to know:
TimeRangeneeds two opt-ins. PostgreSQL has no built-intimerange. You callHasPostgresRangeto create the type, andEnableUnmappedTypeson the data source. The EF Core guide shows both in TimeRange and the custom timerange type.Float64RangeandFloat32Rangeneed the same two opt-ins. Create the types withHasPostgresRange("float8range", "double precision"), without asubtype_diff, which overflows a GiST index over very large values, andHasPostgresRange("float4range", "real"). Rows holdingNaNor an infinity throw when loaded. See Custom range types.- Sets are mapped as single values (scalars). The package brings its own mappings and translators for them. EF Core's primitive-collection support is never involved.
- Inside a
ToJson()column, a value is stored as its PostgreSQL text. This applies to a range, a multirange and a value set. A query into the document runs through a cast:CAST(doc ->> 'Period' AS daterange) @> @day. - That text is the database's form, not the JSON your API sends. A value set is stored as
"{a,b}", and theYearMonthtypes as month-aligned dates. A temporal value is stored at the server's microsecond, truncated the way Npgsql truncates a parameter, with the extremes as-infinityandinfinity. So the document, a column and a parameter all hold the same value. ExecuteUpdateof a value set inside a JSON document throws. You can set a range inside the document, and you can set the whole JSON property.- Scaffolding doesn't produce these types.
dotnet ef dbcontext scaffoldmaps range columns toNpgsqlRange<T>and array columns to plain arrays, because the package provides no design-time services. Change the property types yourself after scaffolding. Using these types is a decision about your model.
Query ranges
var day = new DateOnly(2024, 6, 15);
bookings.Where(b => b.Period.Contains(day)); // b."Period" @> @day
bookings.Where(b => b.Period.Overlaps(other)); // b."Period" && @other
bookings.Select(b => b.Period.Intersect(other)); // b."Period" * @other
bookings.OrderBy(b => b.Period.LowerBound()); // ORDER BY lower(b."Period")
bookings.Select(b => b.Period.Merge(other)); // range_merge(b."Period", @other)
bookings.Select(b => b.BlockedDays.Gaps(month)); // datemultirange(@month) - b."BlockedDays"
bookings.Where(b => b.BlockedDays == someSet); // b."BlockedDays" = @set
bookings.GroupBy(b => b.CustomerId) // range_agg(b."Period")
.Select(g => g.Select(b => b.Period).RangeAgg());
Each comparison runs as one PostgreSQL operator:
| Method | Operator |
|---|---|
Contains |
@> |
Overlaps |
&& |
IsContainedBy |
<@ |
IsStrictlyLeftOf / IsStrictlyRightOf |
<< / >> |
DoesNotExtendLeftOf / DoesNotExtendRightOf |
&< / &> |
CompareTo against 0 |
<, <=, >, >=, = |
IsAdjacentTo runs as -|-. All of these work on ranges and on a RangeSet, with either a range
or a multirange as the argument.
You can combine ranges in the database too:
Intersectruns as*.UnionandExceptturn both sides into multiranges and run as+and-. That matches theirRangeSetreturn type. So the union of two disjoint ranges comes back as a real two-element multirange, not an error.- The factories
CreateFinite,CreateUnboundedStartandCreateUnboundedEndrun as range constructor calls with a guard. The guard keeps the model's rule: bounds in the wrong order give an empty range.
The state checks map to PostgreSQL's range functions:
| Method | SQL |
|---|---|
IsEmpty() |
isempty |
IsUnboundedStart() |
lower_inf |
IsUnboundedEnd() |
upper_inf |
IsInfinity() |
lower_inf AND upper_inf |
IsFinite() |
NOT lower_inf AND NOT upper_inf AND NOT isempty |
Good to know:
- Discrete ranges come back exactly as in memory. PostgreSQL stores
int4range,int8rangeanddaterangehalf-open, as[lower, upper). The model keeps them closed, as[lower, upper]. The package makes up for the difference.UpperBound()runs asupper(x) - 1, andUpperBoundInclusive()asNOT upper_inf(x) AND NOT isempty(x). Results from the database always equal the results in memory, checked against a live PostgreSQL. LowerBound()andUpperBound()returnT?. PostgreSQL'slowerandupperreturnNULLwhen that side is unbounded or the range is empty. The in-memory methods returnnullin the same cases.- Aggregates over zero rows return
NULLin SQL. That is standard PostgreSQL behaviour. In memory,RangeAgg()over nothing returns the empty set.RangeIntersectAgg()returnsnullin both places. DateTimeRangebounds are stored as wall-clock time. They are written astimestampwithDateTimeKind.Unspecified. A UTC-kindedDateTimeis reinterpreted as wall-clock time, not converted.DateTimeOffsetRangebounds are stored in UTC. They are normalized to UTC fortimestamptz. You keep the instant, but the original offset doesn't come back.DateTime.MinValueandMaxValueare stored as-infinityandinfinity. That is Npgsql's default rule. Such a bound is still a finite bound that happens to be infinite. It stays different from an unbounded side, soupper_infstaysfalse.
Query value sets
users.Where(u => u.Roles.Contains("admin")); // u."Roles" @> ARRAY['admin']::text[]
users.Where(u => u.Roles.Overlaps(required)); // u."Roles" && @required
users.Where(u => u.Roles.IsSubsetOf(granted)); // u."Roles" <@ @granted
users.Where(u => u.Roles.Count > 2); // cardinality(u."Roles") > 2
users.Where(u => u.Roles.Remove("admin").IsEmpty); // cardinality(array_remove(…)) = 0
Contains always runs as @>, never as = ANY, so a plain GIN index on the column serves it.
Good to know:
Unionis the one set operation whose SQL result isn't deduplicated and sorted. It runs asarray_cat, which joins two arrays without removing duplicates.- That is harmless in exactly two places. The operators
@>,<@and&&ignore duplicates. When you load the result, reading the set sorts and deduplicates it again. - Anything that depends on order or duplicates throws instead. That covers equality,
Count,GroupBy,Distinct,OrderByand a value inExecuteUpdate. The query throws rather than return a different answer from memory. The package checks this on the translated SQL.
What runs in memory only
These operations give the right answer in memory, but have no SQL form that always gives the same
one. The table says why for each.
In a Where, the query fails to translate. It fails loudly, instead of quietly fetching the whole
table. In a Select, EF Core computes them in memory after it loads the rows.
| Operation | Why |
|---|---|
Intersect, Except, Add on value sets |
PostgreSQL's array type has no intersection, difference or sorted insert |
Length on any range |
the empty range measures 0 where upper(x) - lower(x) yields NULL, and int4range overflows int4 before a cast can widen it |
Values() and Values(step) on a discrete range, Split on any range |
generate_series returns rows, not a scalar |
Shift, Expand on any range |
PostgreSQL has no range arithmetic, and rebuilding the range from lower(x) + d answers differently for the empty range, time past midnight, a daylight-saving change and infinity |
DateRange.Month, Quarter, Year and IsoWeek with an argument read from a column |
not translated yet: PostgreSQL has no calendar-range function. Built from values your code passes in, the range is sent as one constant, so comparing a column against a month runs as SQL |
ToRangeSet() / ToInt32Set() and the other bridge conversions |
arrays and multiranges convert only through unnest and a custom aggregate |
Clamp(value) on any range |
the empty and unbounded shapes have no bound to clamp to |
the value set indexer, set[0] |
canonical order is the CLR comparer's, not the server's |
Everything else on the range, multirange and array surface runs in the database. The EF Core guide puts both halves side by side in What runs where.
Use NodaTime types
For NodaTime types, add
CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime
and call npgsql.UseValueRangesNodaTime() instead.
Documentation
- The EF Core guide shows every translation with the SQL it generates.
- Getting started walks you through a first entity, migration and query.
- The changelog lists what changed in each version.
License
MIT — see LICENSE.
| Product | Versions 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. |
-
net10.0
- CodoMetis.ValueRanges (>= 10.1.0)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 10.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on CodoMetis.ValueRanges.EFCore.PostgreSQL:
| Package | Downloads |
|---|---|
|
CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime
NodaTime support for the CodoMetis.ValueRanges EF Core (Npgsql) plugin: maps LocalDateRange to daterange, LocalDateTimeRange to tsrange, InstantRange to tstzrange and YearMonthRange to a month-aligned daterange (plus their RangeSet<TRange, T> multirange counterparts), bridging through NpgsqlRange<T> via Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime. The full range algebra translates from LINQ to SQL exactly as for the BCL-based types — operators, bound accessors, range_merge, range_agg/range_intersect_agg, factories and multirange operations. v6 also maps the NodaTime value sets to native arrays — LocalDateSet to date[], LocalDateTimeSet to timestamp[], InstantSet to timestamptz[], LocalTimeSet to time[] (no CREATE TYPE needed, unlike timerange) and YearMonthSet to a month-aligned date[] — with the set algebra translating to the array operators (@>, &&, <@, cardinality). Enable with one line: options.UseNpgsql(..., npgsql => npgsql.UseValueRangesNodaTime()) — this implies both UseNodaTime() and UseValueRanges(). |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.1.0 | 205 | 10/19/2026 |
| 10.0.0 | 294 | 10/19/2026 |
| 9.1.0 | 1,574 | 10/17/2026 |
| 9.0.0 | 352 | 10/11/2026 |
| 8.0.0 | 431 | 8/17/2026 |
| 7.0.0 | 210 | 8/17/2026 |
| 6.3.0 | 162 | 8/16/2026 |
| 6.2.1 | 171 | 8/16/2026 |
| 6.2.0 | 242 | 8/15/2026 |
| 6.1.0 | 150 | 8/14/2026 |
| 6.0.0 | 146 | 8/14/2026 |
| 5.0.0 | 141 | 8/13/2026 |
| 4.1.0 | 134 | 8/12/2026 |
| 4.0.0 | 123 | 8/10/2026 |
| 3.1.0 | 159 | 6/17/2026 |
| 3.0.0 | 135 | 6/11/2026 |