Cette page n'est pas encore traduite. La version anglaise est affichée ci-dessous.

FoundationDB .NET client 7.4.6

Released on 2026-09-XX.

In this release: CrystalJson serializes .NET 11 union types and formats and parses dates two to five times faster with every Instant round-tripping to the nanosecond, CrystalXml serializes the data-contract types of a referenced assembly, the emulator applies the cluster's 5 second history window, and the native client ships as one package per FoundationDB branch.

Highlights

SnowBank:

FoundationDB:

Dependencies:

Breaking changes lists the changes that can fail an existing test or restore.

SnowBank

CrystalJson serializes .NET 11 union types (work in progress)

Union support is a work in progress. This release covers the write side: a union serializes as its active case. Reading a union back is not implemented yet, and the behavior described here may change when the read side lands.

If you declared a C# union (a .NET 11 type marked [Union] that implements IUnion, which is what the union keyword produces) and serialized it through CrystalJson, the output was the union struct serialized from its public members, as for any struct the serializer has no rule for. In 7.4.6 a union serializes as its active case value, with no envelope and no type discriminator; this matches what System.Text.Json writes. The none state (Value is null) writes JSON null. The reflection path and generated converters write the same output, and a generated converter dispatches without boxing when every case has a TryGetValue(out TCase) method.

public sealed record Cat(string Name);

public sealed record Dog(string Name);

public union Pet(Cat, Dog);

public sealed record Owner(string Name, Pet Pet);

[CrystalJsonConverter]
[CrystalSerializable(typeof(Pet))]
[CrystalSerializable(typeof(Owner))]
public static partial class AcmeSerializers { }

CrystalJson.Serialize(new Pet(new Cat("Felix")));
// => { "Name": "Felix" }
CrystalJson.Serialize(new Pet(new Dog("Rex")));
// => { "Name": "Rex" }
CrystalJson.Serialize(default(Pet));
// => null
AcmeSerializers.Owner.ToJsonText(new Owner("Alice", new Pet(new Cat("Felix"))));
// => { "Name": "Alice", "Pet": { "Name": "Felix" } }

The union types are detected by the attribute and interface names, so the net8.0 and net10.0 builds of the library include the code, but only a net11.0 application can declare a union.

Reading a union is not part of this release, and the output above has no discriminator to read it back from. CrystalJson.Deserialize<Pet>("""{ "Name": "Felix" }""") returns a Pet whose Value is null (the none state), with no error, and a type with a union member reads the member the same way, on either path. For a union with scalar cases, such as union IntOrString(int, string), the read throws JsonBindingException ("Cannot convert JSON Number to type 'IntOrString'"). Do not round-trip a union through JSON until the read side ships.

CrystalXml serializes a [DataMember] whose setter is not public

If a [CrystalXmlOutput] container with the DataContractCompat format registered a [DataContract] type from a referenced assembly, and the build failed with CXML0013 on a [DataMember] property that has a private or internal setter, this is fixed. Such a property is the usual shape of a data-transfer type: DataContractSerializer writes it and reads it through the non-public setter, so the rejection was wrong for the write-only XML format.

The cause: when the type comes from a referenced assembly, the compiler imports its metadata without non-public members, so the property looked like a get-only one to the generator, and a get-only [DataMember] on a [DataContract] type is what CXML0013 rejects. The generator now serializes such a property from its getter, which is all the XML write path needs. The same shape declared in the same assembly was already accepted, and a fully private member stays invisible and is not serialized.

With Invoice in a referenced Acme.Contracts assembly:

[DataContract]
public sealed class Invoice
{
	public Invoice(string number, decimal total)
	{
		this.Number = number;
		this.Total = total;
	}

	[DataMember]
	public string Number { get; private set; }

	[DataMember]
	public decimal Total { get; internal set; }
}

The consuming assembly registers it:

[CrystalConverter]
[CrystalXmlOutput(CrystalXmlSerializerDefaults.DataContractCompat)]
[CrystalSerializable(typeof(Acme.Contracts.Invoice))]
public static partial class AcmeXmlSerializers { }

AcmeXmlSerializers.Invoice.ToXmlText(new Invoice("INV-2026-0042", 129.90m));
// 7.4.5: error CXML0013 on Number and Total
// 7.4.6: <Invoice xmlns="http://schemas.datacontract.org/2004/07/Acme.Contracts"><Number>INV-2026-0042</Number><Total>129.90</Total></Invoice>

Types without a parameterless constructor deserialize through their constructor

