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().
Install
| Product | Versions |
|---|---|
| .NET |
README
CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime
NodaTime support for the CodoMetis.ValueRanges EF Core plugin: maps the range and value-set types of CodoMetis.ValueRanges.NodaTime to PostgreSQL range, multirange and array columns, bridging through NpgsqlRange<T> and native arrays via Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.
Ranges and multiranges
| Property type | Column type |
|---|---|
LocalDateRange |
daterange |
RangeSet<LocalDateRange, LocalDate> |
datemultirange |
LocalDateTimeRange |
tsrange |
RangeSet<LocalDateTimeRange, LocalDateTime> |
tsmultirange |
InstantRange |
tstzrange |
RangeSet<InstantRange, Instant> |
tstzmultirange |
YearMonthRange |
daterange (month-aligned) |
RangeSet<YearMonthRange, YearMonth> |
datemultirange (month-aligned) |
Value sets (v6)
| Property type | Column type |
|---|---|
LocalDateSet |
date[] |
LocalDateTimeSet |
timestamp without time zone[] |
InstantSet |
timestamp with time zone[] |
LocalTimeSet |
time without time zone[] |
YearMonthSet |
date[] (month-aligned) |
Usage
dotnet add package CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime
options.UseNpgsql(connectionString, npgsql => npgsql.UseValueRangesNodaTime());
UseValueRangesNodaTime() implies both UseNodaTime() (the Npgsql NodaTime plugin, which maps the element types LocalDate, LocalDateTime and Instant) and UseValueRanges() (the base plugin) β neither needs to be called separately, and the BCL-based range and set types keep working in the same model. It registers the range types and the five value set types; nothing further is configured per property.
Plain LocalDate[]/List<Instant> properties keep their native Npgsql array mapping and coexist in the same model, and scaffolding still produces plain arrays β opting into a set type is a model decision.
The full range algebra translates from LINQ to SQL exactly as documented for the base package β operators (@>, &&, <<, -|-, β¦), bound accessors (lower, upper, lower_inc, upper_inc), Merge (range_merge), the RangeAgg/RangeIntersectAgg aggregates, Intersect/Union/Except, multirange operations including == equality, and the CreateFinite/CreateUnboundedStart/CreateUnboundedEnd factories as guarded range constructor calls:
var day = new LocalDate(2024, 6, 15);
reservations.Where(r => r.Period.Contains(day)); // r."Period" @> DATE '2024-06-15'
reservations.OrderBy(r => r.Period.LowerBound()); // ORDER BY lower(r."Period")
reservations.Where(r => r.Window.Overlaps(other)); // r."Window" && @other
reservations.GroupBy(r => r.CustomerId)
.Select(g => g.Select(r => r.Period).RangeAgg()); // range_agg(r."Period")
The value-set algebra translates to the array operators β @>, &&, <@, cardinality, array_remove, array_cat:
reservations.Where(r => r.Holidays.Contains(day)); // r."Holidays" @> ARRAY['2024-06-15']::date[]
reservations.Where(r => r.Holidays.Overlaps(closures)); // r."Holidays" && @closures
reservations.Where(r => r.Holidays.IsSubsetOf(allowed)); // r."Holidays" <@ @allowed
reservations.Where(r => r.BillingMonths.Count > 2); // cardinality(r."BillingMonths") > 2
reservations.Where(r => r.Holidays.Remove(day).IsEmpty); // cardinality(array_remove(β¦)) = 0
Contains always translates as @> rather than = ANY, so a plain GIN index serves it. Intersect, Except and Add are client-side only β PostgreSQL's array type has no intersection, difference or sorted insert β and fail query translation by design.
Notes
- No normalization rules. The base package documents
DateTimeKindreinterpretation fortsrangeand UTC offset normalization fortstzrange. Neither applies here:LocalDateTimeis wall-clock time by construction andInstantis an instant by construction β the value written is the value stored. - Discrete canonicalization is compensated exactly as for
DateRange:LocalDateRange.UpperBound()translates toupper(x) - 1, so server results always equal the in-memory results (verified against live PostgreSQL). - Calendar normalization reaches bare operands.
LocalDateSet/LocalDateTimeSethold ISO elements, so a probe in another calendar is normalized before it is bound βHolidays.Contains(copticDate)queries the ISO date it denotes, not that calendar's year/month/day read as if they were ISO.YearMonthSetandYearMonthRangereject a non-ISO operand outright, since a non-ISO year-month has no lossless ISO equivalent. The same rule applies in memory (see the core NodaTime README). YearMonthSet(v6) persists first-of-month dates in adate[], exactly asYearMonthRangedoes for its bounds; reads validate alignment and throw on a partial-month date rather than silently shifting it to its month.Unionis the one set operation whose SQL result is not canonical β it translates toarray_cat, which concatenates. Harmless inside the operators above (all duplicate-insensitive) and on materialization (reads re-canonicalize), butCountover a union is refused rather than counting duplicates.Removehas no such caveat:array_removeleaves the array sorted and deduplicated.YearMonthRange(v5) is stored as a month-aligneddaterange:[2025-01, 2025-03]becomes[2025-01-01, 2025-04-01), so no custom database type is needed and every operator works server-side. Bound elements convert through first-of-month dates (Contains(yearMonth)β@> DATE 'yyyy-MM-01');UpperBound()'supper(x) - 1lands on the last day of the end month, which reads back as that month. Reads validate month alignment β a partial-monthdaterangethrows instead of silently shifting. The factories cannot be constructed in SQL from column values (months are coarser than thedatesubtype); constant and parameter ranges work as usual.- Precision: NodaTime carries nanoseconds, PostgreSQL stores microseconds β sub-microsecond precision is reduced at the database boundary.
Instant.MinValue/MaxValuemap to PostgreSQL-infinity/infinityby default (Npgsql rule): a finite bound that happens to be infinite, distinct from an unbounded side (upper_infstaysfalse).- External data sources: when the application builds its own
NpgsqlDataSourceinstead of letting EF create one, callUseNodaTime()on that data source builder as well β the same requirementNpgsql.EntityFrameworkCore.PostgreSQL.NodaTimedocuments. - Store-name lookups: with the Npgsql NodaTime plugin active, resolving a mapping by store type name alone (e.g. scaffolding-style
FindMapping("daterange")) is answered by Npgsql's own plugin (DateInterval). The range types always resolve by CLR type, which is unambiguous. - Reverse engineering (
dotnet ef dbcontext scaffold) does not produce these types β apply them manually after scaffolding, as with the base package.
License
MIT β see LICENSE.
Changes from Previous Version
Adoption Assessment
A verified publisher signal is available. Assess fit from the package's supported frameworks, license and dependency graph rather than popularity alone.
Related Guides & Workflows
Learn how to evaluate, integrate, or audit this package in your .NET project.