Repository navigation
CodoMetis.ValueRanges.Npgsql: a trimmable, Native-AOT-safe bridge for Npgsql without EF Core #47
Description
Decision (maintainer, 2026-10-18)
The bridge to Npgsql gets a package of its own, CodoMetis.ValueRanges.Npgsql. It targets code that uses Npgsql without EF Core, such as hand-written ADO.NET, Dapper or Dapper.AOT, and its purpose is a fully trimmable, Native-AOT-compatible way to store and read every range, multirange and value set. Today the only way to get these types into PostgreSQL is EF Core, whose Native AOT support is experimental.
Why a new package: the issue was deferred from 9.0.0 because choosing the wrong home (core, the EF package, or a new package) costs a major to undo. A new package is the one home that can't be wrong. Core stays free of Npgsql. The EF package already depends on Npgsql, so it can depend on this package. Adding a package is additive, so it doesn't need a major.
Target: a 10.x minor (10.1.0) inside the additions window (#178), merged forward to the 11 line. Additions are frozen for 10.0.0 (#58).
Tier 1: a bridge on public, stable Npgsql API only
Conversions between this library's types and what Npgsql reads and writes natively:
- every range union ↔
NpgsqlRange<T> - every
RangeSet↔NpgsqlRange<T>[](Npgsql's multirange representation) - every value set ↔
T[], with a wrapper arity converting element-wise through its primitive - the custom store types (
timerange,float8range,float4range) by data type name on the parameter
It is AOT-safe with NpgsqlSlimDataSourceBuilder plus EnableRanges(), EnableMultiranges() and EnableArrays(). Measure the NodaTime satellite separately (Npgsql.NodaTime on the slim builder) before promising it.
The hard part is the values, not the conversion. A value arriving from PostgreSQL must enter through the library's factories, and a value written must be the one Npgsql would bind: canonical discrete bounds, the ±infinity ↔ MinValue/MaxValue mapping for dates and timestamps, microsecond truncation toward 2000-01-01, NormalizeElement and wrapper validation, and the finite-only guard for the float family (#153). The EF package already implements all of this, internally, in RangeProviderConversion, StoreTemporal/NodaStoreTemporal, StoreText and the type definitions. Those rules move into this package, and the EF package consumes them. Two copies would drift, which is exactly what the clone sweep exists to catch (#200). Before moving anything, check that the code depends only on Npgsql, not on EF.
What a consumer loses compared with EF is LINQ translation. SQL is written with PostgreSQL's operators. The docs give the operator for each member; test/oracles/postgres-surface.json already holds that mapping.
Tier 2: direct reads and writes, later
reader.GetFieldValue<DateRange>() and new NpgsqlParameter<DateRange>(…) need a type info resolver: IPgTypeInfoResolver / PgConverter<T> in Npgsql.Internal. That API is experimental (DynamicTypeInfoResolver is [Experimental("NPG9001")] and [RequiresDynamicCode]). A resolver over the closed, concrete types avoids the dynamic-code requirement, so it can stay AOT-safe. It still ties the package to one Npgsql major, so the 10.x and 11.x lines each need their own build. Decide after tier 1 has shipped and been used.
Not this issue
#209 is the EF Core side of AOT: compiled models generated with dotnet ef dbcontext optimize --nativeaot need a public static Default on each plugin type mapping. It is independent of this package. This package doesn't need #209, and #209 doesn't give a non-EF path.
Done when (tier 1)
- the package ships beside the other four, discovered by the conventions and packaging tests without edits
- the store rules live in it once, and the EF package uses them (
TemporalStoreParityTestsstill passes) - an integration test round-trips every discovered range, multirange and value set type through raw Npgsql on PostgreSQL 16 and 18, with parity against the EF path
- a Native AOT consumer in
test/consumer-smoke-test.shpublishes and runs a real query through the slim data source builder. That consumer is the only proof of AOT safety: seed a defect there, not in a unit test docs/has the page (plain-docs), with claims inclaims.json
Labels: there is no package/* label for the new package yet. Add one when the package is created, rather than inventing it now.
Activity
CaffeinatedCoder commentedon Oct 17, 2026
Moved off 9.1.0 into the backlog (#83), with no milestone.
release/9.xhas been cut, and 9.1.0 ships with the work that's already finished. Nothing has started on this issue, and it shouldn't hold up the tag. This is an addition, so the 10.0.0 epic (#58) doesn't take it either: that epic is kept to thesilentandbreakingfixes so they aren't held up behind new features.When someone picks it up, it goes under the epic of the release it will ship in and takes that milestone. A 9.x minor would need the change cherry-picked onto
release/9.x.[-]Decide on a public NpgsqlRange<T> bridge[/-][+]CodoMetis.ValueRanges.Npgsql: a trimmable, Native-AOT-safe bridge for Npgsql without EF Core[/+]CaffeinatedCoder commentedon Oct 18, 2026
Rewritten 2026-10-18 from 'decide on a public NpgsqlRange bridge' into the package design, after the maintainer chose a separate package. It stays in the backlog (#83) because no 10.1.0 epic exists yet; it moves under that epic, with its milestone, when 10.1.0 is opened. The EF-side AOT path is #209, which is separate.