How does TryGetNonEnumeratedCount work?

26/05/2023
.NETC#

TryGetNonEnumeratedCount attempts to determine the number of elements in a sequence without forcing an enumeration. It returns true if it could and false if it couldn't. The API was added with .NET 6 - let's have a look how that thing works.

A simple example

Have a look at the following example:

var myEnumeration = new[] { 1, 2, 3, 4 };
var could = myEnumeration.TryGetNonEnumeratedCount(out var count);

This one is easy. We have an Array and an Array has a Length property the function could retrieve. So could is true and count is 4. Internally TryGetNonEnumeratedCount calls ICollection<T>.Count if the type implements ICollection<T>. Array implements ICollection<T> so it's easy to get the count.

A more complex example

Have a look at the following example:

var range = Enumerable.Range(1, 4);
var could = myEnumeration.TryGetNonEnumeratedCount(out var count);

Still, could is true and count is 4. But how did that work? Enumerable.Range returns an IEnumerable<int>, and IEnumerable<T> does not have a Count property. So, how did TryGetNonEnumeratedCount get the count? The magic lies in a special internal interface called: IIListProvider. The description of said type:

An iterator that can produce an array or List<TElement> through an optimized path.

And guess what? There is a RangeIterator. This is the actual type your Enumerable.Range method returns. IIListProvider has a GetCount method which returns the count. So TryGetNonEnumeratedCount calls GetCount on the RangeIterator and gets the count. The method signature is:

internal interface IIListProvider<TElement>
{
    int GetCount(bool onlyIfCheap);
}

The onlyIfCheap parameter is a hint to the implementation that it should only return the count if it's cheap to do so:

public int GetCount(bool onlyIfCheap) => unchecked(_end - _start);

That is the actual implementation of the method inside the RangeIterator. You defined the start and end, so it is easy for the method to calculate how many items it would hold.

LINQ - Let's make it spicey!

Okay, even though that seems straightforward, we can go a bit deeper into the rabbit hole. Let's have a look at the following example:

var myEnumeration = Enumerable.Range(1, 4);
var newEnumeration = myEnumeration
    .Order()
    .Select(s => s * s);
var could = newEnumeration.TryGetNonEnumeratedCount(out var count);

What do you think could and count is? could is true and count is 4. But how did that work? And why does it know the count is four without enumerating? Well, let's have a look from a logical/abstract point of view. We know that Enumerable.Range returns a RangeIterator, which is an IIListProvider<int>. We also know that Select or Order doesn't change anything in the quantity of our enumeration, so why should I care then?

And that is exactly how it works. We have certain iterators that just look up what the parent does. Let's take the last Select statement. The real call looks like this:

public int GetCount(bool onlyIfCheap)
{
    if (!onlyIfCheap)
    {
        // ... 
    }

    return _source.GetCount(onlyIfCheap);
}

And guess who our _source is? It's the RangeIterator we already know. So the Select iterator just asks the RangeIterator for the count. And the RangeIterator knows the count because it's an IIListProvider<int>.

If we throw in a Where() statement the IIListProvider has no idea how many items will be returned. It would have to call the Where statement on the concrete instance. As this would enumerate, it will return false and count will not be set.

All in all, it boils down to many internal iterators that represent common operations in the .NET framework that implement IIListProvider and can therefore possibly return the count without enumerating. It is a clever way without exposing the internals!

Special case: IQueryable<T>

Short: It doesn't matter what you do by default IQueryable<T> will always return false and count will not be set:

var myEnumeration = Enumerable.Range(1, 4).AsQueryable();
var could = myEnumeration.TryGetNonEnumeratedCount(out var count);

Even though it looks almost like the above, TryGetNonEnumeratedCount will not work here. IQueryable is neither an ICollection nor does it implement IIListProvider.

Conclusion

TryGetNonEnumeratedCount is a nice addition to the .NET world. It allows you to get the count of an enumeration without forcing an enumeration. This can be useful in certain scenarios. Hope you understand the internals a bit better.

8
An error has occurred. This application may no longer respond until reloaded. Reload x