From 20f995debe80ba4495b2f7d8bc5442e28eef9bc4 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 18 Feb 2026 11:03:01 +0200 Subject: [PATCH] Change Seq.empty to use Enumerable.Empty Seq.empty was implemented as a single-case DU (EmptyEnumerable) which caused serializers to detect the CompilationMapping attribute and treat it as an F# union type instead of a plain enumerable. This made Seq.empty serialize as "EmptyEnumerable" instead of [] with JSON.NET, STJ, and ASP.NET ObjectResult. Replace the implementation with System.Linq.Enumerable.Empty<'T>(), which returns a standard .NET type that serializes correctly. Also remove the EmptyEnumerable type and the related optimization in ConcatEnumerator that is no longer needed. Fixes #17864 --- docs/release-notes/.FSharp.Core/10.0.300.md | 2 ++ src/FSharp.Core/seq.fs | 2 +- src/FSharp.Core/seqcore.fs | 25 +++---------------- src/FSharp.Core/seqcore.fsi | 6 ----- .../SeqModule2.fs | 14 ++++++++++- 5 files changed, 20 insertions(+), 29 deletions(-) diff --git a/docs/release-notes/.FSharp.Core/10.0.300.md b/docs/release-notes/.FSharp.Core/10.0.300.md index c6012363182..f4e9654858a 100644 --- a/docs/release-notes/.FSharp.Core/10.0.300.md +++ b/docs/release-notes/.FSharp.Core/10.0.300.md @@ -1,5 +1,7 @@ ### Fixed +* `Seq.empty` now uses `System.Linq.Enumerable.Empty<'T>()` instead of an internal DU, fixing serialization issues with JSON.NET, STJ, and ASP.NET ObjectResult. ([Issue #17864](https://github.com/dotnet/fsharp/issues/17864), [PR #19322](https://github.com/dotnet/fsharp/pull/19322)) + ### Added ### Changed diff --git a/src/FSharp.Core/seq.fs b/src/FSharp.Core/seq.fs index c05dbdc3701..ed0842488ef 100644 --- a/src/FSharp.Core/seq.fs +++ b/src/FSharp.Core/seq.fs @@ -608,7 +608,7 @@ module Seq = mkUnfoldSeq generator state [] - let empty<'T> = (EmptyEnumerable :> seq<'T>) + let empty<'T> = System.Linq.Enumerable.Empty<'T>() [] let initInfinite initializer = diff --git a/src/FSharp.Core/seqcore.fs b/src/FSharp.Core/seqcore.fs index 1445f9b16c0..e73d6bd536b 100644 --- a/src/FSharp.Core/seqcore.fs +++ b/src/FSharp.Core/seqcore.fs @@ -62,16 +62,6 @@ module internal IEnumerator = let Empty<'T> () = (new EmptyEnumerator<'T>() :> IEnumerator<'T>) - [] - type EmptyEnumerable<'T> = - - | EmptyEnumerable - - interface IEnumerable<'T> with - member _.GetEnumerator() = Empty<'T>() - - interface IEnumerable with - member _.GetEnumerator() = (Empty<'T>() :> IEnumerator) type GeneratedEnumerable<'T, 'State>(openf: unit -> 'State, compute: 'State -> 'T option, closef: 'State -> unit) = let mutable started = false @@ -313,17 +303,10 @@ module RuntimeHelpers = let rec takeOuter() = if outerEnum.MoveNext() then let ie = outerEnum.Current - // Optimization to detect the statically-allocated empty IEnumerables - match box ie with - | :? EmptyEnumerable<'T> -> - // This one is empty, just skip, don't call GetEnumerator, try again - takeOuter() - | _ -> - // OK, this one may not be empty. - // Don't forget to dispose of the enumerator for the inner list now we're done with it - currInnerEnum.Dispose() - currInnerEnum <- ie.GetEnumerator() - takeInner () + // Don't forget to dispose of the enumerator for the inner list now we're done with it + currInnerEnum.Dispose() + currInnerEnum <- ie.GetEnumerator() + takeInner () else // We're done x.Finish() diff --git a/src/FSharp.Core/seqcore.fsi b/src/FSharp.Core/seqcore.fsi index 17226767a2d..9e68f348588 100644 --- a/src/FSharp.Core/seqcore.fsi +++ b/src/FSharp.Core/seqcore.fsi @@ -24,12 +24,6 @@ module internal IEnumerator = val Empty: unit -> IEnumerator<'T> - [] - type EmptyEnumerable<'T> = - | EmptyEnumerable - - interface IEnumerable - interface IEnumerable<'T> [] type Singleton<'T> = diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/SeqModule2.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/SeqModule2.fs index db3b8a3adcc..bc00d5f0637 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/SeqModule2.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/SeqModule2.fs @@ -2066,4 +2066,16 @@ type SeqModule2() = // empty list & out of bounds Assert.AreEqual([0; 0], Seq.insertManyAt 0 [0; 0] [] |> Seq.toList) CheckThrowsArgumentException (fun () -> Seq.insertManyAt -1 [0; 0] [1] |> Seq.toList |> ignore) - CheckThrowsArgumentException (fun () -> Seq.insertManyAt 2 [0; 0] [1] |> Seq.toList |> ignore) \ No newline at end of file + CheckThrowsArgumentException (fun () -> Seq.insertManyAt 2 [0; 0] [1] |> Seq.toList |> ignore) + + [] + member _.EmptySerializesAsEmptyArray() = + let json = System.Text.Json.JsonSerializer.Serialize(Seq.empty) + Assert.AreEqual("[]", json) + + [] + member _.EmptyFormatsCorrectly() = + // Seq.empty uses Enumerable.Empty which returns a cached empty array, + // so %A formats it with array notation + let formatted = sprintf "%A" Seq.empty + Assert.AreEqual("[||]", formatted) \ No newline at end of file