If you registered a positional record such as record Toy(string Name, int Size = 3) in a generated container, the build failed inside the generated file with CS7036 ("There is no argument given that corresponds to the required parameter 'Name'"), and CrystalJson.Deserialize<Toy> threw JsonBindingException ("Failed to construct a new instance of type 'Toy'"). Both paths only called a parameterless constructor. The workaround was to declare the members as init properties.

In 7.4.6 both paths bind a constructor, with the rule System.Text.Json applies. A type with a parameterless constructor is still built with it, and nothing changes for it. Otherwise the constructor is the one marked [JsonConstructor] (the System.Text.Json attribute, matched by name), else the single public constructor whose every parameter matches a serialized member by name (case-insensitive) and type. Each parameter receives the value of its member. When the member is absent from the document, the parameter's own default value applies (Size = 3), else the member's default. A member that no parameter covers is assigned after construction, as before. Serialization is unchanged.

public sealed record Toy(string Name, int Size = 3);

public sealed class Bowl
{
	public Bowl(string material) { this.Material = material; }
	public string Material { get; }
	public int Capacity { get; set; }
}

[CrystalJsonConverter]
[CrystalSerializable(typeof(Toy))]
[CrystalSerializable(typeof(Bowl))]
public static partial class AcmeSerializers { }

CrystalJson.Deserialize<Toy>("""{ "Name": "ball" }""");
// => Toy { Name = ball, Size = 3 }
AcmeSerializers.Toy.Unpack(JsonObject.Parse("""{ "Name": "ball", "Size": 5 }"""));
// => Toy { Name = ball, Size = 5 }
AcmeSerializers.Bowl.Unpack(JsonObject.Parse("""{ "Material": "steel", "Capacity": 2 }"""));
// => Material bound through the constructor, Capacity assigned after it

When no constructor can be called, the generator reports CJSON0027 (an error) on the type instead of emitting code that does not compile: two constructors match and none is marked, [JsonConstructor] is on two of them, or a parameter matches no member. The reflection path throws JsonBindingException with the same reason at bind time, so a type that is only serialized is not affected.

Generator diagnostics respect #pragma warning and .editorconfig

The diagnostics of the JSON source generator (CJSON, CRYS and CXML ids) now respect #pragma warning disable and dotnet_diagnostic.<id>.severity in .editorconfig; a project that silenced one of them with <NoWarn> can move the suppression to the declarations it concerns.

The symptom: a #pragma warning disable CJSON0025 around a type had no effect, and neither did a severity line in .editorconfig. Only a project-wide <NoWarn> worked, which silenced the diagnostic for every type in the project. The cause: the generator reported each diagnostic with a location that had a file path and a span but no syntax tree, and the compiler applies per-file suppressions through the tree. The generator now binds each diagnostic to the tree of the compilation it reports on. <NoWarn> is a compilation option, needed no tree, and still works.

The pragma wraps the declaration the diagnostic points at. CJSON0025 points at the type that implements IJsonPackable, not at the container that registers it:

// GpsPoint implements its own format on purpose: no proxy is wanted
#pragma warning disable CJSON0025
public sealed record GpsPoint : IJsonPackable
{
	public int Lat { get; init; }
	public int Lon { get; init; }

	JsonValue IJsonPackable.JsonPack(CrystalJsonSettings settings, ICrystalJsonTypeResolver resolver)
		=> JsonString.Return($"{this.Lat}:{this.Lon}");
}
#pragma warning restore CJSON0025

[CrystalConverter]
[CrystalJsonOutput]
[CrystalSerializable(typeof(GpsPoint))]
public static partial class AcmeSerializers { }

Or, for one file or folder, in .editorconfig:

[*.cs]
dotnet_diagnostic.CJSON0025.severity = none

TuPack unpacks into a caller-supplied Range buffer

If a hot path unpacks keys and the profiler shows a Range[] allocation per call, these overloads remove it. TuPack.Unpack(ReadOnlySpan<byte>) and TryUnpack allocate an array for the range of each element. The new overloads take a Span<Range> that the caller supplies, typically a stackalloc, and the returned SpanTuple is backed by that buffer, so the buffer must stay valid and untouched while the tuple is used. CountItems and TryCountItems count the elements without decoding them, so a caller can size the buffer up front.

Slice packed = TuPack.EncodeKey("orders", 42, "shipped");

Span<Range> buffer = stackalloc Range[8];
SpanTuple tuple = TuPack.Unpack(packed.Span, buffer);
tuple.Get<int>(1);
// => 42

