Sequences
editFilter null and empty values
string?[] values = ["a", null, "", " ", "b"];
// Remove null values, and get a sequence of non-nullable items
IEnumerable<string> notNull = values.WhereNotNull();
Assert.Equal(["a", "", " ", "b"], notNull);
Assert.Equal(["a", " ", "b"], values.WhereNotNullOrEmpty());
Assert.Equal(["a", "b"], values.WhereNotNullOrWhiteSpace());
// Treat a null sequence as an empty one
IEnumerable<int>? missing = null;
Assert.Empty(missing.EmptyIfNull());- WhereNotNull removes the
nullvalues of a sequence of reference types or of nullable value types, and returns a sequence of non-nullable items, so the compiler knows the items are notnull. - WhereNotNullOrEmpty and WhereNotNullOrWhiteSpace filter sequences of strings.
- The same three methods exist for
IAsyncEnumerable<T>. - EmptyIfNull returns an empty sequence for
null, to loop over an optional collection without a check.
Check a sequence
Assert.True(new[] { 1, 2, 3 }.IsDistinct());
Assert.False(new[] { "a", "A" }.IsDistinct(StringComparer.OrdinalIgnoreCase));
Assert.False(new[] { (Id: 1, Name: "a"), (Id: 1, Name: "b") }.IsDistinctBy(item => item.Id));
Assert.Equal(1, new[] { "a", "b" }.IndexOf("b", StringComparer.Ordinal));
Assert.True(new[] { "Alpha", "Beta" }.ContainsIgnoreCase("beta"));
Assert.Equal(TimeSpan.FromMinutes(3), new[] { TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(2) }.Sum());- IsDistinct and IsDistinctBy tell whether a sequence has duplicates, with an optional comparer. They stop at the first duplicate.
- IndexOf and LongIndexOf return the position of an item in any sequence, or -1.
- ContainsIgnoreCase looks for a string with an ordinal case-insensitive comparison.
- Sum and Average add up and average
TimeSpanvalues.
Update a collection
ICollection<int> numbers = new List<int> { 1, 2, 3, 4 };
numbers.AddRange(5, 6);
numbers.RemoveAll(number => number % 2 == 0);
Assert.Equal([1, 3, 5], numbers);
IList<string> names = ["a", "b"];
names.Replace("a", "A"); // Throws when the item is not in the list
names.AddOrReplace("c", "C"); // Adds the item when it is not in the list
Assert.Equal(["A", "b", "C"], names);- AddRange adds items to any
ICollection<T>, and RemoveAll removes the items that match a predicate. - Replace replaces an item of an
IList<T>, and throws anArgumentOutOfRangeExceptionwhen it is not in the list. AddOrReplace adds the new item when the old one is not in the list. EnqueueRangeadds several items to aQueue<T>or aConcurrentQueue<T>.
Update a dictionary
var cache = new Dictionary<string, int>(StringComparer.Ordinal);
Assert.Equal(5, cache.GetOrAdd("hello", key => key.Length));
Assert.Equal(5, cache.GetOrAdd("hello", key => throw new InvalidOperationException("Not called")));
// TryUpdate only changes existing keys
Assert.True(cache.TryUpdate("hello", (key, value) => value * 2));
Assert.False(cache.TryUpdate("missing", 1));
Assert.Equal(10, cache["hello"]);GetOrAdd returns the value of a key, and adds it when the key is missing, from a value or a factory. The factory runs only when the key is missing. TryUpdate changes the value of an existing key, and returns false when the key is missing.
Loop asynchronously
var processed = new ConcurrentBag<int>();
// One item at a time, in order
await new[] { 1, 2, 3 }.ForEachAsync(async item =>
{
await Task.Yield();
processed.Add(item);
});
// Up to 2 items at the same time
await new[] { 4, 5, 6 }.ParallelForEachAsync(degreeOfParallelism: 2, async item =>
{
await Task.Yield();
processed.Add(item);
});
Assert.Equal([1, 2, 3, 4, 5, 6], processed.Order());- ForEachAsync awaits the action for each item, one item after the other. Overloads give the action the index of the item and a
CancellationToken. - ParallelForEachAsync runs the action for several items at the same time, at most
degreeOfParallelismat once (the number of processors by default). The items are not processed in order. - ForEach is the synchronous loop, with or without the index.
To turn an IEnumerable<T> into an IAsyncEnumerable<T>, for an API that only accepts the latter, use ToAsyncEnumerable from System.Linq.AsyncEnumerable, included in .NET 10. EnumerableExtensions.ToAsyncEnumerable is obsolete and is not an extension method anymore, so it does not make source.ToAsyncEnumerable() ambiguous. ToAsyncEnumerator turns an IEnumerator<T> into an IAsyncEnumerator<T>.
Enumerate once
An IEnumerable<T> can be expensive to enumerate: a database query, a file read line by line, a generator. CachedEnumerable.Create enumerates the source once, the first time, and replays the items it has already read:
var enumerationCount = 0;
IEnumerable<int> GetExpensiveEnumerable()
{
enumerationCount++;
yield return 1;
yield return 2;
}
using var cached = CachedEnumerable.Create(GetExpensiveEnumerable());
Assert.Equal([1, 2], cached); // First enumeration
Assert.Equal([1, 2], cached); // Uses cached results
Assert.Equal(1, enumerationCount);- The cache is thread-safe by default. Pass
threadSafe: falsewhen only one thread enumerates it. - The source is read lazily: an enumeration that stops early reads only the items it needs.
- Dispose the cached sequence to dispose the enumerator of the source. Enumerating it after that throws an
ObjectDisposedException. There is also an overload forIAsyncEnumerable<T>, which returns anIAsyncDisposable. - When the source throws, the enumeration that reads it gets the exception, and so do the next enumerations once they have replayed the items read before the failure. The source is not enumerated again.
- For an
IAsyncEnumerable<T>, the source is shared by all the enumerations, so it does not receive theCancellationTokenof the enumeration that starts it: it receives a token cancelled when the cached sequence is disposed. Cancelling an enumeration stops it from waiting for the source, and the next enumeration gets the item the source was reading.
To find code that enumerates a sequence twice, wrap it with AsEnumerableOnce. A second enumeration throws:
var once = new[] { 1, 2 }.AsEnumerableOnce();
Assert.Equal(3, once.Sum());
// A second enumeration throws, which reveals code that enumerates a sequence twice
Assert.Throws<InvalidOperationException>(() => once.Sum());ToOnlyEnumerable hides the type of a collection behind an iterator, so the code that receives it cannot cast it back to a list and change it.
Sort in parallel
ParallelSort and ParallelStableSort sort an array, a List<T> or a Span<T> on several threads, with an optional comparer and degree of parallelism. ParallelStableSort keeps the order of equal items. They help with large collections; for small ones, Array.Sort or List<T>.Sort is enough.