CHAPTER 9
In the previous chapters, we have gone through data retrieval and the various possibilities. We have also seen that the find() method was one of the very important entry points.
The MongoDB C# driver offers the Find and FindAsync methods to issue a query to retrieve data from a collection. As was the case when using the MongoDB shell, all queries made by using Find have the scope of a single collection.
The FindAsync method returns query results in a IAsyncCursor, while the Find method returns an object that implements the IFindFluent interface. To avoid iterating through the list, we can use the ToListAsync method to return the results as a list. This also means that all the documents will be held in memory; therefore, one must pay attention to the quantity of data being returned.
To return all the data from a collection, we simply don’t have to specify any filter when calling the FindAsync() method. The following snippet shows a method that queries the collection and then uses the returned cursor object to iterate through the associated items. The important thing in this particular case is that the filter is actually a BsonDocument, which in this case is empty.
Code Listing 84: Finding movies as BsonDocuments
public async static void FindMoviesAsDocuments(string dbName, string collName) { var db = DatabaseHelper.GetDatabaseReference("localhost", dbName); var collection = db.GetCollection<BsonDocument>(collName); var filter = new BsonDocument(); int count = 0; using (var cursor = await collection.FindAsync<BsonDocument>(filter)) { while (await cursor.MoveNextAsync()) { var batch = cursor.Current; foreach (var document in batch) { var movieName = document.GetElement("name").Value.ToString(); Console.WriteLine("Movie Name: {0}", movieName); count++; } } } } /* Calling the method*/ MovieManager.FindMoviesAsDocuments(databaseName, "movies_bson"); /* Returns the following output*/ Movie Name: The Seven Samurai Movie Name: The Godfather |
Note that the result returned actually contains the BsonDocuments, and in order to get the values from the particular field, we need to use document.GetElement("name").Value.
If we were to query by using the Movie POCO object, then the search would look just a bit different. I’ve highlighted the differences in grey in the following code snippet.
Code Listing 85: Finding documents by using strongly typed movie collections
public async static void FindMoviesAsObjects(string dbName, string collName) { var db = DatabaseHelper.GetDatabaseReference("localhost", dbName); var collection = db.GetCollection<Movie>(collName); var filter = new BsonDocument(); int count = 0; using (var cursor = await collection.FindAsync<Movie>(filter)) { while (await cursor.MoveNextAsync()) { var batch = cursor.Current; foreach (var movie in batch) { Console.WriteLine("Movie Name: {0}", movie.Name); count++; } } } } /* Calling the method*/ MovieManager.FindMoviesAsObjects(databaseName, "movies_poco"); /* Returns the following output*/ Movie Name: The Seven Samurai Movie Name: The Godfather |
Now that we have seen the basics, we can extend the example to contain the definition of the filter. The MongoDB C# driver offers the FilterDefinitionBuilder, which can help in defining the proper query without going into too many details of the MongoDB semantics (as we had to do by using the MongoDB shell). FilterDefinition supports both BsonDocument notation (name–value) and .NET expressions (lambda). The entry point for defining queries is the Builders<T> class. As we will see, Builders<T> can support various types of operations such as filtering, sorting, or defining projections.
Let’s quickly see some basic examples by using Eq, which stands for equal.
Code Listing 86: Statically typed vs. BsonDocument filter definition
/* Filter to retrieve movies where the name equals to "The Godfather" */ var expresssionFilter = Builders<Movie>.Filter.Eq(x => x.Name, "The Godfather");
/* Filter to retrieve movies where the name equals to "The Godfather" * by using BsonDocument notation */ var bsonFilter = Builders<BsonDocument>.Filter.Eq("name", "The Godfather"); |
Builder is quite flexible, and it allows defining all the supporting MongoDB query operators such as Equal, Greater Than, Less Than, and Contains. It is also possible to create various combinations between Or and And. The following code shows an example of an Or operator:
Code Listing 87: Building a filter by using the Or operator
/* find movies where the name is "The Godfather" OR "The Seven Samurai" */ var filter = Builders<Movie>.Filter.Or(new[] { new ExpressionFilterDefinition<Movie>(x => x.Name == "The Godfather"), new ExpressionFilterDefinition<Movie>(x => x.Name == "The Seven Samurai") }); |
It is also very useful to know that is possible to use the C# operators & (binary AND) and | (binary OR) to build more complex and compile-time safe queries, such as the following:
Code Listing 88: Using C# conditional operators to build a filter
/* find movies where the name is "The Godfather" OR year > 1900 */ var builder = Builders<Movie>.Filter; var query = builder.Eq("name", movieName) | builder.Gt("year", 1900); var result = await collection.Find(query).ToListAsync(); |
By using this simple technique, we can change our original query so that we return the movies with a given name. The slight difference from the previous example is that we are converting the result to a list by using the ToListAsync() method.
In addition, we are also showing here how we can use the Builder to specify the sorting, which can be used to concatenate the various field sorting. In our case, we order Ascending by Name, and then Descending by Year.
Code Listing 89: Sorting the result
var collection = db.GetCollection<Movie>(collName);
var filter = Builders<Movie>.Filter.Eq(x => x.Name, movieName); var movies = await collection.Find(filter).ToListAsync(); var sort = Builders<Movie>.Sort.Ascending(x => x.Name).Descending(x => x.Year);
foreach (var movie in movies) { Console.WriteLine("Match found: movie with name '{0}' exists", movie.Name); } |
The Find method also supports expressions as parameters, so this is also a valid query—and perhaps faster to write, as it doesn’t need the Builders to be used.
Code Listing 90: Find with lambda expressions
var collection = db.GetCollection<Movie>(collName);
var movies = collection.Find(x => x.Name == "The Godfather"); |
In the RDBMS, one of the most natural things to do is to return just a subset of data for a given query. In MongoDB, this is called a projection of data. There are few ways of constructing the return data, but the best thing would be to start with an example, which we will then enhance slowly.
We can say that the main entry point for the projections is the Builders object—the same one used previously for filtering. Builders offers the possibility to define what data will be returned through the object ProjectionDefinition. There are two kinds of projections: one in which we know what the object to be returned (mapped to) is, and one in which we don’t have the object representation of the data returned from the database.
Code Listing 91: Projection definition examples
/* using the ProjectDefinition object to specify an object that will only return the _id and year as two attributes. */ ProjectionDefinition<Movie> projection = new BsonDocument("year", 1); /* using the strongly typed Builders object to specify the return attributes. */ var projection = Builders<Movie>.Projection .Include("name") .Include("year") .Exclude("_id"); // or var projection = Builders<Movie>.Projection .Include(x => x.Name) .Include(x => x.Year) .Exclude(x => x.MovieId); // or var projection = Builders<Movie>.Projection.Expression(x => new { X = x.Name, Y = x.Year }); |
An example of specifying the return data in BsonDocument format is as follows:
Code Listing 92: Projection as BsonDocument
var collection = db.GetCollection<Movie>(collName);
var projection = Builders<Movie>.Projection .Include("name") .Include("year") .Exclude("_id"); var data = collection.Find(new BsonDocument()) .Project<BsonDocument>(projection) .ToList();
foreach (var item in data) { Console.WriteLine("Item retrieved {0}", item.ToString()); } |
The structure being returned would be as follows:
Code Listing 93: data returned as defined by the projection
|
Item retrieved { "name" : "The Seven Samurai", "year" : 1954 } Item retrieved { "name" : "The Godfather", "year" : 1972 } |
If we would like to have a strongly typed object, then it is as easy as specifying the Project<Movie> instead of Project<BsonDocument>. In the projection definition, we can use the expressions, which simplifies things since we don’t have to remember the serialized attribute’s name.
Code Listing 94: Projection defined as strongly typed Movie object
var collection = db.GetCollection<Movie>(collName);
var projection = Builders<Movie>.Projection .Include(x => x.Name) .Include(x => x.Year) .Exclude(x => x.MovieId); var data = await collection.Find(new BsonDocument()) .Project<Movie>(projection) .ToList();
foreach (Movie item in data) { Console.WriteLine("Item retrieved {0}", item.ToString()); } |
The objects returned will be of type Movie; however, only the name and year attributes will be populated with data, and other attributes will have the default value.
The async version of the method behaves a bit differently, and returns IAsyncCursor. In addition, there is no Project method, but the projection should be passed as part of the options.
Code Listing 95: Async version of the strongly typed projection definition
var collection = db.GetCollection<Movie>(collName);
var projection = Builders<Movie>.Projection .Include(x => x.Name) .Include(x => x.Year) .Exclude(x => x.MovieId);
var options = new FindOptions<Movie, BsonDocument> { Projection = projection }; var cursor = await collection.FindAsync(new BsonDocument(), options); var data = cursor.ToList();
foreach (var item in data) { Console.WriteLine("Item retrieved {0}", item.ToString()); } |
In one of the previous chapters, we explained the aggregation and aggregation pipeline as it happens when using the MongoDB shell. As expected, the same can be done in C#.
The entry point of the functionality is the Aggregate method, which then can be expanded to specify the pipeline and the various options—pretty much what we have already gone through.
The following example shows how it is possible to group by a given field and calculate the count, in our case a count of the movies per year.
Code Listing 96: Aggregation of data by grouping
public static void AggregateMovies(string dbName, string collName) { var db = DatabaseHelper.GetDatabaseReference("localhost", dbName); var collection = db.GetCollection<Movie>(collName);
var data = collection.Aggregate() .Group(new BsonDocument { { "_id", "$year" }, { "count", new BsonDocument("$sum", 1) } });
foreach (var item in data.ToList()) { Console.WriteLine("Item retrieved {0}", item.ToString()); } } |
This query will return the _id, which will be the value of the year (hence the $ sign in front of the attribute), and the count attribute with the actual value.
Code Listing 97: Aggregation result
Item retrieved { "_id" : 1972, "count" : 2 } Item retrieved { "_id" : 1954, "count" : 1 } |
It is also possible to specify the various stages in the aggregation pipeline. Therefore, we can add the Match before executing the grouping in order to prefilter the data we want to work against.
Code Listing 98: Aggregation by prefiltering the data to be grouped
var aggregate = collection.Aggregate() .Match(Builders<Movie>.Filter.Where(x => x.Name.Contains("Godfather"))) .Group(new BsonDocument { {"_id", "$year"}, {"count", new BsonDocument("$sum", 1)} });
var results = aggregate.ToList(); |
In this particular case, the Match works as we would normally filter the data. We have used the Where method in order to filter only the movies whose names include Godfather.
The driver contains an implementation of LINQ that targets the underlying aggregation framework. The rich query language available in the aggregation framework maps very easily to the LINQ expression tree. This makes it possible to use the LINQ statements in order to perform queries.
The entry point is the AsQueriable() method that offers a world of possibilities in order to perform queries as we got used to with LINQ. AsQueriable is available at the collection level. There is support for the filtering, sorting, projecting, and grouping of data, and some basic functionality of joining to other collections.
Here’s a quick example showing that both styles of LINQ are supported. The following code transforms the collection into AsQueriable and selects only the Name and Age from a movie. Therefore, we have only two attributes returned. This makes the projection of data very easy!
Code Listing 99: Using LINQ queries
var collection = db.GetCollection<Movie>(collName);
var query = from p in collection.AsQueryable() select new { p.Name, p.Age };
// both queries are equivalent.
var query = collection.AsQueryable().Select(p => new { p.Name, p.Age }); |
Applying a Where clause is as easy as calling the Where extension method.
Code Listing 100: LINQ query, data filtering by Where clause
var collection = db.GetCollection<Movie>(collName);
var query = collection.AsQueryable().Where(p => p.Age > 21); |
Pagination becomes very easy with the Take and Skip methods.
Code Listing 101: Usage of Take and Skip
var collection = db.GetCollection<Movie>(collName);
var movies = collection.AsQueryable().Skip(10).Take(10).ToList(); |
Take will return a limited number of documents (in our case 10), while Skip makes sure to bypass the given number of documents. In our case, the documents from 11 to 20 will be returned.
The MongoDB C# driver offers quite a few ways to update the data. Here are just a few of the useful methods on a collection that can make it easy to search and manipulate documents:
Table 17: Update document methods
FindOneAndUpdate FindOneAndUpdateAsync | Updates one document based on a filter and, as a result, returns the updated document before or after the change. |
UpdateMany UpdateManyAsync | Updates multiple documents based on a filter and returns the UploadResult. |
UpdateOne UpdateOneAsync | Updates one document based on a filter and, as a result, returns the UpdateResult. |
ReplaceOne ReplaceOneAsync | Replaces an entire document. |
FindOneAndReplace FindOneAndReplaceAsync | Replaces one document based on a filter and, as a result, returns the replaced document before or after the change. |
At first sight, the two methods FindOneAndUpdate and UpdateOne might seem to be pretty much identical; however, their use cases might differ. One returns a full document before or after the update operation, while the other just returns the information about the operation itself. If the data does not need to be returned, then UpdateOne is probably more efficient, as it doesn’t have to perform another query and return data.
Updating a document doesn’t differ very much from what we have already seen, as the patterns are pretty much the same: use the Builders object to define the query on which the update operation will be performed, and at the same time, use the Builders object to define what kind of update operation will be applied.
Let’s go through the three different methods and show how it works in practice. The following example uses the UpdateOne method. As mentioned previously, we can see that in order to update the document by using the Builers<T>.Filter, we need to specify the query to find the document to be updated. Additionally, the interesting part is to define the update statement that happens through the Set() method, which can be written by using either the lambda expression and the value (as specified for the Year), or by manually supplying the name and the value. It is possible to specify more than one update by chaining multiple Set methods.
Code Listing 102: Updating a movie
public static void UpdateMovie(string dbName, string collName) { var db = DatabaseHelper.GetDatabaseReference("localhost", dbName); var collection = db.GetCollection<Movie>(collName);
var builder = Builders<Movie>.Filter; var filter = builder.Eq("name", "The Godfather"); var update = Builders<Movie>.Update UpdateResult result = collection.UpdateOne(filter, update);
Console.WriteLine(result.ToBsonDocument()); } |
UpdateMany comes with exactly the same signature; however, it would update multiple documents at the time.
On the other side, FindOneAndUpdate becomes quite interesting with the options it can be supplied with. It makes it possible to do the following:
Here is an example of using the FindOneAndUpdate method.
Code Listing 103: Example of using the FindOneAndUpdate
var filter = Builders<Movie>.Filter.Eq("name", "The Godfather"); var update = Builders<Movie>.Update var updateOptions = new FindOneAndUpdateOptions<Movie, Movie>() { ReturnDocument = ReturnDocument.After, Projection = Builders<Movie> .Projection .Include(x => x.Year) .Include(x => x.Name) }; Movie movie = collection.FindOneAndUpdate(filter, update, updateOptions); |
As part of the update options, we have specified the Projection, which means that only the attributes specified as part of it will be returned by the method after the movie has been updated. So, when the Movie object is returned, it will be filled in only with the _id, year, and name. The rest of the attributes will have the default value. It is possible to choose between ReturnDocument.After and ReturnDocument.Before, which are the instructions to return the status of the movie before or after the update, respectively.
We use the ReplaceOne method to replace the entire document. Bear in mind that the _id field cannot be replaced, as it is immutable. The replacement document can have fields different from the original document. In the replacement document, you can omit the _id field, since the _id field is immutable. If you do include the _id field, it must be the same value as the existing value.
In the following example, we are showing how to replace a document. First, we are retrieving an already existing document from the database that will be replaced. In addition, there is a new instance of the Movie, which happens to be a different movie from the original. At the end, we call the ReplaceOneAsync method.
ReplaceOneAsync returns the ReplaceOneResult object, which contains properties such as MatchedCount and ModifiedCount that tell if the object to be modified has been found and modified.
Code Listing 104: Example of ReplaceOneAsync usage
var collection = db.GetCollection<Movie>(collName);
var builder = Builders<Movie>.Filter; var filter = builder.Eq("name", "The Godfather");
//find the ID of the Godfather movie... var theGodfather = await collection.FindAsync(filter); var theGodfatherMovie = theGodfather.FirstOrDefault();
Movie replacementMovie = new Movie { MovieId = theGodfatherMovie.MovieId, Name = "Mad Max: Fury Road", Year = 2015, Actors = new[] { new Actor {Name = "Tom Hardy"}, new Actor {Name = "Charlize Theron"}, }, Director = "George Miller" }; ReplaceOneResult r = await collection.ReplaceOneAsync(filter, replacementMovie);
Console.WriteLine(r.ToBsonDocument()); |
In a very similar way to the update options, the MongoDB C# driver supports a few ways to delete data. The following methods on the collection can make it quite easy to delete the already existing documents:
Table 18: Methods that delete a document
FindOneAndDelete FindOneAndDeleteAsync | Deletes the first document in the collection that matches the filter. The sort parameter can be used to influence which document is updated. |
DeleteMany DeleteManyAsync | Removes all documents that match the filter from a collection. |
DeleteOne DeleteOneAsync | Deletes the first matching document based on a filter and, as a result, returns the UpdateResult. |
DeleteOne and DeleteMany are pretty similar in their implementation. Both return the DeleteResult, which will inform us of the successful removal of the document through its DeletedCount property. Both of them accept either an expression or, as we have seen previously, a FilterDefinition that can be constructed using the Builders mechanism.
The following example uses the lambda expression as the DeleteOneAsync parameter:
Code Listing 105: Example of using the DeleteOneAsync and DeleteManyAsync
var collection = db.GetCollection<Movie>(collName);
DeleteResult result = await collection.DeleteOneAsync(m => m.Name == "The Seven Samurai"); //or
var result = await collection.DeleteManyAsync(m => m.Name == "The Seven Samurai" || m.Name == "Cabaret"); |
Here is an example that uses the Builders:
Code Listing 106: DeleteManyAsync with the specified filter
var collection = db.GetCollection<Movie>(collName);
var builder = Builders<Movie>.Filter; var filter = builder.Eq("name", "The Godfather"); var result = await collection.DeleteManyAsync(filter); |
FindOneAndDelete is a bit different, as it also offers the possibility to return the data as part of the deleting operation. It accepts the FindOneAndDeleteOptions to be passed in, with the ability to specify the sorting, which is the sorting on the collection before the delete happens (remember, only one document will be deleted), and the projection, which will return the data of the deleted document (obviously in the state it was in before being deleted).
Here’s an example of how to use the FindOneAndDeleteAsync method:
Code Listing 107: FindOneAndDeleteAsync with options defined
This will return a BsonDocument that contains only the attribute _id.
In this lengthy chapter, we have seen the most important aspects when it comes to manipulating data in MongoDB. Certainly, not every possible mechanism has been mentioned, as there are many other hidden features. I tried to illustrate the features that are going to be used the most and that have patterns we can follow.
After reading this chapter, the hope is that you have become aware of various possibilities and techniques for effectively using the MongoDB driver.