TuPack.CountItems(packed.Span);
// => 3

When the buffer is too small, Unpack throws ArgumentException ("The buffer holds 2 ranges, but the tuple has more than 2 items.") and TryUnpack returns false; the buffer is never replaced by a larger one. IKeySubspace.Unpack(ReadOnlySpan<byte>, Span<Range>) is the subspace form of the same overload.

Two related changes on the decoding side need no caller change. An embedded tuple inside a packed tuple now decodes as a SlicedTuple that views the packed bytes, instead of a list that boxed every element. TuplePacker<IVarTuple>.Deserialize(Slice) returns that view over the caller's Slice instead of a copy.

Dates format and parse two to five times faster

Date formatting was the last thing left at the top of a profile of a JSON-heavy service once everything else had been tuned: every DateTime, DateTimeOffset and Instant cost as much as a dozen integers. In 7.4.6 the ISO 8601 writer computes the time of day with two fixed-point multiplications instead of four divisions, takes the calendar date from DateTime.Deconstruct, writes every digit pair from a table, and formats in place in the output buffer instead of in a stack buffer copied afterwards. The parsers read digits directly instead of calling int.TryParse on each field, and the DateTime and Instant conversions take the span parser before the BCL and NodaTime parsers, which remain as fallbacks for the spellings the span parser does not recognize.

Measured with BenchmarkDotNet on one x64 developer machine, .NET 10, Release, per value:

Operation 7.4.5 7.4.6
Write a DateTime with a fraction 24.7 ns 15.4 ns
Write a DateTimeOffset 29.4 ns 17.8 ns
Write an Instant 25.1 ns 15.0 ns
Write a DateOnly 11.4 ns 6.5 ns
JsonString.Return(Instant) 89 ns, 320 bytes 14 ns, 104 bytes (6 times faster)
JsonValue.ToDateTime() 88 ns 17 ns (5 times faster)
JsonValue.ToDateTimeOffset() 61 ns 23 ns
JsonValue.ToInstant() 86 ns, 256 bytes 19 ns, no allocation (4.5 times faster)
JsonValue.ToInstant() from a literal with an offset 157 ns, 336 bytes 22 ns, no allocation

The writer paths allocate nothing, before and after. JsonString.Return(DateTime) keeps the same time (20 ns) and the same two allocations, the string and the JsonString.

CrystalJsonParser.TryParseIso8601Instant is the new entry point of the Instant conversion; it is public for code that parses instants outside a JSON document.

CrystalJsonParser.TryParseIso8601Instant("2025-06-16T19:46:17.934567891+02:00", out Instant parsed);
// => true, parsed == 2025-06-16T17:46:17.934567891Z

An Instant keeps its nanoseconds, and every route spells a date the same way

An Instant with nanoseconds below the tick came back changed after a round trip: the writer converted it to a DateTime (100-nanosecond ticks) and wrote seven fraction digits, so …17.934567891Z was written as …17.9345678Z and read back as a different instant. JsonString.Return(Instant) used the NodaTime pattern and wrote nine digits, so the same value had two spellings depending on the route.

In 7.4.6 the writer formats the instant from its own nanoseconds. An instant with tick precision keeps its previous text; one with nanoseconds below the tick gets nine fraction digits, and both parse back to the same instant on every route.

The same alignment applies to DateTime and DateTimeOffset: JsonString.Return, JsonValue.FromValue and JsonDateTime.ToJsonText now produce the text the writer produces. Before, JsonString.Return used the BCL round-trip format, so a document built through the DOM and the same document serialized from a CLR object differed on whole seconds and on unspecified midnights.

Value 7.4.5 7.4.6, every route
Instant with 934567891 nanoseconds (text route) "…17:46:17.9345678Z" "…17:46:17.934567891Z"
DateTime on a whole second, UTC (DOM route) "2025-06-16T17:46:17.0000000Z" "2025-06-16T17:46:17Z"
DateTime at midnight, unspecified kind (DOM route) "2025-06-16T00:00:00.0000000" "2025-06-16"
DateTime.MaxValue, UTC (DOM route) "9999-12-31T23:59:59.9999999Z" "9999-12-31T23:59:59.9999999"
default(Instant), the Unix epoch (text route) "1970-01-01T00:00:00Z" ""
Instant.MinValue (text route) "" "-9998-01-01T00:00:00Z"

The last two rows settle which instant means "unset": default(Instant) is the empty string on every route, the rule DateTime.MinValue already follows, and NodaTime's Instant.MinValue is a regular date that round-trips. Before, the writer used Instant.MinValue as the sentinel, so that value came back as the epoch.

