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)); } ///