Repository navigation
v9.0.0
9.0.0 — 2026-10-11
Wire-format corrections, and the machinery that proves them. It started from three gaps measured
by wrapping a DateRange in a downstream value object and putting it on an ASP.NET Core API —
JSON with nothing registered, dictionary keys, OpenAPI — and a value-semantics review of the result
found more of the same kind, each one answering wrongly without throwing. Nothing was removed or
resignatured — package validation passes against the 8.0.0 baseline.
This is a major for the reason 7.0.0 and 8.0.0 were: several existing calls now answer
differently, and a caller cannot discover that from a compile error. A floating 8.* reference
must not pick any of these up silently.
What can break you, in order of likelihood:
JsonSerializer.Serialize(range)with default options writes the literal, not
{"Length":181}. The old form could not be read back by anything, this package included, but a
consumer that only ever wrote it — into a response, a document, a log — sees its shape change.- A culture provider no longer reaches the literal.
Parse(s, culture)reads the invariant
literal, and formatting without a format string writes it. Code that parsed culture-formatted
text throughParse(s, de-DE)deliberately must parse the bounds itself. - Temporal literals are strict ISO 8601.
[01.02.2027,05.03.2027)used to parse — month
first, as January 2 to May 2 — and aDateTimeOffsetwithout an offset took the parsing
machine's. Dotted, slashed and named-month dates, date-only text for a date-time and an
offset-lessDateTimeOffsetare now aFormatException;HH:mmand PostgreSQL's own output
still parse. - Lenient decimal forms are refused.
"1,000","(5)"and currency symbols in a
DecimalRangeorDecimalSetliteral are aFormatException, as they are in PostgreSQL. - Interpolating an integer or decimal range is invariant under every culture. Only code that
depended on[1,5,2)— which parsed back as a different range — is affected. - The EF Core provider writes temporal values as text at the database's resolution. A SQL
constant now carries the microsecond Npgsql would bind, so a query that compared a constant
against its own row and missed now matches; and a temporal set whose elements are less than a
microsecond apart is stored with one of them, as the database could only ever hold one. - EF Core queries composing on a server-computed value set
Unionthrow where they answered
wrongly — equality in a projection or on a projected union, a conditional or??around one,
GroupBy,Distinct,OrderBy, set operations,Countthrough??, andExecuteUpdateto a
union. Each returned rows that disagreed with memory, or stored a non-canonical array. - The literal grammar follows PostgreSQL's. A quoted-empty or whitespace-only range bound is
refused instead of read as unbounded; a backslash escapes in unquoted array elements and range
bounds ({a\,b}is one element);{{a,b}},{a}b}and{a"b}are refused; only ASCII
whitespace is skipped. An empty format string writes the literal, asnulldoes. - A
DateTimeliteral with an offset keeps its wall clock.2026-01-01T09:30+05:30is 09:30
(Local) on every machine; it was converted into the parsing machine's zone. - JSON precedence and coverage changed in three places. A union converter registered on the
options now reaches values serialized throughobjector as a variant; a property declared as
a range or set interface writes the literal (it wrote{}); and a NodaTime date in a non-ISO
calendar — bare, or as a wrapper-set element — is refused, where it was written thirteen days
off.
Everything else is additive: a TypeConverter on every type, ranges as dictionary keys, ranges
and sets inside EF Core JSON columns, Native AOT, Deconstruct on every bounded variant, and an
OpenAPI transformer to copy. The
migration guide has the call-site edits.
Changed
-
⚠️ Every range, variant, multirange and value set serializes as its literal with nothing
registered. The converters were opt-in (AddRangeConverters()), and System.Text.Json's fallback
for a type with no converter is a property dump:DateRangewrote{"Length":181}, aRangeSet
[{"Length":5}], aStringSet{"Values":["a","b"],"Count":2,"IsEmpty":false}, and an
object-typed range every public property of its variant. None of them read back — the abstract
union threwNotSupportedException, the restJsonException— and nothing threw on the way
out, so an ASP.NET Core host that forgot the registration wrote wrong responses and refused every
request body with a 400. Each type now carries[JsonConverter], so"[2026-01-01,2026-06-30]",
"{[1,5],[7,10]}"and["a","b"]are the wire form by default.- System.Text.Json does not inherit
[JsonConverter], so each sealed variant carries its own —
a value reached throughobjectis a variant, and an attribute on the union alone would have
left it on the property dump. - The attribute is the lowest-precedence route: a converter registered on the options or on a
property still wins.AddRangeConverters()stays valid and is now redundant. - Source-generated
JsonSerializerContexts honour it too. Unions, variants and the non-generic
sets name a closed converter type, which the generator instantiates directly;RangeSetand the
wrapper arities nameRangeJsonConverterFactory, since an attribute cannot name an open generic
converter. A context serializing a value set must also list the element type — the set
converter resolves the element's contract through the options. - Payload change for anyone who wrote the old form: a response or document that carried
{"Length":…}now carries the literal.
- System.Text.Json does not inherit
-
⚠️ Temporal bounds and elements parse as strict ISO 8601.DateRange,DateTimeRange,
DateTimeOffsetRange,TimeRange,DateSet,DateTimeSet,DateTimeOffsetSet,TimeSetand
the temporal wrapper arities read element text with the lenient BCLParse, which reads the
invariant culture month first:[01.02.2027,05.03.2027)was January 2 to May 2,9:30 AMwas
09:30, and aDateTimeOffsetwithout an offset took whichever machine parsed it — all silently.
They now accept exactly what this library writes and what PostgreSQL prints in its ISO
DateStyle:yyyy-MM-dd;HH:mmorHH:mm:sswith any number of fraction digits, truncated to
100 ns;Tor a space between them; for aDateTimean optionalZ(Utc) or±HH,
±HH:mmor±HH:mm:ss(Local) with the wall clock kept as written — the offset is no longer
applied: it was converted into the parsing machine's zone, so2026-01-01T09:30+05:30was
05:00 in Berlin and 04:00 on a UTC server, and atsrangestores the wall clock; PostgreSQL's own
timestampinput drops the offset the same way, and a local value now round-trips exactly, even
across a DST gap — for a
DateTimeOffseta requiredZ,±HH,±HH:mmor±HH:mm:ss(a historical local-mean-time
offset, kept as the instant at offset zero); andinfinity/-infinityasMaxValue/
MinValue, what Npgsql materializes for them. Everything else is aFormatExceptionthrough
Parse,TryParse, JSON, theTypeConverterand configuration binding. The wrapper arities —
BCL and NodaTime — read the text as their primitive first and hand the element its round-trip
form, so a lenient generated elementParsenever sees non-ISO text, and an element that
ignores the format specifier fails on read instead of truncating silently. The NodaTime types
were already strict; they now also readinfinityforInstant,LocalDateand
LocalDateTime, the types Npgsql maps it for. A closed temporal set reads its JSON elements with
the same grammar — System.Text.Json's own converter read an offset-lessDateTimeOffset, or a
bare date, in the parsing machine's offset — unless a converter is registered for the element
type, which still wins; the written form is System.Text.Json's, unchanged.TemporalLiteralGrammarTestspins the
grammar;TemporalLiteralStrictnessContractTests(conventions) refuses a non-ISO element through
every entry point of all 26 temporal range and set types. -
The range interfaces are declared not to be an extension point.
IRange<T>, the shape
interfaces andIRangeFactory<TRange, T>are the shared vocabulary of the shipped range types;
implementing them outside the library is unsupported, and they may gain members in a minor
version. Nothing changes for code that consumes them. The XML docs anddocs/architecture.md
state the policy.
Fixed
-
⚠️ Interpolating aDecimalRangeunder a comma-decimal culture produced a different range.
Int32Range,Int64RangeandDecimalRangehave noFormatValueoverride, and the default
formatted anullprovider — what string interpolation,string.Formatand logging renderers
pass — in the current culture. Underde-DE,$"{DecimalRange [1.5,2)}"was[1,5,2), which
the parser reads back as[1,52), silently;[1.5,2.25)came back as[1,5225). Under
sv-SEa negative bound took U+2212 as its minus sign and no longer parsed. Anullprovider
now means the invariant culture in both defaultFormatValues, as every override already did.
ToString(), JSON, theTypeConverterand the EF bridge were never affected — they pass the
invariant culture explicitly.LiteralCultureContractTests(conventions) formats every shape,
multirange and set family underde-DE,sv-SE,fa-IRandth-TH. -
⚠️ Decimal bounds and elements parse with PostgreSQL's numeric grammar.DecimalRangeand
DecimalSetparsed withNumberStyles.Any, which also accepts group separators, currency
symbols and parenthesised negatives:["1,000",2000]read as[1000,2000],"(5)"as −5 —
literals PostgreSQL rejects. It is also what let5,2read as 52 above, and what made an
explicit comma-decimal provider misread silently:DecimalRange.Parse("[1.5,2.5)", de-DE)was
[15,25). They now parse withNumberStyles.Float— sign, digits, point, exponent — and those
inputs are aFormatException. Integer types already usedNumberStyles.Integer. If you
relied on the lenient forms, they now fail. The wrapper arities are covered too:DecimalSet<T>
and the integer arities handed the raw element text to the element's ownParse, and a generated
one (decimal.Parse(s, provider),NumberStyles.Number) accepts group separators, so
DecimalSet<Price>.Parse("{\"1,5\"}")was{15}— throughTryParse, configuration binding and
a JSON string too — whileDecimalSetrefused it. They now parse with the family's grammar first
and hand the element its invariant text, so its validation still runs and scale is kept; the
decimal JSON writer and the EF Core bridge read element text the same way.
WrapperGrammarContractTests(conventions) holds every wrapper family to its primitive sibling. -
⚠️ An empty format string wrote a culture-formatted literal. .NET reads""as the general
format, exactly likenull, but the literal checked only fornull:ToString("", de-DE)on
DecimalRange [1.5,2)wrote[1,5,2), aDateRangewith("", th-TH)Buddhist-era short
dates. An empty format now writes the invariant literal everywherenulldoes.
LiteralProviderContractTestsandLiteralCultureContractTestssweep""alongsidenull. -
⚠️ A quoted empty range bound read as an unbounded one.Int32Range.Parse("[\"\",5)")was
(,4]and("","")was(,), silently, where PostgreSQL answersinvalid input syntax; a
whitespace-only bound ([ ,5)) was trimmed to nothing the same way. Bounds now follow PostgreSQL's
range_in: only an absent bound is infinite, quotes may wrap any part of a bound, and a backslash
escapes the next character inside quotes or out. Applies to every range type and multirange. -
⚠️ Array literals departed from PostgreSQL'sarray_in.StringSet.Parse(@"{a\,b}")was two
elements,a\andb, where PostgreSQL reads the one elementa,b;{{a,b}}was the set
{"{a","b}"}where PostgreSQL reads a two-dimensional array;{a}b}was{"a}b"}where
PostgreSQL errors. Unquoted elements now honour backslash escapes, an unescaped{,}or"
inside one is aFormatException, and whitespace inside an element is kept. Both literal parsers
skip only the ASCII whitespace PostgreSQL skips.LiteralGrammarParityTests(integration) asks a
live server for the same answer. -
The NodaTime types refused
HH:mm.TimeRange.Parse("[09:00,17:00)")parsed but
LocalTimeSet.Parse("{09:00}")threw.HH:mmis now accepted wherever the BCL types accept it —
aLocalTime, the time part of aLocalDateTime, anInstantwith its offset orZ. -
⚠️ A converter registered on the options for a range union reached union-typed declarations
only. System.Text.Json resolves a converter by the exact type, and a range reached through
objector declared as a variant is handed over as its variant, whose own[JsonConverter]
wrote the literal — so with a customInt32Rangeconverter,Serialize(range)wrote the custom
form andSerialize<object>(range)wrote"[1,2]". The variant converter now hands values and
dictionary keys, on both legs, to whatever System.Text.Json would choose for the union from
Converters, unless that is this package's own; a read goes through the union and narrows.
Payload change for anyone who registered a union converter and serialized ranges through
object. -
⚠️ A property declared as a range or set interface was written as a property dump.
IRange<int>wrote{}andIValueSet<string>{"Values":["a"]}, with no error, and neither
read back. The interfaces now carryInterfaceDeclaredJsonConverter, which writes through the
runtime type's contract (the literal, or a converter registered for that type) and refuses a read
withNotSupportedExceptionnaming the remedy: declare the concrete type. -
⚠️ NodaTime:AddNodaTimeRangeConverters()shadowedConfigureForNodaTime, and wrote bare
non-ISO values in their own calendar's fields. Registered first, its element converters won for
all five NodaTime types, contrary to the documented "either order"; and a Julian
LocalDate(2024, 1, 1)— ISO 2024-01-14 — was written"2024-01-01"and read back thirteen days
off. Any other converter registered for the type now wins wherever it sits, and a bare
LocalDate,LocalDateTimeorYearMonthin a non-ISO calendar is refused on write with
ArgumentException, as NodaTime's own converter refuses it. Convert with
WithCalendar(CalendarSystem.Iso)first. -
⚠️ NodaTime wrapper sets stored an element in another calendar as a different date.
NodaTime's ISO patterns format a value in its own calendar's fields, so aLocalDateSet<T>over a
JulianLocalDate(2024, 1, 1)— ISO 2024-01-14 — wrote{2024-01-01}and["2024-01-01"], and
the EF Core bridge bound, inlined and stored ISO 2024-01-01: thirteen days off, with no error and
an unequal round trip.LocalDateTimeSet<T>andYearMonthSet<T>likewise; against live
PostgreSQL a Julian-January parameter matched a row holding ISO January. The closed sets convert
such a value to ISO; a wrapper arity can only build its element by parsing, so it now refuses
it withArgumentException— inFrom, collection expressions and reads, and as the probe of
Contains/Add/Remove— and the EF Core bridge refuses one as a bare operand. If you wrapped
non-ISO values, convert them withWithCalendar(CalendarSystem.Iso)first. -
⚠️ A culture provider no longer changes the literal.Parse(s, provider)handed the provider
to the element parser, andToString(null, provider)to the element formatter.th-THuses the
Buddhist calendar, soDateRange.Parse("[2026-01-01,2026-06-30]", th-TH)returned
[1483-01-01,1483-06-30]andToString(null, th-TH)wrote2569;ar-SAread dates as
Hijri;de-DEread the decimal1.5as fifteen. Nothing threw. The literal is PostgreSQL's
text and has no culture, so:Parse/TryParse(string and span, every range,RangeSetand value set) read the invariant
literal and ignore the provider, asGuid.Parsedoes.- Without a format string, formatting writes the invariant literal whatever the provider. A
provider still applies alongside an explicit format (ToString("yyyy-MM-dd", th-TH)), which
asks for display text. - The library's own paths — JSON, the
TypeConverter, EF Core — always passed the invariant
culture and are unaffected, as is ASP.NET Core model binding, which passes it too.
LiteralProviderContractTests(conventions) parses and formats every shape, multirange and set
family underde-DE,sv-SE,fa-IR,th-THandar-SA. If you parsed culture-formatted
text throughParse(s, culture)deliberately, parse the elements yourself and build the range
with the factories.
-
Ranges, multiranges and value sets inside an EF Core JSON column (
ToJson()) could not be
saved — the plugin's type mappings had noJsonValueReaderWriter, andSaveChangesthrew a
bareNullReferenceExceptionfrom inside EF. Each mapping now writes the store type's text,
which also makes queries into the document translate:
CAST(doc ->> 'Period' AS daterange) @> @day.- The document holds the store's representation, not the API JSON, and it is a persisted
format: a value set is"{a,b}"(an array in API JSON), theYearMonthtypes — wrapper
arity included — their month-aligned dates, since that is what the cast reads. - Temporal values are stored as a parameter would bind them: truncated to the microsecond
toward 2000-01-01 as Npgsql truncates, the extremes Npgsql maps as-infinity/infinity.
Writing every digit and letting the server round them half-to-even made the document hold a
different value from the column, soDoc.Period == periodcould miss the row it was saved
from andContainsdisagreed at the bounds. Materializing from the document now gives what
materializing from a native column gives. ExecuteUpdateof a value set inside the document is refused: EF Core writes it with
to_jsonb, storing a JSON array the document cannot hold, after which the row no longer loads.
The refusal reads the translatedUPDATE, so a join orEF.Propertycannot reach past it.
Ranges, multiranges, a whole complex property holding a set, and the whole JSON property can be
set.SaveChanges(complex and owned, nested, null and empty kept apart) and the operators inside
the document were checked against a live server,ExecuteUpdatetonullfails in Npgsql for
any type, and EF Core refusesExecuteUpdateon owned JSON types —docs/efcore.mdhas the
matrix.
JsonColumnTests(integration) round-trips and queries every type inside one jsonb column;
TemporalStoreParityTestsholds the temporal rule for 18 types and seven probes (remainders
above, below and at 500 ns, before 2000, the extremes, sub-microsecond ranges);
ExecuteUpdateJsonTeststhe update paths.MappingParityTestsrequires a reader/writer on every
mapping. - The document holds the store's representation, not the API JSON, and it is a persisted
-
⚠️ Temporal SQL constants carried more precision than the database, and set arrays duplicates.
The EF Core provider rendered a temporal range, multirange or set constant with seven fraction
digits (nine for NodaTime), which the server rounds half-to-even, while a parameter is truncated
toward 2000-01-01 — so a constant could disagree with a parameter for the same value, and EF 10
inlines evaluatable expressions as constants withoutEF.Constant.DateTime.MinValuerendered
as a finite date where a parameter carries-infinity. Constants now carry the value Npgsql
would bind — a set's element constant too, which forInstantSetandLocalTimeSetwas Npgsql's
own nine-digit literal, soset.Contains(EF.Constant(x))missed the elementxwas stored as.
And a temporal set whose elements truncate to one microsecond was bound with both,
so the stored array'scardinalitydisagreed with the set'sCount; it is now deduplicated on
every path. A range's element operand (range.Contains(x), a constructor argument) has the
range's own mapping now too: a constant is the stored value rather than Npgsql's literal, and a
parameter is normalized like the bounds —DateTimeOffsetRange.Contains(dto)threw "only offset
0 (UTC) is supported" for any non-UTC offset, andDateTimeRange.Containslikewise for a
UTC-kindedDateTime. -
A range built inline in a query failed to translate.
r.Period == DateRange.CreateFinite(new DateOnly(2026, 1, 1), new DateOnly(2026, 1, 31))— orInt32Range.Parse("[1,5]", null), or any
factory call over values that depend on nothing in the query — threw "does not have a type mapping
assigned" under==,!=,Equalsand inside a conditional. EF Core evaluates such a call on the
client and inlines the result as a constant typed by its sealed variant, for which no mapping
exists. The query interceptor now retypes it to its range type; the literal goes through the
store text like any other constant.Contains,Overlaps, captured variables andEF.Constant
were never affected.InlineRangeConstantExecutionTests(integration) runs one per range type. -
⚠️ A server-computed value setUnionis refused wherever it would answer wrongly (EF Core).
Uniontranslates toarray_cat, which concatenates; 8.0.0's LINQ-level refusal caught only
==/Equalsin predicates andCountover a bare union, and its claim that a projection is
client-evaluated was false. Against live PostgreSQL, with{a,c} ∪ {a,b}and target{a,b,c}: a
projected comparison answeredfalse, a ternary or??around the union matched 0 rows,
GroupBymade 2 groups where memory makes 1,Distinct().Count()said 2, andExecuteUpdate
stored{a,c,a,b}. The refusal now runs on the final SQL tree, through a decorator around the
provider'sIRelationalParameterBasedSqlProcessorFactory, and follows a union through
array_remove,CASE,COALESCEand subquery columns. It may still feed@>,<@,&&,
Remove,IsEmpty, or the outermost projection, which materializes canonical.
UnionCanonicalityTests(integration) executes every shape and the legal compositions. -
A NodaTime range's element operand in another calendar was compared by its own fields (EF
Core).LocalDateRangeandLocalDateTimeRangepassed no ISO normalization for an operand, so
Contains(EF.Constant(julianDate))rendered the Julian year, month and day as if they were ISO
and missed its row, and aLocalDateparameter threw inside Npgsql. The operand is now converted
to ISO, as the bounds are and as the NodaTime sets already did.
Added
-
A
TypeConverteron every range, variant, multirange and value set. None had one, so
TypeDescriptorcould not convert them from a string, andMicrosoft.Extensions.Configuration
binding of"Period": "[2026-01-01,2026-06-30]"into aDateRangeproperty producednull
with no error. Each type now converts to and from its literal (RangeTypeConverter,
RangeVariantTypeConverter,RangeSetTypeConverter,ValueSetTypeConverter, and
LiteralTypeConverterfor the generic types).- Always invariant: the culture argument is ignored, since the literal is a wire format.
- An empty string converts to
null(absent), never to the empty range, which isempty. - Unlike
[JsonConverter],[TypeConverter]is inherited, so without their own a variant
would have silently used the union's and returned whatever shape the literal had. Each variant
carries a narrowing converter that refuses another shape. - ASP.NET Core model binding is unaffected — minimal APIs and MVC bind through the static
TryParsethese types already have.
-
Native AOT and trimming.
RangeSetand the validated-wrapper sets failed under Native AOT
with "missing native code": their JSON converters andTypeConverters were built with
MakeGenericTypeover instantiations the AOT compiler never generated, and whether one existed
depended on unrelated code elsewhere in the program. Each generic type now registers its own
closed converters, which are compiled with it, so even a type a program only deserializes works.
RangeJsonConverterFactoryandLiteralTypeConverterno longer useMakeGenericTypeor walk
interfaces,ValueSetJsonConverterresolves elements throughJsonTypeInfo<T>, and both
libraries build withIsAotCompatible— no trim or AOT warnings in a consumer's publish. The
consumer smoke test gained a NativeAOT consumer that publishes and runs a native binary; seeding
outRangeSet's own registration fails exactly the range set cases there. The core and NodaTime
READMEs now say so, and so do their NuGet tags (native-aot,trimming);PackagingConventionTests
holds both claims to the project'sIsAotCompatiblein both directions. The EF Core packages make
no such claim. A source-generated context must list a value set's element type as well as the
set; leaving it out is aNotSupportedExceptionnaming the element, even for the families that
bring their own element converter (SetSourceGenerationTestspins both halves, and the NativeAOT
consumer runs the documented context for a primitive set and a range throughobject). -
Ranges and multiranges as dictionary keys.
Dictionary<DateRange, int>threw
NotSupportedExceptionin both directions, registered or not.RangeJsonConverter,
RangeVariantJsonConverterandRangeSetJsonConverternow override
ReadAsPropertyName/WriteAsPropertyName, writing the same literal as the value form
({"[2026-01-01,2026-06-30]":181}). Keys canonicalize on read like values, and a malformed or
wrong-shape key is the sameJsonException.DictionaryKeyPolicyis not applied — a naming
policy would rewrite the literal into text that does not parse. Value sets are not keys: their
value form is a JSON array, not a string. Because keys canonicalize, two spellings of one key —
"[1,5]"and"[1,6)"— collide: by default the later value silently wins, as for any duplicate
key, while .NET 10'sAllowDuplicateProperties = falserefuses the collision, since it checks the
parsed key.docs/serialization.mdrecommends the latter wherever a dropped entry matters. -
Deconstructon every bounded variant. The positional variants had one from the compiler, but
everyFiniteis written by hand — its constructor is internal, so the factories stay the only
way to build one — and so are the unbounded variants ofLocalDateRange,LocalDateTimeRange
andYearMonthRange, whose constructors normalize the calendar.var (start, end) = finite
compiled on no range type. Each now deconstructs into exactly what its constructor takes, as a
positional record would:(start, end)on a discrete type. Both bounds are inclusive — the canonical form — so they pass
straight back toCreateFinite, whose default there is closed.(start, end, startInclusive, endInclusive)on a continuous type, deliberately with no
two-value form: passed back toCreateFinite,(1.5,2]would come out as its half-open
default[1.5,2).
VariantDeconstructContractTests(conventions) holds every bounded variant of every discovered
range type to its constructor's parameters, and checks the values written against the properties
under both inclusiveness settings. -
Docs: an OpenAPI schema transformer. ASP.NET Core's generator describes a range, multirange
or value set body as{}— it cannot see through a custom converter, and .NET 10 has no public
hook for a converter to declare its schema.docs/serialization.md#openapicarries a
IOpenApiSchemaTransformerto copy into an API project: ranges and multiranges astype: string
with a literal pattern, value sets as arrays of unique elements. It ships in no package; the
repository compiles and tests it againstMicrosoft.AspNetCore.OpenApi10 and holds the doc to
the tested copy verbatim. Query and route parameters were alreadytype: string. -
Tests:
TypeConverterContractTests(conventions) converts every shape through the union's and
the variant's converter underde-DE, and every set family. A new
CodoMetis.ValueRanges.AspNetCore.Testsproject runs configuration binding and a real ASP.NET
Core host (MVC and minimal APIs) with no ValueRanges registration.JsonDefaultContractTests(conventions) serializes every shape of every discovered
range type — through the union, throughobject, inside aRangeSet, and as a dictionary key by
union, variant and multirange — and every value set family in both arities, with nothing
registered, against the registered output. The consumer smoke test asserts the unregistered wire
form against the packed package.
Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.