var instant = Instant.FromUtc(2025, 6, 16, 17, 46, 17) + Duration.FromNanoseconds(934_567_891);
CrystalJson.Serialize(instant);
// => "2025-06-16T17:46:17.934567891Z"
CrystalJson.Deserialize<Instant>("\"2025-06-16T17:46:17.934567891Z\"") == instant;
// => true
JsonValue.FromValue(instant).ToJsonText() == CrystalJson.Serialize(instant);
// => true

var whole = new DateTime(2025, 6, 16, 17, 46, 17, DateTimeKind.Utc);
JsonValue.FromValue(whole).ToJsonText();
// => "2025-06-16T17:46:17Z" (7.4.5: "2025-06-16T17:46:17.0000000Z")
CrystalJson.Serialize(default(Instant));
// => "" (7.4.5: "1970-01-01T00:00:00Z")

Every spelling, old and new, parses back to the same value, so documents written by 7.4.5 read unchanged. What changes is the text: a test that compares the JSON text of a DOM-built document, an ETag or a hash computed over it, or a stored document compared byte for byte with a fresh serialization sees a difference for whole-second dates, unspecified midnights, and instants equal to the epoch. Update those expectations to the writer's text.

Fixed: date literals that threw, were rejected, or parsed the wrong value

Seven defects in the date parsers and the DOM factories, found by the round-trip tests added for this release. Each line gives the 7.4.5 behavior and the 7.4.6 behavior.

  • StringConverters.ParseDateTime("2025-06-16T17:46:17") returned 17:06:17: the YYYY-MM-DDTHH:mm:ss and YYYY-MM-DDTHH:mm:ssZ cases passed the month as the minute. Now 17:46:17. The JSON conversions reach this parser only when the ISO 8601 parsers decline a literal, so a JSON date was affected only through a direct call.
  • "2016-12-31T23:59:60Z" (a leap second): JsonValue.ToDateTime(), ToDateTimeOffset() and ToInstant() threw ArgumentOutOfRangeException, and TryConvertToDateTimeOffset threw instead of returning false. Now the Try methods return false and the conversions throw FormatException, since neither DateTime nor Instant can represent second 60.
  • "2025-06-16T17:46:17+13:00" (Tonga, Samoa, Kiribati, Chatham): the offset parser rejected any offset beyond 12 hours, and the fallback parser dropped the offset and shifted the ticks. Now offsets up to 14 hours, the DateTimeOffset limit, parse exactly.
  • JsonString.Return(DateTimeOffset) and JsonValue.FromValue(DateTimeOffset) compared the value with DateTime.MinValue, a conversion that applies the local time zone and throws ArgumentOutOfRangeException on any machine east of Greenwich. Now the comparison is with DateTimeOffset.MinValue.
  • JsonDateTime.ToJsonText() wrapped a DateTimeOffset in single quotes, '2025-06-16T17:46:17+02:00', which is not JSON. Now double quotes.
  • "2025-06-16T17:46:+7Z" or " 7" inside a field: int.TryParse accepted a sign or a space as part of a two-digit field. Now a field is two digits, nothing else.
  • "2025-06-16T17:46:17.934567891Z" into a DateTime: the BCL parser rejects more than seven fraction digits, so an instant written with nanoseconds failed to deserialize into a DateTime member. Now the value truncates to the tick, …17.9345678.

String literals, decimals and Guid values serialize faster

On .NET 8 and later, the parser locates a string literal without escape sequences with one vectorized scan for the closing quote or a backslash, then copies or interns it in one operation instead of one character at a time. An escape, a non-ASCII byte in a UTF-8 document, or a missing closing quote falls back to the previous character loop with nothing consumed, so accepted documents and their values are unchanged. A decimal literal with at most 2^53 in its digits and at most 22 fraction digits, 1234.56 or 0.000001, is one exact division instead of a double.Parse of the literal; both operands are exact doubles, so the result is the correctly rounded value the BCL returns. The writer formats a Guid or a Uuid128, Uuid96, Uuid80 or Uuid64 in place between its quotes; Uuid96 and Uuid80 no longer allocate a string per value.

On a 614-byte order document (a Guid, two dates, five line items, three tags), parsing to the DOM takes 1.7 µs from a string (2.7 µs in 7.4.5) and 2.0 µs from UTF-8 bytes (2.5 µs). Writing a Guid takes 7.3 ns (9.0 ns). The netstandard2.0 build keeps the previous code paths.

