# 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** 並對他進行兩次列舉。
* 執行結果如下圖所示:

* 可以發現,第二次列舉時有例外(**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)