Convert C# classes to and from MongoDB Documents automatically using .NET reflection
There are a number of C# based MongoDB projects being actively developed right now but one thing that I needed was a way to convert a standard C# class to a MongoDB document for easy insertion. It isn’t hard to manually type out and set each property by hand, but it certainly is not the most efficient way, especially when you know you are going to be doing it a lot.
For example:
Lets say I have a class called SomeClass that looks something like this:
class SomeClass { public string StringTest; public int IntTest; }
And somewhere in my code, I have an instance of this class named someClassInstance. If I want to create a MongoDB document from this class, I’d have to do something like this:
Document document = new Document(); document.add('StringTest', someClassInstance.StringTest); document.add('IntTest', someClassInstance.IntTest);
So that isn’t such a big deal, right? But what about when I have a class with many more properties? Then it starts to get messy and cumbersome. I thought that there should be an straightforward way to easily convert any class to a mongo-csharp compatible Document object. (I am using Sam Corder’s mongo-csharp driver, so that I am targeting the Document object from that library.)
Default values
I also wanted to have a way to specify what the default values were for each class property so that when we did the conversion, we would (hopefully) not end up with any null values. Plus, if for some reason there was a document in MongoDB that was missing a particular key-value pair, the DocumentConverter would automatically fill in that empty field with the default value so in the code we should never have any nulls.
This is something that I would like for my own purposes and may not suit everyone’s needs. If it doesn’t, simply leave off the DefaultValueAttribute and you’ll never know the difference.
My proposed solution
I figured the easiest way to accomplish this was to create a class that would encapsulate all the functionality needed to convert to and from Document objects and have my other classes inherit from that one. I imagined that the above code would change to something like this:
class SomeClass : DocumentConverter { [Attributes.DefaultValue("Default StringTest value!")] public string StringTest; [Attributes.DefaultValue(16)] public int IntTest; }
And to do the conversion would be very simple. To convert from someClass to Document would be:
Document document = someClassInstance.ToMongoDocument();
To convert from a Document object to someClass would be:
SomeClass someOtherClassInstance = new SomeClass(); omeOtherClassInstance .FromMongoDocument(someDocumentObject);
The DefaultAttribute class
using System;
namespace MyApp.Attributes { [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] class DefaultValueAttribute : Attribute { private readonly object _value; public DefaultValueAttribute(object Value) { _value = Value; } public object GetDefaultValue() { return _value; } } }
The DocumentConverter class
Reflection isn’t something that I use too often so there may be better ways of accomplishing what I am trying to do, but this is what I’ve got for now. If there are better ways, please let me know. Without further ado…
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using MyApp.Classes.Attributes; using MongoDB.Driver; namespace MyApp.Classes { public class DocumentConverter { public void FromMongoDocument(Document document) { foreach (DictionaryEntry kvp in document) { object propertyValue; if (kvp.Value != null && (kvp.Value.GetType() == typeof(Document))) { // We have a document object - Now lets get a reference to the class property's type var propertyType = GetType().GetProperty(kvp.Key.ToString()).PropertyType; // create new instance of that class var propertyInstance = Activator.CreateInstance(propertyType); // call FromMongoDocument on that class and pass in the document MethodInfo method = propertyInstance.GetType().GetMethod("FromMongoDocument"); method.Invoke(propertyInstance, new[] { kvp.Value }); propertyValue = propertyInstance; } else { // This is not a Document so lets just assign the value propertyValue = kvp.Value; } GetType().GetProperty(kvp.Key.ToString()).SetValue(this, propertyValue, null); } } public Document ToMongoDocument() { Document document = new Document(); foreach (PropertyInfo property in GetType().GetProperties()) { // Get the value of this property object propertyValue = property.GetValue(this, null); // If this value is null, then lets try to see if there is a default value attribute and assign that if (propertyValue == null) { object[] attributes = property.GetCustomAttributes(typeof(DefaultValueAttribute), true); foreach (DefaultValueAttribute defaultValue in attributes.Cast<defaultvalueattribute>()) { propertyValue = defaultValue.GetDefaultValue(); } document.Add(property.Name, propertyValue); } else { // We have a property, now lets see if this property has a ToMongoDocument method MethodInfo method = propertyValue.GetType().GetMethod("ToMongoDocument"); if (method == null) { document.Add(property.Name, property.GetValue(this, null)); } else { document.Add(property.Name, method.Invoke(propertyValue, null)); } } } return document; } } }
That’s all for now
I hope this is useful for someone. It is a rough draft of what I threw together last night at around 1AM while half asleep. So far, it has passed all of my initial tests but if you have suggestions to make it better, please leave some comments here.
|
|
|
|
|
![]() |












February 25th, 2010 at 1:30 pm
Very cool!
I would suggest that rather than requiring the class be derived from DocumentConverter, you make DocumentCoverter a static class with FromMongoDocument and ToMongoDocument as generic extension methods. That way, any object within a hierarchy can be saved/loaded.
You can then also speed up (de)serialization by caching some of the reflection information in a static member, say in a Dictionary.
March 9th, 2010 at 9:28 pm
The System.ComponentModel Namespace already has a DefaultValueAttribute.
http://msdn.microsoft.com/en-us/library/system.componentmodel.defaultvalueattribute.aspx
March 19th, 2010 at 3:31 pm
This is a very useful class and I have been using this for last couple of days. I am difficulty using this class for collection data types. For example, if I embed comments collection in Article document then I am able retrieve comments if there is only one but there are more than one then it is not able to handle.
Do you have any ideas how to go about doing this.
Thanks again for great work!
-tk
May 14th, 2010 at 1:54 pm
when I try to insert the document after to convert the object, I had the following exception:
Unhandled Exception: System.ArgumentOutOfRangeException: Specified argument was
out of the range of valid values.
Parameter name: Type: System.UInt16 not recognized
at MongoDB.Driver.Bson.BsonWriter.TranslateToBsonType(Object val)
at MongoDB.Driver.Bson.BsonWriter.CalculateSize(Object val)
at MongoDB.Driver.Bson.BsonWriter.CalculateSize(Document doc)
at MongoDB.Driver.Protocol.InsertMessage.ChunkMessage(BsonWriter writer)
at MongoDB.Driver.Protocol.InsertMessage.Write(Stream stream)
at MongoDB.Driver.Connections.Connection.SendMessage(IRequestMessage msg)
at MongoDB.Driver.Collection.Insert(IEnumerable`1 docs)
at AppSecInc.MongoDB.Test.MongoDBUtil`1.Insert(Object entity) in C:\Users\jch
iavaro\Documents\Visual Studio 2008\Projects\MongoDBUtilities\AppSecInc.MongoDB.
Test\MongoDBUtil.cs:line 44
at AppSecInc.MongoDB.Test.Program.Main(String[] args) in C:\Users\jchiavaro\D
ocuments\Visual Studio 2008\Projects\MongoDBUtilities\AppSecInc.MongoDB.Test\Pro
gram.cs:line 35
Do you have any idea?
Thanks!
May 14th, 2010 at 1:59 pm
@JC
Are you trying to insert an array or object collection?
-tk
May 14th, 2010 at 2:12 pm
i’m trying to insert an object, like this:
Document row = new Document();
// I changed the method to ‘static’
row = DocumentConverter.ToMongoDocument(entity);
Collection.Insert(row);
The values in ‘row’ seem to be fine, but when it tries to insert it throws that exception.
Thanks for the response!