FoundationDB

The emulator applies the cluster's 5 second history window

This changes the behavior of tests that read at an old version. Read the population paragraph below to see whether it concerns you.

The symptom: on a cluster, a read at a version older than about 5 seconds fails with transaction_too_old, and the emulator (FoundationDB.FakeDb) did not implement this. Every committed version stayed readable forever, memory grew with every commit of a long test run, and a test that passed on the emulator could fail on the cluster with transaction_too_old.

In 7.4.6 the emulator implements the window. After each commit, the store runs a retention policy that removes the versions outside the window; the default policy is FdbSnapshotRetention.KeepWindow(FdbSnapshotRetention.DefaultWindow), a 5 second window measured on the store's TimeProvider. A transaction that starts a read at a removed version fails with an FdbException whose Code is FdbError.TransactionTooOld. A transaction that already took its read version can still read at that version, however old it is. At commit, a transaction whose read version is older than the oldest retained version fails with FdbError.TransactionTooOld when it has write conflicts to check, as the cluster's resolver does with the history it has.

The window is measured on the store's TimeProvider, the one introduced in 7.4.5. With a FakeTimeProvider, the window is virtual time: no version is removed while the test does not advance its clock, and a test that advances the clock past 5 seconds removes versions at the instant it chooses.

Who is affected: a test that sets a read version older than 5 seconds of store time, and a test that keeps one transaction open for more than 5 seconds of store time and then commits writes. Under the system clock, "store time" is wall-clock time, so a suite whose transactions are short sees no change. Under a FakeTimeProvider, only a test that advances its clock past the window sees the change. A test that fails on the emulator for one of these reasons would fail on a cluster too.

To keep the previous behavior, for example in a test that inspects every version of a run, select FdbSnapshotRetention.KeepEverything where the store is created:

using var store = new FakeDbStore(retention: FdbSnapshotRetention.KeepEverything);

Or in the provider options, when the emulator is registered in a service collection:

services.AddFakeDb(
	730,
	FdbPath.Root,
	configure: options => options.Retention = FdbSnapshotRetention.KeepEverything
);

With this policy, memory grows with every commit, as before. FakeDbProviderOptions.Retention is ignored when the options also supply a Store: a shared store is configured where it is created.

Three policies are built in, and a custom one is a FdbSnapshotRetentionPolicy delegate:

Policy Retained versions
FdbSnapshotRetention.KeepWindow(TimeSpan) the versions published within the window of the newest one, on the store's TimeProvider (the default, with DefaultWindow = 5 s)
FdbSnapshotRetention.KeepLast(int) the most recent count versions
FdbSnapshotRetention.KeepEverything every version (the behavior before 7.4.6)

A custom policy receives a FdbSnapshotRetentionContext: the retained versions oldest first (Count, the indexer, Head), the store's TimeProvider (Time), and Drop(version) to mark a version for removal. The store calls the policy under its write lock after each commit; Drop throws ArgumentException for the newest version.

FoundationDB.Client.Native: one package per FoundationDB branch

If you needed the native client for a 7.3 cluster, or could not tell which libfdb_c a 7.4.4.1 package contained, this is the change. A native client only connects to a cluster with the same major.minor, and the package version now names the native version.

Through 7.4.5, the FoundationDB.Client.Native package had a version (7.4.4.1) that matched neither the binding nor the native library exactly. In 7.4.6 the package version is the native client version, and the package exists once per FoundationDB branch:

Package version Ships For clusters
FoundationDB.Client.Native 7.4.7 libfdb_c 7.4.7 7.4.x
FoundationDB.Client.Native 7.3.78 libfdb_c 7.3.78 7.3.x

A fourth version component is reserved for a repack of the same binaries. Both packages depend on FoundationDB.Client 7.4.1 or later with no upper bound, so they restore next to a future major version of the binding.

Pin the branch of your cluster with a floating patch:

dotnet add package FoundationDB.Client.Native --version 7.4.*

Or in the project file:

<ItemGroup>
	<!-- other packages -->
	<PackageReference Include="FoundationDB.Client" Version="7.4.6" />
	<PackageReference Include="FoundationDB.Client.Native" Version="7.4.*" />
</ItemGroup>

