---
# System prepended metadata

title: 'IAsyncCursor<TDocumen\>的問題之一 : 不能重複巡覽'

---

# IAsyncCursor<TDocumen\>的問題之一 : 不能重複巡覽

## 前言
    
* 在C#中，和mongoDB溝通的套件 **MongoDB.Driver**，是大家最常用套件之一。而在和資料庫溝通的需求中，又以「讀取」為最常用的功能。
## 問題描述
* MongoDB.Driver提供**FindAsync()** 這個非同步方法來讀取資料庫中的資料，而其回傳的Type為**Task<IAsyncCursor\<TDocument>>**
    * **FindAsync()** 之定義如下
    ```csharp=
    Task<IAsyncCursor<TProjection>> FindAsync<TProjection>(FilterDefinition<TDocument> filter, FindOptions<TDocument, TProjection> options = null, CancellationToken cancellationToken = default(CancellationToken));
    ```
    * 從上述定義來看，該函式會回傳一個類型為**IAsyncCursor\<TDocument>** 的物件；可以把他想像成一個「游標」，一個在文件中遊走的游標。他可以一次跳過一個item，也可以一次跳過多個。該介面的定義如下:
        ```csharp=
        //
        // 摘要:
        //     Represents an asynchronous cursor.
        //
        // 類型參數:
        //   TDocument:
        //     The type of the document.
        public interface IAsyncCursor<out TDocument> : IDisposable
        {
            //
            // 摘要:
            //     Gets the current batch of documents.
            //
            // 值:
            //     The current batch of documents.
            IEnumerable<TDocument> Current{get;}

            //
            // 摘要:
            //     Moves to the next batch of documents.
            //
            // 參數:
            //   cancellationToken:
            //     The cancellation token.
            //
            // 傳回:
            //     Whether any more documents are available.
            bool MoveNext(CancellationToken cancellationToken = default(CancellationToken));

            //
            // 摘要:
            //     Moves to the next batch of documents.
            //
            // 參數:
            //   cancellationToken:
            //     The cancellation token.
            //
            // 傳回:
            //     A Task whose result indicates whether any more documents are available.
            Task<bool> MoveNextAsync(CancellationToken cancellationToken = default(CancellationToken));
        }
        ```
        * 可以把**IAsyncCursor<TDocument\>** 看成**IAsyncEnumerator<IEnumerable<TDocument\>\>** :他們都可以非同步的巡覽**IEnumerable<TDocument\>** 的物件
    
* **MongoDB.Driver** 也提供擴充方法**ToEnumerable<TDocument\>(this IAsyncCursor<TDocument\> cursor)**，以方便使用**Linq** 相關功能。詳細定義如下:
    ```csharp=
    //
    // 摘要:
    //     Wraps a cursor in an IEnumerable that can be enumerated one time.
    //
    // 參數:
    //   cursor:
    //     The cursor.
    //
    //   cancellationToken:
    //     The cancellation token.
    //
    // 類型參數:
    //   TDocument:
    //     The type of the document.
    //
    // 傳回:
    //     An IEnumerable
    public static IEnumerable<TDocument> ToEnumerable<TDocument>(this IAsyncCursor<TDocument> cursor, CancellationToken cancellationToken = default(CancellationToken))
    {
        return new AsyncCursorEnumerableOneTimeAdapter<TDocument>(cursor, cancellationToken);
    }
    ```
    * 該擴充方法回傳的物件**AsyncCursorEnumerableOneTimeAdapter<TDocument\>(cursor, cancellationToken)** ，為一個實作介面**IEnumerable<TDocument\>** 之物件。
        * 因為擴充方法**ToEnumerable()** 是回傳**IEnumerable**，所以外面的使用者只能得到一個**IEnumerable** 的物件。
        * 類別**AsyncCursorEnumerableOneTimeAdapter<TDocument\>(cursor, cancellationToken)** ，顧名思義他只能被**列舉一次**。
        * 但矛盾的是，**IEnumerable** 有提供**GetEnumerator()** 的方法，讓使用者可以重複的**取得新列舉器(Enumerator)**。因此，理論上使用者是可以對一個**IEnumeable** 之物件進行**重複列舉**。
