How to MongoDB in C# – Part 1

Every new project holds new surprises and adventures, but imagine my surprise when realizing I am the victim who is going to work with MongoDB. As a C# developer who is accustomed to SQL server, I was like:

Say whaaa?

So.. when the news were digested, I did the next best thing – googled “MongoDB” ( also binged 😉 )

A few hours later, I was frustrated with my findings. There were a lot of examples on how to work my way up the old legacy driver, but very few ones with the new C# driver API.

And so I decided to write this blog-post series on “How to MongoDB in C#”, to save you the frustration part of my journey.

This post will briefly talk about how to setup MongoDB etc. (there are more then plenty tutorials concerning that topic). But, in the ones to follow, I will be focusing on the different ways MongoDB’s CRUD API has to offer, both standard and advanced (called Aggregation-FW). In order to reach the interesting stuff, we will have to setup our development environment.


Introduction

Consider the following JSON structure:

{
 _id:1,
 name:"John smith"
 company_name:"Bloggy-dev",
 knowledge_base:[
   {
     langu_name:"C#",
     technology:"WPF",
     rating:"9"
   },
   {
     langu_name:"C#",
     technology:"TPL",
     rating:"7"
   },
   {
     langu_name:"C++",
     technology:"QT",
     rating:"8"
   }
 ]
}

This JSON structure will represent our “Developer” object inside the DB (well actually, MongoDB stores objects as BSON objects which are a binary JSON, but that is a topic for a different blog post).