Two platform notes:

  • A new osx-x64 runtime contains the macOS x86_64 client, next to osx-arm64.
  • Since FoundationDB 7.4.5, the upstream linux-x64 client is compiled with AVX instructions. On an x86-64 host without AVX (a virtual machine on the qemu64, kvm64 or x86-64-v2 CPU model, or hardware older than 2011), the process is killed by an illegal-instruction signal the first time the library runs, before any transaction. Any CPU at the x86-64-v3 level or above has AVX. The win-x64, linux-arm64, osx-arm64 and osx-x64 libraries have no such requirement.

Aspire: the 7.4 image tag is now 7.4.7

FdbAspireHostingExtensions.LatestVersion74 is now 7.4.7, so an AppHost that resolves a 7.4 version with the default FdbVersionPolicy.LatestPatch policy starts a 7.4.7 container. Since 7.4.5 the upstream 7.4 images are built with AVX and have no non-AVX twin, and since 7.4.6 they are published for both amd64 and arm64, so Docker on an Apple Silicon host runs the arm64 image natively. An amd64 host without AVX cannot run any 7.4 image since 7.4.5. The 7.3, 7.2 and 7.1 branches still ship AVX and non-AVX pairs, and the same policy still selects their newest even (non-AVX) tag.

The test container replaces a stale container from another image

If a suite built on FoundationDB.Testing failed to start after an image tag change with "FdbServer test container 'fdb-test-...' is conflicting with another container! Please delete the old container", this is fixed. FdbServerTestContainer now detects a same-name container built from another image tag, removes it and its volume, and creates a fresh one from the expected image. A name conflict on a container of the same image that Testcontainers does not recognize is handled the same way, keeping the volume. On .NET Framework, where the container is driven through the docker command line, the same image check runs before an existing container is restarted.

The experimental text index writes documents without reflection

FdbTextIndex.IndexAsync (in FoundationDB.Layers.Experimental) now writes the stored document with JsonObject.ToJsonText() instead of the reflection serializer. A trimmed or Native AoT application that uses the layer no longer gets IL2026 and IL3050 warnings from it. The stored bytes are unchanged.

Dependencies

The net11.0 build targets .NET 11 RC1. Its BCL and ASP.NET Core package references move from the .NET 11 preview 7 packages (11.0.0-preview.7.26381.103) to the RC1 packages (11.0.0-rc.1.26425.128), and the SDK pinned in global.json is 11.0.100-rc.1.26425.128. The net10.0 build moves to the 10.0.12 servicing packages, and the net9.0 build to 9.0.20.

Build 7.4.5 7.4.6
net11.0 .NET 11 preview 7 (11.0.0-preview.7.26381.103) .NET 11 RC1 (11.0.0-rc.1.26425.128)
net10.0 10.0.11 10.0.12
net9.0 9.0.18 9.0.20
net8.0 (shared .NET wave) 10.0.11 10.0.12

A consumer that builds the net11.0 target needs the .NET 11 RC1 SDK. The net10.0, net9.0 and net8.0 builds use released servicing packages and need no SDK change. A consumer that restores the net11.0 build on the preview 7 SDK fails to restore the 11.0.0-rc.1 packages: install the .NET 11 RC1 SDK.

Breaking changes

  • The emulator applies the cluster's 5 second history window. A test that reads at a version older than 5 seconds of store time, or commits writes from a transaction older than that, fails with FdbError.TransactionTooOld, as on a cluster. To keep the previous behavior: new FakeDbStore(retention: FdbSnapshotRetention.KeepEverything), or options.Retention = FdbSnapshotRetention.KeepEverything in AddFakeDb.
  • FoundationDB.Client.Native has a new version numbering: 7.4.7 and 7.3.78 are the versions of the native client each package ships, where the previous package was 7.4.4.1. A project that pins the exact version 7.4.4.1 stays on the 7.4.4 client; move it to 7.4.*.
  • Dates built through the DOM change their text. JsonString.Return, JsonValue.FromValue and JsonDateTime.ToJsonText now write a whole second without a fraction ("…17:46:17Z", not "…17:46:17.0000000Z"), an unspecified midnight as a date only, and the extremes without a time zone. The writer writes default(Instant) as "" (was "1970-01-01T00:00:00Z") and an instant with nanoseconds below the tick with nine fraction digits. Every spelling parses back to the same value; only tests, hashes and byte comparisons over the JSON text see a difference.
  • The net11.0 build targets .NET 11 RC1. A consumer that builds the net11.0 target on the .NET 11 preview 7 SDK fails to restore the 11.0.0-rc.1 packages; install the .NET 11 RC1 SDK. The net10.0, net9.0 and net8.0 builds move to released servicing (10.0.12, 9.0.20) and need no change.
  • Everything else in this release is additive.