CodoMetis.ValueRanges.EFCore.PostgreSQL
8.0.0
Prefix Reserved
dotnet add package CodoMetis.ValueRanges.EFCore.PostgreSQL --version 8.0.0
NuGet\Install-Package CodoMetis.ValueRanges.EFCore.PostgreSQL -Version 8.0.0
<PackageReference Include="CodoMetis.ValueRanges.EFCore.PostgreSQL" Version="8.0.0" />
<PackageVersion Include="CodoMetis.ValueRanges.EFCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="CodoMetis.ValueRanges.EFCore.PostgreSQL" />
paket add CodoMetis.ValueRanges.EFCore.PostgreSQL --version 8.0.0
#r "nuget: CodoMetis.ValueRanges.EFCore.PostgreSQL, 8.0.0"
#:package CodoMetis.ValueRanges.EFCore.PostgreSQL@8.0.0
#addin nuget:?package=CodoMetis.ValueRanges.EFCore.PostgreSQL&version=8.0.0
#tool nuget:?package=CodoMetis.ValueRanges.EFCore.PostgreSQL&version=8.0.0
CodoMetis.ValueRanges.EFCore.PostgreSQL
Entity Framework Core (Npgsql) plugin for
CodoMetis.ValueRanges: maps the range types
to PostgreSQL range columns, RangeSet<TRange, T> to multirange columns and the value set types to
native array columns — then translates the full algebra from LINQ to SQL, so the same code gives
identical results in memory and against a live database.
dotnet add package CodoMetis.ValueRanges.EFCore.PostgreSQL
Enable it with one line — no value converters, comparers or column types to configure:
options.UseNpgsql(connectionString, npgsql => npgsql.UseValueRanges());
Mapped by convention
| Property type | Column type |
|---|---|
Int32Range |
int4range |
RangeSet<Int32Range, int> |
int4multirange |
DateRange |
daterange |
RangeSet<DateRange, DateOnly> |
datemultirange |
TimeRange |
timerange (custom type — see below) |
StringSet |
text[] |
GuidSet |
uuid[] |
| … and so on for every type |
Wrapper set instantiations like StringSet<AccessRight> are recognized automatically from the open
generic — there is no per-element registration to forget. Plain string[]/List<Guid> properties
keep their native Npgsql array mapping and coexist in the same model.
Range algebra
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.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());
Contains, Overlaps, IsContainedBy, IsStrictlyLeftOf/RightOf,
DoesNotExtendLeftOf/RightOf and IsAdjacentTo map to @>, &&, <@, <<, >>, &<, &>
and -|- — on ranges and on RangeSet with range or multirange operands. Intersect maps to *;
Union and Except lift both operands to multiranges (+/-), matching their RangeSet return
type, so a disjoint union is a real two-element multirange rather than an error. The
CreateFinite/CreateUnboundedStart/CreateUnboundedEnd factories translate to guarded range
constructor calls carrying the model's inverted-bounds-yield-empty semantics.
State checks translate directly: IsEmpty() → isempty, IsUnboundedStart() → lower_inf,
IsUnboundedEnd() → upper_inf, IsInfinity() → lower_inf AND upper_inf, IsFinite() → the
negation of both plus NOT isempty.
Value set algebra
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 translates as @> rather than = ANY, so a plain GIN index serves it.
What does not translate
These compute the right answer in memory, fail translation in a Where — loudly, rather than
silently fetching the table — and fall back to client evaluation in a Select:
| 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() on a discrete range |
generate_series returns rows, not a scalar |
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 translates. The EF Core guide tabulates both halves together.
Notes
- Discrete canonicalization is compensated. PostgreSQL canonicalizes
int4range,int8rangeanddaterangeto half-open[lower, upper)while the model canonicalizes to closed[lower, upper], soUpperBound()translates toupper(x) - 1andUpperBoundInclusive()toNOT upper_inf(x) AND NOT isempty(x). Server results always equal in-memory results, verified against live PostgreSQL. LowerBound()/UpperBound()returnT?because PostgreSQL'slower/upperreturnNULLfor an unbounded or empty operand. The in-memory implementation matches.- Aggregates return
NULLin SQL for zero input rows (standard PostgreSQL behaviour), while the in-memoryRangeAgg()returns the empty set.RangeIntersectAgg()returnsnullin both. - Timestamps.
DateTimeRangebounds are written astimestampwithDateTimeKind.Unspecified— a UTC-kindedDateTimeis reinterpreted as wall-clock time, not converted.DateTimeOffsetRangebounds are normalized to UTC fortimestamptz: the instant is preserved, the original offset is not round-tripped. DateTime.MinValue/MaxValuemap to PostgreSQL-infinity/infinityby Npgsql's default rule — a finite bound that happens to be infinite, still distinct from an unbounded side (upper_infstaysfalse).TimeRangeneeds two opt-ins, because PostgreSQL has no built-intimerange:HasPostgresRangeto create the type andEnableUnmappedTypeson the data source.Unionis the one set operation whose SQL result is not canonical — it translates toarray_cat, which concatenates without deduplicating. Harmless inside the duplicate-insensitive operators and on materialization (reads re-canonicalize), butCountover a union is refused rather than counting duplicates.- Sets are mapped as scalars with plugin-owned mappings and translators, never through EF's primitive-collection machinery.
- Reverse engineering (
dotnet ef dbcontext scaffold) maps range columns toNpgsqlRange<T>and array columns to plain arrays, not to these types — the plugin provides no design-time services. Apply the types manually after scaffolding; opting into them is a model decision.
For NodaTime types, add
CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime
and call npgsql.UseValueRangesNodaTime() instead.
Documentation
The EF Core guide documents every translation with its generated SQL, and Getting started walks through a first entity, migration and query. See also the changelog.
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 (>= 8.0.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.