From 82fcd1c510147f7888c24029f608a3c4b4112137 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 06:31:17 +0000 Subject: [PATCH] [patch] Keep items whose ToString() is null in ToStringEnumerable and Join ToStringEnumerable filtered on the projected string, so NullItemHandling.Throw and Remove silently dropped a non-null item whose ToString() returns null, while Join emitted the same item as an empty entry. Null handling now tests the item only, such an item becomes "", and Join delegates to ToStringEnumerable so the two always agree. Fixes #153 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01P2fpLkFHYU6nd3JVKufVtK --- Extensions.Test/EnumerableExtensionsTests.cs | 38 ++++++++++++++++++++ Extensions/EnumerableExtensions.cs | 16 +++++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/Extensions.Test/EnumerableExtensionsTests.cs b/Extensions.Test/EnumerableExtensionsTests.cs index 8feac9d..68951b9 100644 --- a/Extensions.Test/EnumerableExtensionsTests.cs +++ b/Extensions.Test/EnumerableExtensionsTests.cs @@ -456,4 +456,42 @@ public void ToStringEnumerableWithNullItemHandlingThrowCatchesANullAddedBeforeEn Assert.ThrowsExactly(() => result.ToList()); } + + private sealed class NullToString + { + public override string? ToString() => null; + } + + [TestMethod] + [DataRow(NullItemHandling.Remove)] + [DataRow(NullItemHandling.Include)] + [DataRow(NullItemHandling.Throw)] + public void ToStringEnumerableKeepsAnItemWhoseToStringIsNullAsEmpty(NullItemHandling nullItemHandling) + { + object[] items = [new NullToString(), "x"]; + + List result = [.. items.ToStringEnumerable(nullItemHandling)]; + + CollectionAssert.AreEqual(new List { string.Empty, "x" }, result); + } + + [TestMethod] + public void ToStringEnumerableWithoutHandlingKeepsAnItemWhoseToStringIsNullAsEmpty() + { + object[] items = [new NullToString(), "x"]; + + List result = [.. items.ToStringEnumerable()]; + + CollectionAssert.AreEqual(new List { string.Empty, "x" }, result); + } + + [TestMethod] + [DataRow(NullItemHandling.Remove)] + [DataRow(NullItemHandling.Include)] + public void JoinMatchesToStringEnumerableForTheSameHandling(NullItemHandling nullItemHandling) + { + object?[] items = [new NullToString(), null, "x"]; + + Assert.AreEqual(string.Join(",", items.ToStringEnumerable(nullItemHandling)), items.Join(",", nullItemHandling)); + } } diff --git a/Extensions/EnumerableExtensions.cs b/Extensions/EnumerableExtensions.cs index 6e34cb6..233a0ea 100644 --- a/Extensions/EnumerableExtensions.cs +++ b/Extensions/EnumerableExtensions.cs @@ -190,7 +190,10 @@ public static IEnumerable ToStringEnumerable(this IEnumerable item /// The type of the items in the enumerable. /// The enumerable to convert. /// Specifies how to handle null items. - /// An enumerable of strings. + /// + /// An enumerable of strings. applies to null items only; a non-null item whose + /// returns null is always kept, as an empty string. + /// /// Thrown while enumerating the result if is set to and the enumerable contains null items. public static IEnumerable ToStringEnumerable(this IEnumerable items, NullItemHandling nullItemHandling) { @@ -203,9 +206,11 @@ public static IEnumerable ToStringEnumerable(this IEnumerable item // The argument check above stays eager; the null check happens during the one enumeration the // caller makes, so a single-pass source is not consumed early and a null added later is caught. + // Null handling tests the item, not its string: an item whose ToString() returns null becomes "". return items - .Select(item => ThrowIfNullItem(item, nullItemHandling)?.ToString()) - .Where(item => nullItemHandling is NullItemHandling.Include || item is not null); + .Select(item => ThrowIfNullItem(item, nullItemHandling)) + .Where(item => nullItemHandling is NullItemHandling.Include || item is not null) + .Select(item => item is null ? null : item.ToString() ?? string.Empty); } /// @@ -260,10 +265,7 @@ public static string Join(this IEnumerable items, string separator, NullIt } #pragma warning restore KTSU0004 // Use Ensure.NotNull instead of manual null check - return string.Join(separator, items - .Select(item => ThrowIfNullItem(item, nullItemHandling)) - .Where(item => nullItemHandling is NullItemHandling.Include || item is not null) - .Select(i => i?.ToString())); + return string.Join(separator, items.ToStringEnumerable(nullItemHandling)); } ///