Channels ▼
RSS

Tools

MongoDB with C#


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);
        }
    }
}

Inspecting the Game instance in MongoDB/Visual Studio 2012
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.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video