* 綜上所述，我們發現一個矛盾點:
### 「IAsyncCursor.ToEnmerable()轉出的物件不符合介面規範」

## 測試
* 我們寫了簡單的程式碼，以確認上述問題確定存在
    * 程式碼如下:
        ```csharp=
        public static async Task Main()
        {
            var mongo = new MongoClient("mongodb://localhost:27017").GetDatabase("EnumeratorPlayground").GetCollection<MySchema>(nameof(MySchema));
            var mongoEnumerable = (await mongo.FindAsync(Builders<MySchema>.Filter.Empty)).ToEnumerable();

            for (int i = 0; i < 2; i++)
            {
                foreach(var iten in mongoEnumerable){}
                Console.WriteLine($"cursor enumeration finished...\n");
            }
        }
        ```
        * 取得**IAsyncCursor<MySchema\>** 物件後，先將他轉成**IEnumerable** 並對他進行兩次列舉。
    * 執行結果如下圖所示:
        ![](https://i.imgur.com/N5WfpUw.png)
        * 可以發現，第二次列舉時有例外(**Exception**)產生。
## 原因
* 當然，最主要的原因就是**ToEnumerable()** 產生的**IEnumerable** 是由類別**AsyncCursorEnumerableOneTimeAdapter<TDocument\>** 實作；而該物件不支援**重複列舉**。
    * **AsyncCursorEnumerableOneTimeAdapter** 的[source code](https://github.com/mongodb/mongo-csharp-driver/blob/master/src/MongoDB.Driver.Core/Core/Operations/AsyncCursorEnumerableOneTimeAdapter.cs)如下:
        ```csharp=
        internal class AsyncCursorEnumerableOneTimeAdapter<TDocument> : IEnumerable<TDocument>
        {
            // private fields
            private readonly CancellationToken _cancellationToken;
            private readonly IAsyncCursor<TDocument> _cursor;
            private bool _hasBeenEnumerated;

            // constructors
            public AsyncCursorEnumerableOneTimeAdapter(IAsyncCursor<TDocument> cursor, CancellationToken cancellationToken)
            {
                _cursor = Ensure.IsNotNull(cursor, nameof(cursor));
                _cancellationToken = cancellationToken;
            }

            // public methods
            public IEnumerator<TDocument> GetEnumerator()
            {
                if (_hasBeenEnumerated)
                {
                    throw new InvalidOperationException("An IAsyncCursor can only be enumerated once.");
                }
                _hasBeenEnumerated = true;
                return new AsyncCursorEnumerator<TDocument>(_cursor, _cancellationToken);
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
        }
        ```
        * 在方法**GetEnumerator()** 中，會記錄該使用者是否已列舉過；若已列舉過就會丟出一個**Exception** 。

## 結論
* 究竟一個interface之實作，能不能不支援某些方法呢?尤其是對於MongoDB.Driver這種使用群眾如此龐大的package?
    * 事實上，也有許多interface之實作不支援其中特定某些方法或屬性，並會拋出**NotSupportException** 之類的例外，以告知使用者。而且**AsyncCursorEnumerableOneTimeAdapter** 拋出的例外也非常淺顯易懂。
* 但以大型專案角度來看，這確實會造成開發和維護上的一些困擾。
    * 若是小型專案，只要稍加留意，不要將該**IEnumerable** 物件重複列舉即可。
    * 但若在大型專案中，該**IAsyncCursor** 被轉成**IENumerable** 後可能會被包近很多層函式。
        * 若是不熟悉資料底層實作的開發人員，在接到一個**IEnumerable** 之物件後，並不一定會注意到該**IEnumerable** 只能被巡覽一次。
## 改善方法
1. 將**IAsyncCursor<TDoucument\>** 轉成**IAsyncEnumerator<TDocument\>** 或**IAsyncEnumerator<IEnumerable<<TDocument\>\>** ，以明確定義該列舉器無法被重複列舉。
    * **IAsyncCursor<TDocument\>** 有兩個方法和一個屬性。你可以將他想像成一個類型為 **IAsyncCursor<IEnumerable<TDocument\>\>**，他是一個非同步的迭代器(**IAsyncEnumerator**)，其中每個element都是一個實作**IEnumerable<TDocument\>** 介面的物件。
        * 以下是**IAsyncEnumerator**之定義:
        ```csharp=
        //
        // 摘要:
        //     Supports a simple asynchronous iteration over a generic collection.
        //
        // 類型參數:
        //   T:
        //     The type of the elements in the collection.
        public interface IAsyncEnumerator<out T> : IAsyncDisposable
        {
            //
            // 摘要:
            //     Gets the element in the collection at the current position of the enumerator.
            //
            // 傳回:
            //     The element in the collection at the current position of the enumerator.
            T Current{get;}

            //
            // 摘要:
            //     Advances the enumerator asynchronously to the next element of the collection.
            //
            // 傳回:
            //     A System.Threading.Tasks.ValueTask`1 that will complete with a result of true
            //     if the enumerator was successfully advanced to the next element, or false if
            //     the enumerator has passed the end of the collection.
            ValueTask<bool> MoveNextAsync();
        }
        ```
    * 觀察上述**IAsyncCursor**和**IAsyncEnumerator**之定義可發現，他們倆個都有方法**MoveNextAsync()** 和屬性**Current** ，並且都在做幾近相同的事情
        * 重點是，**IAsyncCursor**和**IAsyncEnumerator**最契合的地方是「都無法重複列舉」，因為缺少類似**IEnumerator** 中的方法**Reset()** 。
        * 當你將**IAsyncCursor**或**IAsyncEnumerator**列舉完，你無法將他們重置並重新列舉。
2. 將**IAsyncCursor<TDoucument\>** 轉成**Array** 。
3. 實作一個**IEnumerable** 物件，並用工廠模式以便在每次重新列舉前產生新的**IAsyncCursor** 物件。
    * 以下是簡單實作:
        ```csharp=
        /// <summary>
        /// Object that adapt <see cref="IEnumerable{TDocument}"/> from <see cref="IAsyncCursor{TDocument}"/>.
        /// Result od adaption can request multiple times.
        /// </summary>
        /// <typeparam name="TDocument">Type of element in <see cref="IAsyncCursor{TDocument}"/>.</typeparam>
        public class AsyncCursorToEnumerableMultipleTimeAdapter<TDocument> : IEnumerable<TDocument>
        {
            /// <summary>
            /// Factory of <see cref="IAsyncCursor{TDocument}"/>.
            /// </summary>
            private readonly Func<Task<IAsyncCursor<TDocument>>> CursorFactoryAsync;

            /// <summary>
            /// Initialize an instance of <see cref="AsyncCursorToEnumerableMultipleTimeAdapter{TDocument}"/> class.
            /// </summary>
            /// <param name="cursorFactoryAsync"></param>
            public AsyncCursorToEnumerableMultipleTimeAdapter(Func<Task<IAsyncCursor<TDocument>>> cursorFactoryAsync)
            {
                CursorFactoryAsync = cursorFactoryAsync;
            }

            /// <inheritdoc/>
            public IEnumerator<TDocument> GetEnumerator()
            {
                return CursorFactoryAsync().Result.ToEnumerable().GetEnumerator();
            }

            /// <inheritdoc/>
            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
        }
        ```
## 參考資料
* 物件**AsyncCursorToEnumerableMultipleTimeAdapter** 之[原始碼](https://github.com/mongodb/mongo-csharp-driver/blob/master/src/MongoDB.Driver.Core/Core/Operations/AsyncCursorEnumerableOneTimeAdapter.cs)
* 物件**AsyncCursorEnumerator** 之[原始碼](https://github.com/mongodb/mongo-csharp-driver/blob/master/src/MongoDB.Driver.Core/Core/Operations/AsyncCursorEnumerableOneTimeAdapter.cs)