Counting Documents

The CountDocuments and CountDocumentsAsync methods can be used to count all the documents matching a particular filter.

var count = collection.CountDocuments(new BsonDocument("x", 10));

// or

var count = collection.CountDocuments(x => x.Age > 20);
var count = await collection.CountDocumentsAsync(new BsonDocument("x", 10));

// or

var count = await collection.CountDocumentsAsync(x => x.Age > 20);

Counting all the documents in a collection requires an empty filter:

var count = collection.CountDocuments(new BsonDocument());
var count = await collection.CountDocumentsAsync(new BsonDocument());

Finding Documents

Finding all the documents in a collection is done with an empty filter and the method Find. Once we have a cursor (of type IAsyncCursor<TDocument>), we can iterate it like we manually iterate an IEnumerable<TDocument>.

var filter = new BsonDocument();
using (var cursor = collection.Find(filter).ToCursor())
{
    while (cursor.MoveNext())
    {
        foreach (var doc in cursor.Current)
        {
            // do something with the documents
        }
    }
}
var filter = new BsonDocument();
using (var cursor = await collection.Find(filter).ToCursorAsync())
{
    while (await cursor.MoveNextAsync())
    {
        foreach (var doc in cursor.Current)
        {
            // do something with the documents
        }
    }
}

Note
It is imperative that the cursor get disposed once you are finished with it to ensure that resources on the server are cleaned up.

Some options are available in the optional FindOptions parameter such as setting maxTimeMS, a batch size, or a comment. Others are available as part of the fluent interface such as skip, limit, and sort.

var filter = new BsonDocument();
var options = new FindOptions
{
    MaxTime = TimeSpan.FromMilliseconds(20)
};
using (var cursor = collection.Find(filter, options).Skip(10).Limit(20).ToCursor())
{
    // etc...
}
using (var cursor = await collection.Find(filter, options).Skip(10).Limit(20).ToCursorAsync())
{
    // etc...
}

Note
The order of the chained methods is unimportant. Limit followed by Skip is the same as Skip followed by Limit. In addition, specifying a method multiple times will result in the last one winning.

Iteration

Other methods of iteration besides using a cursor directly are available.

First, the ToList and ToListAsync methods are available. These methods are useful when the list will be small or you simply need them all as a list. If you are returning a large number of documents, then memory should be considered a factor.

var list = collection.Find(filter)
    .Skip(10)
    .ToList();
var list = await collection.Find(filter)
    .Skip(10)
    .ToListAsync();

Second, ForEachAsync is available. This method is useful when you just need to process each document and don’t need to keep them around.

await collection.Find(filter)
    .Skip(10)
    .ForEachAsync(doc => Console.WriteLine(doc));

To avoid blocking while processing each document you can use an async lambda with ForEachAsync:

await collection.Find(filter)
    .Skip(10)
    .ForEachAsync(async (doc) => await Console.WriteLineAsync(doc));

When using the synchronous API you can use the C# foreach statement to iterate over the documents in a cursor.

var cursor = collection.Find(filter)
    .Skip(10)
    .ToCursor();
foreach (var doc in cursor.ToEnumerable())
{
    Console.WriteLine(doc); 
}

Note
These iteration methods don’t require you to dispose of the cursor. That will be handled for you automatically.

Single Results

When you only want to find one document, the First, FirstOrDefault, Single, SingleOrDefault methods, and their asynchronous counterparts FirstAsync, FirstOrDefaultAsync, SingleAsync, and SingleOrDefaultAsync are available.

var result = collection.Find(filter)
    .Skip(10)
    .FirstOrDefault();
var result = await collection.Find(filter)
    .Skip(10)
    .FirstOrDefaultAsync();

Aggregation

MongoDB offers the aggregation framework which can be accessed via the Aggregate method. The result type is IAggregateFluent and provides access to a fluent API to build up an aggregation pipeline.

The first example from MongoDB’s documentation is done in a type-safe manner below:

[BsonIgnoreExtraElements]
class ZipEntry
{
    [BsonId]
    public string Zip { get; set; }

    [BsonElement("city")]
    public string City { get; set; }

    [BsonElement("state")]
    public string State { get; set; }

    [BsonElement("pop")]
    public int Population { get; set; }
}
var results = db.GetCollection<ZipEntry>.Aggregate()
    .Group(x => x.State, g => new { State = g.Key, TotalPopulation = g.Sum(x => x.Population) })
    .Match(x => x.TotalPopulation > 20000)
    .ToList();
var results = await db.GetCollection<ZipEntry>.Aggregate()
    .Group(x => x.State, g => new { State = g.Key, TotalPopulation = g.Sum(x => x.Population) })
    .Match(x => x.TotalPopulation > 20000)
    .ToListAsync();

This will result in the following aggregation pipeline getting sent to the server:

[{ group: { _id: '$state', TotalPopulation: { $sum : '$pop' } } },
{ $match: { TotalPopulation: { $gt: 20000 } } }]

Note
You can call ToString on the pipeline to see what would be sent to the server.

More samples are located in the source.

Stage Operators

All the stage operators are supported, however some of them must use the AppendStage method due to lack of support for certain projections in the language.

important
Unlike Find, the order that stages are defined in matters. Skip(10).Limit(20) is not the same as Limit(20).Skip(10).

$project

A $project is defined using the Project method and its overloads. Unlike in Find, an aggregate projection is not executed client-side and must be fully translatable to the server’s supported expressions. See expressions for more detail about the expressions available inside a $project.

Project(x => new { Name = x.FirstName + " " + x.LastName });
{ $project: { Name: { $concat: ['$FirstName', ' ', '$LastName'] } } }

Note
In an aggregation framework projection, a new type, either anonymous or named, must be used.

$match

A $match stage is defined using the Match method and its overloads. It follows the same requirements as that of Find.

Match(x => x.Age > 21);
{ $match: { Age: { $gt: 21 } } }

$redact

There is no method defined for a $redact stage. However, it can be added using AppendStage.

$limit

A $limit stage is defined using the Limit method.

Limit(20);
{ $limit: 20 }

$skip

A $skip stage is defined using the Skip method.

Skip(20);
{ $skip: 20 }

$unwind

An $unwind stage is defined using the Unwind method and its overloads. Because $unwind is a type of projection, you must provide a return type, although not specifying one will use the overload that projects into a BsonDocument.

Unwind(x => x.ArrayFieldToUnwind);
{ $unwind: 'ArrayFieldToUnwind' }

$group

A $group stage is defined using the Group method and its overloads. Because $unwind is a type of projection, you must provide a return type. The most useful of the overloads is where two lambda expressions are expressed, the first for the key and the second for the grouping. See expressions for more detail about the expressions available inside a $group.

Group(x => x.Name, g => new { Name = g.Key, AverageAge = g.Average(x = x.Age) });
{ $group: { _id: '$Name', AverageAge: { $avg: '$Age'} } }

As in project, it is required that the result of the grouping be a new type, either anonymous or named. If the Key of the grouping is not used, an _id will still be inserted as this is required by the $group operator.

$sort

A $sort stage is defined using the Sort method. However, SortBy, SortByDescending, ThenBy, and ThenByDescending are also available.

SortBy(x => x.LastName).ThenByDescending(x => x.Age);
{ $sort: { LastName: 1, Age: -1 } }

$geoNear

There is no method defined for a $geoNear stage. However, it can be added using AppendStage.

$out

A $out stage is defined using the Out or OutAsync methods. It must be the final stage and it triggers execution of the operation.

Out("myNewCollection");
await OutAsync("myNewCollection");
{ $out: 'myNewCollection' }