In this console application, I'll demonstrate two possible mechanisms to retrieve data from the retrogames
MongoDB database. One of the possibilities consists of creating your own C# classes to represent the documents in the solution and perform the necessary mapping tweaks. So when you read a document from the database, you get back a C# object, known as a POCO (short for "Plain Old C# Object") instance. The driver transforms the BSON document into the POCO instance through deserialization. When you create a new POCO instance to add a new document to the database, the driver transforms the POCO instance you created to a BSON document through serialization. (When you chose to go this route, you might have some issues with the underlying schema flexibility. There is another alternative that preserves the schema flexibility and doesn't require your own domain classes by working with plain BsonDocument
instances,I'll dive into that option later.)
For this example, I'll focus on creating the domain classes. First, add a new interface, IMongoEntity
:
namespace RetrogamesConsole { using System; using MongoDB.Bson; public interface IMongoEntity { ObjectId Id { get; set; } } }
The IMongoEntity
interface defines an Id field of the MongoDB.Bson.ObjectId
type that represents a BSON ObjectId
. Next, add a new class, MongoEntity
, that implements the previously created IMongoEntity
interface:
namespace RetrogamesConsole { using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; public class MongoEntity { [BsonId] public ObjectId Id { get; set; } } }
The MongoEntity
class uses the MongoDB.Bson.Serialization.Attributes.BsonIdAttribute (BsonId)
attribute to specify that the Id
field must be mapped to the _id
field for each document during serialization and deserialization.
If you take a look at the previously explained Text View for the games
collection in MongoVUE, you will be able to determine the fields required in a C# class to represent a game
document. The following lines show the JSON text of the existing document as shown in the Text View:
/* 0 */ { "_id" : ObjectId("513a90ec507f318c7d15c744"), "name" : "Invaders 2013", "release_date" : ISODate("2013-04-02T03:00:00Z"), "categories" : ["space", "shooter", "remake"], "played" : true }
Add a new class, Game
, that inherits from MongoEntity
:
namespace RetrogamesConsole { using System; using System.Collections.Generic; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; [BsonIgnoreExtraElements] public class Game : MongoEntity { public Game() { Categories = new List<string>(); } [BsonElement("name")] public string Name { get; set; } [BsonElement("release_date")] public DateTime ReleaseDate { get; set; } [BsonElement("categories")] public List<string> Categories { get; set; } [BsonElement("played")] public bool Played { get; set; } } }
The Game
class uses the MongoDB.Bson.Serialization.Attributes.BsonElementAttribute (BsonElement)
attribute to specify the BSON document's field name, which has to be mapped to the property. Thus, the underlying serialization process knows that the value for release_date
is mapped to the ReleaseDate
property. In this case, I've used different names and cases in the class; therefore, it was necessary to annotate it with attributes to configure automatic serialization. However, if you use the same names in the document's fields and in the class, you won't need to add annotations.
There are other attributes that allow you to override the automatic serialization behavior for the different types. For example, you can force the BSON representation of a .NET type to BSON Int64 by using the following annotation:
[BsonRepresentation(BsonType.Int64)]
When you use annotations, your domain classes are not independent of the persistent layer. To avoid that situation, it is also possible to configure serialization in code instead of using attributes. For example, the following lines are the equivalent serialization configuration of the previously shown attributes for the Game
class that use the methods of the MongoDB.Bson.Serialization.BsonClassMap
class:
BsonClassMap.RegisterClassMap<Game>(g => { g.AutoMap(); g.SetIgnoreExtraElements(true); g.SetIdMember(g.GetMemberMap(x => x.Id)); g.GetMemberMap(x => x.Name).SetElementName("name"); g.GetMemberMap(x => x.ReleaseDate).SetElementName("release_date"); g.GetMemberMap(x => x.Categories).SetElementName("categories"); g.GetMemberMap(x => x.Played).SetElementName("played"); });
The following lines show the code for Program.cs
, which uses the FindOne
method to retrieve the first document in the games
collection that includes a name
field with the value equal to "Invaders 2013
." I haven't included all the necessary exception handling code in order to keep the example as simple as possible. The driver transforms the BSON document to an instance of the Game
class through deserialization (taking into account the annotations), as shown in Figure 10:
namespace RetrogamesConsole { using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Driver.Builders; using MongoDB.Driver.GridFS; using MongoDB.Driver.Linq; class Program { public static void Main(string[] args) { //// Warning: The following code is an example without the appropriate //// Exception handling var connectionString = "mongodb://localhost"; //// Get a thread-safe client object by using a connection string var mongoClient = new MongoClient(connectionString); //// Get a reference to a server object from the Mongo client object var mongoServer = mongoClient.GetServer(); //// Get a reference to the "retrogames" database object from the Mongo server object var databaseName = "retrogames"; var db = mongoServer.GetDatabase(databaseName); //// Get a reference to the "games" collection object from the Mongo database object var games = db.GetCollection<Game>("games"); var gameQuery = Query<Game>.EQ(g => g.Name, "Invaders 2013"); var foundGame = games.FindOne(gameQuery); } } }
Figure 10: Inspecting the Game instance (foundGame) retrieved from the MongoDB retrogames database in Visual Studio 2012.
Understanding the Different Query Builder Alternatives
The previously shown code is equivalent to the following JavaScript commands in the MongoDB shell:
use retrogames db.games.findOne({ name: "Invaders 2013"})
The code gets a thread-safe client object with a simple connection string that doesn't specify a port because the server is running in localhost
using the default port: 27017
. It isn't necessary to call any methods to either connect or disconnect to MongoDB because the driver automatically manages a connection pool. The code simply gets a reference to the MongoDB server object and uses it to get a reference to the retrogames
database.
The following three lines get a reference to the games
collection object, use the typed query builder (Query<Game>
) to build a query that the driver will translate to an equivalent MongoDB query at runtime, and finally call the FindOne
method with the typed query (gameQuery
) as a parameter:
var games = db.GetCollection<Game>("games"); var gameQuery = Query<Game>.EQ(g => g.Name, "Invaders 2013"); var foundGame = games.FindOne(gameQuery);
The typed query builder is both type-aware and type safe; therefore, it allows you to use the properties defined in your domain classes to build the query. It is also possible to use the untyped query builder, but it requires you to work with the underlying field names. For example, the following lines use the untyped query builder to generate the same results as the previously shown lines:
var games = db.GetCollection<Game>("games"); var gameQuery = Query.EQ("name", "Invaders 2013"); var foundGame = games.FindOne(gameQuery);
The methods provided by the untyped query builder require an element name; so if you provide the wrong element name, the query's execution will fail. Coding in the untyped query builder is definitely error-prone.