Search code examples
c#mongodbmongodb-.net-driverdatabase-cursor

How to run code on final iteration on a cursor


I have a repository function, GetDocs(), that returns a MongoDB cursor.

Where I call GetDocs I iterate over the cursor and on every fifth iteration I call SetLastId().

Question: How do I identify when I am processing the last element of the cursor so I can call SetLastId() before exiting the loop?

public async Task GetDocs(string id, Func<Model, Task> processor)
        {
            var filter = Builders<Model>.Filter;
            var sort = Builders<Model>.Sort;
            using (var cursor = await Coll.Find(filter.Eq(f => f.id, id)).ToCursorAsync())
            {
                foreach (var doc in cursor.Current)
                {                    
                    await processor(doc);
                }
            }
        }

.

    using (OdbcConnection conn = new OdbcConnection(context.connectionString))
    {
        conn.Open();
        int counter = 0;

        await repo.GetDocs(context.Id, async (doc) =>
         {
             if (counter % 5 == 0)
             {
                var success = await SetLastId(doc.Id);
             }
             counter++;
         });
    }

Solution

  • What about something like this? Basically, the loop will store the previous document in memory and process it on the next iteration. That way, once it exits the loop, it has the "last doc" to hand, and can flag it as such to the processor.

    public async Task GetDocs(string id, Func<Model, bool, Task> processor)
    {
        var filter = Builders<Model>.Filter;
        var sort = Builders<Model>.Sort;
        using (var cursor = await Coll.Find(filter.Eq(f => f.id, id)).ToCursorAsync())
        {
            Model previousDoc = null;
            foreach (var doc in cursor.Current)
            {
                if (previousDoc != null)
                {
                    await processor(previousDoc, false);
                }
                previousDoc = doc;
            }
            if (previousDoc != null)
            {
                await processor(previousDoc, true);
            }
        }
    }
    

    You could also wrap it up into a reusable method that works with any IEnumerable (I've used ValueTuples here, but you could make your own type if you can't use them):

    public static IEnumerable<(T Model, bool IsLast)> Map<T>(IEnumerable<T> items)
    {
        T prevModel = default(T);
        bool isFirst = true;
        foreach (var model in items)
        {
            if (!isFirst)
            {
                yield return (prevModel, false);
            }
            else
            {
                isFirst = false;
            }
            prevModel = model;
        }
    
        if (!isFirst)
        {
            yield return (prevModel, true);
        }
    }
    
    
    public async Task GetDocs(string id, Func<Model, bool, Task> processor)
    {
        var filter = Builders<Model>.Filter;
        var sort = Builders<Model>.Sort;
        using (var cursor = await Coll.Find(filter.Eq(f => f.id, id)).ToCursorAsync())
        {
            foreach (var docWrapper in Map(cursor.Current))
            {
                await processor(docWrapper.Model, docWrapper.IsLast);
            }
        }
    }