CHAPTER 7
Before starting with various examples, we should spend a few words on the BSON representation of the data as stored in the MongoDB database, which is achieved through the BsonDocument object. There is a very high chance of encountering the BsonDocument when working with the driver, so it’s crucial to understand what it is and what it represents.
We can think of BsonDocument as an equivalent to a table row in an RDBMS database, as it represents the document as it is stored in the database. This is also a way to dynamically express the data being returned or passed to the database.
The following code shows an example of creating a new BsonDocument, in our case representing the movie The Seven Samurai. As we would expect, the document is declaring the attributes Name, Director, Actors, and Year, with the respective values. The array of values is being injected by using another structure, the BsonArray object, and the items of the array are themselves declared as BsonDocument (a sort of a subdocument).
Code Listing 64: BsonDocument creation
BsonDocument sevenSamurai = new BsonDocument() { { "Name" , "The Seven Samurai" }, { "Director" , "Akira Kurosawa" }, { "Actors", new BsonArray { new BsonDocument("Name", "Toshiro Mifune"), new BsonDocument("Name", "Takashi Shimura")}}, { "Year" , 1954 } }; |
In the MongoDB C# driver, using the BsonDocument as a means of sending or returning data is perfectly legal, and is fully supported. BsonDocument can be directly used when we want to work on a “lower” level with the data, without the need for deserialization seen in other formats, such as POCO (“plain old CLR object”), or when the schema is fluid and dynamic. However, most applications are built with a schema modeled in the application itself rather than the database. In these cases, it is likely that the application uses classes. Therefore, an alternative to the BsonDocument is to work directly with C# POCO objects.
Code Listing 65: C# representation of the BsonDocument
public class Movie { public string MovieId { get; set; } public string Name { get; set; } public string Director { get; set; } public Actor[] Actors { get; set; } public int Year { get; set; } }
public class Actor { public string Name { get; set; } } |
The following instance of the Movie class corresponds exactly to the previously shown BsonDocument that represents the same movie.
Code Listing 66: Creation of a movie object in C#
Movie sevenSamurai = new Movie() { Name = "Seven Samurai", Director = "Akira Kurosawa", Year = 1954, Actors = new Actor[]() { new Actor { Name = "Toshiro Mifune"}, new Actor { Name = "Takashi Shimura"}, } }; |
The MongoDB driver offers a few ways of controlling the mapping of the values from the database to the POCO class, and vice versa. This is very important in case the names of the attributes as shown in the class differ from what is really stored (persisted) in the database, so it is crucial that there is a mechanism through which we can control exactly what comes in and out.
Serialization is the process of mapping an object to a BSON document that can be saved in MongoDB, and deserialization is the reverse process of reconstructing an object from a BSON document. For that reason, the serialization process is also often referred to as object mapping. The default BSON serializer available as part of the driver will take care of this conversion under the hood.
The MongoDB driver mainly supports two ways of mapping properties from and to the BSON representation: using the .NET attributes assigned to the class members, and using the BsonClassMap class.
We will use the previous Media and Actor classes in order to show the various options. One of the most frequently used options is property mapping, which means making sure that each member of the class is mapped to a particular value, but is not limited to it.
Let’s see what the Movie class would look like when “decorated” with some extra (previously mentioned) attributes. Please note that there are two additional attributes, Age and Metadata, which will be explained further down the line.
Code Listing 67: POCO object with BSON attributes
[BsonIgnoreExtraElements] public class Movie { [BsonId(IdGenerator = typeof(StringObjectIdGenerator))] public string MovieId { get; set; }
[BsonElement("name")] public string Name { get; set; }
[BsonElement("directorName")] public string Director { get; set; }
[BsonElement("actors")] public Actor[] Actors { get; set; }
[BsonElement("year")] public int Year { get; set; }
[BsonIgnore] public int Age { get { return DateTime.Now.Year - this.Year; } }
[BsonExtraElements] public BsonDocument Metadata { get; set; } } |
An alternative to these attributes is the BsonClassMap, which is the entry point for the mapping.
Tip: BsonClassMap should be called only once per application.
The BsonElement attribute will make sure to map the class property to a given name, which means that when the class gets (de)serialized, the names specified as the parameter will be used. As an example, the property Director, when serialized, will actually be stored in MongoDB as directorName rather than with the property name that would be the default behavior.
If we were to use the BsonClassMap, we could achieve the same property-name mapping by chaining the SetElementName method on the MapProperty method.
Code Listing 68: Mapping done via BsonClassMap
BsonClassMap.RegisterClassMap<Movie>(movie => { movie.MapProperty(p => p.Name).SetElementName("name"); movie.MapProperty(p => p.Director).SetElementName("directorName"); movie.MapProperty(p => p.Year).SetElementName("year"); movie.MapProperty(p => p.Actors).SetElementName("actors"); }); |
The BsonId attribute does mainly two things. It makes the property act as a primary key of the class and, on the other side, it allows us to assign the IdGenerator to the property. In our case, it maps the MovieId property, which (when serialized) will be transformed into the standard _id, as we have seen previously. IdGenerator is responsible for assigning the new value to the class when this is serialized. There are several serializers that are already part of the MongoDB C# driver, such as BsonObjectIdGenerator, CombGuidGenerator, GuidGenerator, ObjectIdGenerator, StringObjectIdGenerator, and ZeroIdChecker<T>. We are using the StringObjectIdGenerator, as in our case the MovieId is of type string; if it were of type ObjectID, then we would need to choose among the other generators.
We can also create our own unique key generators by implementing the IIdGenerator interface. What follows is a very naïve implementation of the primary key where we use the Movie_ prefix followed by a Guid:
Code Listing 69: Example of a custom ID generator
public class MovieIdGenerator : IIdGenerator { public object GenerateId(object container, object document) {
return "Movie_" + System.Guid.NewGuid().ToString(); }
public bool IsEmpty(object id) { return id == null || string.IsNullOrEmpty(id.ToString()); } } |
Which then would be used as follows:
Code Listing 70: Using the custom ID generator via attributes
public class Movie { [BsonId(IdGenerator = typeof(MovieIdGenerator))] public string MovieId { get; set; } … |
If we were to use the mapping class, then instead of using the MapProperty method, we would use the MapIdProperty. MapIdProperty has the ability to set the IdGenerator by using the SetIdGenerator method.
Code Listing 71: Setting the custom generator in the BsonClassMap
BsonClassMap.RegisterClassMap<Movie>(movie => { movie.MapIdProperty(p => p.MovieId).SetIdGenerator(new MovieIdGenerator()); }); |
BsonIgnore will simply make sure that the BSON serializer will ignore and not serialize the element to the BSON format. So, in our case the Age property will be never stored into the database, and the mapper will simply not fill this property if the value is available in the database. The corresponding notation of the BsonClassMap uses the UnmapProperty method.
Code Listing 72: Making the attribute not BSON serializable (ignored)
BsonClassMap.RegisterClassMap<Movie>( movie => { movie.UnmapProperty(p => p.Age); }); |
When a BSON document is deserialized back to a POCO, the name of each element is used to look up a matching field or property; when deserializer doesn’t find the mapping property, it throws an exception. This is when the BsonIgnoreExtraElements attribute becomes very handy, as it will ignore those extra properties and won’t try to link them back to the class.
The SetIgnoreExtraElements method on BsonClassMap achieves the same.
Code Listing 73: Instructing the serializer to ignore extra elements
BsonClassMap.RegisterClassMap<Movie>( movie => { movie.SetIgnoreExtraElements(true); }); |
This attribute is one of the two ways of allowing the mapping of extra elements (those that are not part of the original object) dynamically. In fact, this is the way to mix static and dynamic data. In order for this to work, the dynamic property in a class should be of type BsonDocument. In our Movie class, we have the Metadata property that is of type BsonDocument.
The corresponding BsonClassMap method to be used is the MapExtraElementsMember.
Code Listing 74: Enabling the mapping of extra elements
BsonClassMap.RegisterClassMap<Movie>( movie => { movie.MapExtraElementsMember(p => p.Metadata); }); |
The following is the example of how the movie will be serialized if we specify the Metadata as a new BsonDocument.
Code Listing 75: Instance of a movie to be serialized
Movie theGodFather = new Movie() { Name = "The Godfather", Director = "Francis Ford Coppola", Year = 1972, Actors = new Actor[] { new Actor { Name = "Marlon Brando" }, new Actor { Name = "Al Pacino" }, }, Metadata = new BsonDocument("href", "http://thegodfather.com") }; |
It will be stored in the database, such as the following:
Code Listing 76: Serialized movie
{ "_id" : "587a4496c6d11b31a0a6b829", "name" : "The Godfather", "directorName" : "Francis Ford Coppola", "actors" : [ { "Name" : "Marlon Brando" }, { "Name" : "Al Pacino" } ], "year" : 1972, "href" : "http://thegodfather.com" } |
We can clearly see that the href looks like an ordinary attribute, and the Metadata property, as it is in the C# file, is not even mentioned.
If we would omit the BsonExtraElements attribute, then the class would be serialized as follows:
Code Listing 77: Serialized movie without BsonExtraElements specified
{ "_id" : "587a45d9c6d11b40944c32f6", "name" : "The Godfather", "directorName" : "Francis Ford Coppola", "actors" : [ { "Name" : "Marlon Brando" }, { "Name" : "Al Pacino" } ], "year" : 1972, "Metadata" : { "href" : "http://thegodfather.com" } } |
Please note the difference from the previous example. Now, the Metadata attribute is shown.