In MongoDB, everything is considered to be a document (or to be precise, a BsonDocument – in the C# API. Later on, I will explain the correlation between our strongly typed Developer class and MongoDB’s BsonDocument).

So here is our corresponding C# class representation of the JSON object:

public class Developer
{
 [BsonId]
 public ObjectId ID { get; set; }
 [BsonElement("name")]
 public string Name { get; set; }
 [BsonElement("company_name")]
 public string CompanyName { get; set; }
 [BsonElement("knowledge_base")]
 public List<Knowledge> KnowledgeBase { get; set; }
}

public class Knowledge
{
 [BsonElement("langu_name")]
 public string Language { get; set; }
 [BsonElement("technology")]
 public string Technology { get; set; } 
 [BsonElement("rating")]
 public ushort Rating { get; set; }
}

And this is how a developer object will look like:
Initial insert of developer object

Throughout this post-series, we will be using a variety of OzCode’s finest 🙂

We are going to use MongoDB’s CRUD operation to work with a lot (and I mean, A LOT) of data, so we better equip our selves with the proper debugging tools.

I will be demonstrating how to search through a retrieved collection of objects using the Search feature , how to filter them and display only those which answer specific criteria using Collection-Filtering and even export the huge amount of data using Export (see in action starting from the 26:30 minute)

In the screenshot above I’m using OzCode’s awesome Reveal feature to better visualize the debug experience, otherwise I would need to wrap my POCO objects with a DebuggerDisplayAttribute or even worse, building a debugger proxy using DebuggerTypeProxyAttribute. (Developer class debug-display can be configured by selecting the relevant asterisks)

About that MongoDB’s object mapping

There is a bunch of new stuff you are probably wondering about, let’s go over it.

As you’ve probably noticed, I don’t have to match my C# class properties to the JSON object, MongoDB offers two different ways to achieve that during its store and retrieve operations:

  1. Automatic” – which is attribute-based. there is also a way of defining system-wide general conventions using ConventionPack
  2. Manual” – we can configure each property using BsonClassMap, RegisterClassMap static method

Using automatic mapping

About those attributes:

  • BsonId, lets you specify which property will be act as a unique ID for your document.
  • BsonElementAttribute, lets you tweak the name of your property and tells MongoDB under which property key it should be stored inside the JSON.

A few things worth of mentioning regarding the unique ID property :

  1. MongoDB will always store your ID property as “_id” inside the DB, it cannot be changed.
    • Trying to alter the name using BsonElementAttribute will be simply ignored.
  2. You don’t have to specify a property to act as a unique ID, MongoDB will figure out you are missing an ID property and auto-generate an ObjectId based “_id” for you.
    public class Developer
    {
     [BsonElement("name")]
     public string Name { get; set; }
     [BsonElement("company_name")]
     public string CompanyName { get; set; }
     [BsonElement("knowledge_base")]
     public List<Knowledge> KnowledgeBase { get; set; }
    }
    

    Here as you can see, our Developer class is missing its ID property (the last property in the tree is missing).
    Developer inserted without ID within C#

    I’m using MongoDB ‘s client (ships with the installation), to retrieve the inserted Developer object. Although we didn’t add an ID property, you can clearly see MongoDB has added it for us:
    Developer inserted without ID

    **Be aware – when retrieving this object back into your C# application, MongoDB will throw an exception telling you an ID property is missing. If You decide to choose this strategy for your application, you must mark your class with BsonIgnoreExtraElements

    [BsonIgnoreExtraElements]
    public class Developer
    {
     [BsonElement("name")]
     public string Name { get; set; }
     [BsonElement("company_name")]
     public string CompanyName { get; set; }
     [BsonElement("knowledge_base")]
     public List<Knowledge> KnowledgeBase { get; set; } } 
  3. ID property can be of a complex type – this is one of the cases when MongoDB won’t auto-generate an ID for you and uses the default value of your class/struct
    public class Developer
    {
     [BsonId]
     public CustomID ID { get; set; }
     [BsonElement("name")]
     public string Name { get; set; }
     [BsonElement("company_name")]
     public string CompanyName { get; set; }
     [BsonElement("knowledge_base")]
     public List<Knowledge> KnowledgeBase { get; set; }
    }
    
    public class CustomID
    {
     public DateTime GraduationDate { get; set; }
     public decimal GPA { get; set; }
     public string Gender { get; set; }
    }
    

    Complex custom ID

     

  4. If you do decide to decorate your class with an ID property, you are not obligated to use MongoDB ‘s ObjectId struct. Actually, the ID can be of different types, such as string, int or even GUID. The main thing to remember is that MongoDB assigns an Index of type Unique to it’s “_id” field. So make sure you add an entry with a unique ID value, because same as in remark #3 it will use the default value for your ID type (Except GUID type – MongoDB will auto generate values for it), and will throw an exception when trying to add an entry with the same default ID value.
  5. You can use an IdGenerator of type IIdGenerator, as a BsonIdAttribute parameter. There are few build-in IdGenerator which you can leverage from such as the StringObjectIdGenerator in case you don’t want to use the ObjectId struct. Passing this type to your BsonIdAttribute, will store ObjectId’s string representation onto your ID property.
     [BsonId(IdGenerator = typeof(StringObjectIdGenerator))]
     public string ID { get; set; }
    

    Or a simple custom ID generator

    public class Developer
    {
     [BsonId(IdGenerator = typeof(CounterIdGenerator))]
     public int ID { get; set; }
     .
     .
     .
    }
    
    public class CounterIdGenerator : IIdGenerator
    {
     private static int _counter = 0;
     public object GenerateId(object container, object document)
     {
      return _counter++;
     }
    
     public bool IsEmpty(object id)
     {
      return id.Equals(default(int));
     }
    }
    

    The way it works, for each document MongoDB tries to insert, it will pass the ID value into IsEmpty method, returning true will pass the document to GenerateId method, which accepts a container (the Collection responsible for your document’s CRUD operations, amount other stuff) and the document itself.

Using MongoDB’s automatic conventions

One last thing – I have mentioned earlier we can generalize our application using  ConventionPack.

Consider the following example: your team decides all property names will be stored as lower cased and an underscore will separate between each capital letter inside MongoDB (CompanyName becomes company_name). This type of decision will require you to go over each document and its properties, and apply the name convention. What if you refactor your class? what if a new convention will be set by your team?

Luckily (!!) MongoDB offers a convention based API.

You can create a ConventionPack instance, which is basically a collection of IConvention types (MongoDB offers a wide range of predefined conventions for you which can be found here). You can then add as many conventions as you’d like, and register the ConventionPack within the “ConventionRegistry.Register” static method.

The method accepts 3 parameters:

  1. Name for your convention pack
  2. The ConventionPack to be used
  3. Func<Type,bool> filter, to which types this convention doesn’t apply to (simply pass a “t => true” to apply for all types)
static void Main(string[] args)
{
 SetConventions();
 Developer d = CreatePreviewOnlyDeveloper();
 InsertDeveloperToDb(d);
}

private static void SetConventions()
{
 ConventionPack cp = new ConventionPack
 {
  new SeperateWordsNamingConvention(),
  new LowerCaseElementNameConvetion()
 };
 ConventionRegistry.Register("dev", cp, type => typeof(Developer) == type);
}

private class LowerCaseElementNameConvetion : IMemberMapConvention
{
 public string Name => nameof(LowerCaseElementNameConvetion);
 public void Apply(BsonMemberMap memberMap)
 {
  memberMap.SetElementName(memberMap.ElementName.ToLower());
 }
}

private class SeperateWordsNamingConvention : IMemberMapConvention
{
 //Taken from:
 //http://stackoverflow.com/questions/4488969/split-a-string-by-capital-letters?answertab=votes#tab-top
 private static readonly Regex s_seperateWordRegex =
  new Regex(@"
   (?<=[A-Z])(?=[A-Z][a-z]) |
   (?<=[^A-Z])(?=[A-Z]) |
   (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace); 

 public string Name => nameof(SeperateWordsNamingConvention);
 public void Apply(BsonMemberMap memberMap)
 {
  var replace = s_seperateWordRegex.Replace(memberMap.ElementName, "_");
  memberMap.SetElementName(replace);
 }
}

I decided to implement the 2 conventions I had described earlier based on IMemberMapConvetion (derives from IConvention), and apply them only to the Developer class.  Now our Developer class looks a bit cleaner:

public class Developer
{
 [BsonId]
 public ObjectId ID { get; set; }
 public string Name { get; set; }
 public string CompanyName { get; set; }
 public List<Knowledge> KnowledgeBase { get; set; }
}

Oh yeah, there is also the Manual way of things

This is addressed to all those coders, who loveeee to type :). As you might imagine, anything which can be done in the automated (attribute-based) way can be done in the manual way. I Won’t elaborate much on this part, because this manual has a pretty decent tutorial on how to work with the manual way (and the automated way, in a much MORE detailed-oriented way).

 

So…. we covered so far the top-most basics of:

  1. The relationship between JSON object and it’s twin C# brother, the Document.
  2. How to setup and customize your class using attributes
  3. ID property highlights – from MongoDB ‘s perspective

What’s next

But there’s still a lot to cover during our dive into MongoDB , in future posts I hope to cover the following:

  • How to setup your DB connection
  • How to instantiate the MongoClient, MongoDatabase and MongoCollection classes
  • Analysing CRUD operations results using OzCode’s Search, Collection Filter and Export features

and anything else that I come up with in the meantime, Stay tuned..

Stas Rivkin

Software consultant @CodeValue & a @OzCode evangelist. He has been writing software professionally for about 5 years, mostly in C# and .Net. Considers Clean Code as a form of an art.

  • Alex

    Great post!