Cached IEnumerable Implementation

Recently I had a little discussion with a colleague about IEnumerable and that it evaluates its entries each time it is enumerated. This peaked my interest, and I decided to implement a cached version.
There are many other implementations out there, some of them very similar to mine. It still seemed like a fun little project, so here it goes.

Implementation Concept

To create a cached IEnumerable, an implementation for IEnumerable and IEnumerator is needed.
The basic idea is that the CachedIEnumerable wraps the IEnumerable that should be cached, and includes a List of already evaluated entries, so that they don’t get evaluated multiple times.
The CachedIEnumerator receives both, the IEnumerable and the shared cached values. Only if it moves past the cached values, will it evaluate the next entry and add it to the cached entries. Then, all other enumerators will have access to the new entry without having to evaluate it.

Actual Code

Here is the implementation for CachedIEnumerable:

public class CachedIEnumerable<T> : IEnumerable<T>, IDisposable, IList<T>
{
    private List<T> enumeratedValues;
    private IEnumerable<T> enumerable;
    private IEnumerator<T> enumerator;

    public CachedIEnumerable(IEnumerable<T> enumerable)
    {
        this.enumeratedValues = new List<T>();
        this.enumerable = enumerable;
        this.enumerator = enumerable.GetEnumerator();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new CachedIEnumerator<T>(enumeratedValues, enumerator);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public void Dispose()
    {
        (enumerable as IDisposable)?.Dispose();
    }

    #region IList
    public int Count
    {
        get
        {
            if (enumerable is ICollection<T>)
                return ((ICollection<T>)enumerable).Count;

            var count = 0;
            var enumerator = this.GetEnumerator();
            while (enumerator.MoveNext()) count++;
            return count;
        }
    }
    public bool IsReadOnly => true;

    public T this[int index]
    {
        get
        {
            if (index < 0) throw new ArgumentOutOfRangeException("The given index is lower than 0");

            if (index < enumeratedValues.Count)
                return enumeratedValues[index];

            var enumerator = this.GetEnumerator();
            for (var i = 0; i <= index; i++)
                if (!enumerator.MoveNext()) throw new ArgumentOutOfRangeException("The given index exceeds the collection");
            return enumerator.Current;
        }
        set { throw new NotSupportedException(); }
    }

    public int IndexOf(T item)
    {
        var index = -1;
        var enumerator = this.GetEnumerator();
        while (enumerator.MoveNext())
        {
            index++;
            if (enumerator.Current.Equals(item))
                return index;
        }
        return -1;
    }

    public bool Contains(T item)
    {
        var enumerator = this.GetEnumerator();
        while (enumerator.MoveNext())
        {
            if (enumerator.Current.Equals(item))
                return true;
        }
        return false;
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        if (array == null) throw new ArgumentNullException("The given array is null");
        if (arrayIndex < 0) throw new ArgumentOutOfRangeException("The given start index is smaller than 0");

        var enumerator = this.GetEnumerator();
        while (enumerator.MoveNext())
        {
            if (arrayIndex >= array.Length) throw new ArgumentException("The number of elements is greater than the available space in the target array. Keep in mind that the available space was filled with objects of this collection");
            array[arrayIndex] = enumerator.Current;
            arrayIndex++;
        }
    }

    public void Add(T item)
    {
        throw new NotSupportedException();
    }

    public void Insert(int index, T item)
    {
        throw new NotSupportedException();
    }

    public bool Remove(T item)
    {
        throw new NotSupportedException();
    }

    public void RemoveAt(int index)
    {
        throw new NotSupportedException();
    }

    public void Clear()
    {
        throw new NotSupportedException();
    }
    #endregion
}

And here the implementation for CachedIEnumerator:

public class CachedIEnumerator<T> : IEnumerator<T>
{
    private List<T> sharedEnumeratedValues;
    private IEnumerator<T> enumerator;
    private int currentIndex = -1;

    public CachedIEnumerator(List<T> sharedEnumeratedValues, IEnumerator<T> enumerator)
    {
        this.sharedEnumeratedValues = sharedEnumeratedValues;
        this.enumerator = enumerator;
    }

    public T Current { get; set; }

    object IEnumerator.Current
    {
        get { return Current; }
    }

    public void Dispose()
    {
    }

    public bool MoveNext()
    {
        currentIndex++;
        if (currentIndex < sharedEnumeratedValues.Count)
        {
            Current = sharedEnumeratedValues[currentIndex];
            return true;
        }

        var success = enumerator.MoveNext();
        if (success)
        {
            Current = enumerator.Current;
            sharedEnumeratedValues.Add(Current);
            return true;
        }
        else
        {
            Current = default(T);
            return false;
        }
    }

    public void Reset()
    {
        throw new NotSupportedException();
    }
}

Additional Info

You’ll notice that I have implemented IList on my version of cached enumerable. At the moment, this doesn’t have many advantages over a simple IEnumerable implementation, due to it being readonly, except the additional methods it supports, like IndexOf and CopyTo.
In further iterations of it, I’ll implement the methods to change state. This will then allow you to overwrite values at specific indices, without the enumerable having to be evaluated to that point. This means you can override the 10th value without evaluating the underlying enumerable to that point. How cool is that?!

You can also find the code on Github as a dotnet core project, including a lot of tests.

Happy Coding! :)