27831

Serializing C# classes to MongoDB without using discriminators in subdocuments

I'm writing C# code that writes to a Mongo database used by an existing Web app (written in PHP), so I need to not change the existing structure of the database. The database structure looks something like this:

{
    "_id": ObjectId("5572ee670e86b8ec0ed82c61")
    "name": "John Q. Example",
    "guid": "12345678-1234-5678-abcd-fedcba654321",
    "recordIsDeleted": false,
    "address":
    {
        "line1": "123 Main St.",
        "city": "Exampleville"
    }
}


I read that in to a class that looks like this:

public class Person : MongoMappedBase
{
    public ObjectId Id { get; set; }
    public Guid Guid { get; set; }
    public bool RecordIsDeleted { get; set; }
    public string Name { get; set; }
    public AddressData Address { get; set; }
    // etc.
}

public class AddressData : MongoMappedBase
{
    public string Line1 { get; set; }
    public string City { get; set; }
    // etc.
}


The reading code looks like:

var collection = db.GetCollection<Person>("people");
List<Person> people = collection.Find<Person>(_ => true).ToListAsync().Result;


(Note: I'm still in development. In production, I'm going to switch to ToCursorAsync() and loop through the data one at a time, so don't worry about the fact that I'm pulling the whole list into memory.)

<strong>So far, so good.</strong>

However, when I write the data out, this is what it looks like:

{
    "_id": ObjectId("5572ee670e86b8ec0ed82c61")
    "name": "John Q. Example",
    "guid": "12345678-1234-5678-abcd-fedcba654321",
    "recordIsDeleted": false,
    "address":
    {
        "_t": "MyApp.MyNamespace.AddressData, MyApp",
        "_v":
        {
            "line1": "123 Main St.",
            "city": "Exampleville"
        }
    }
}


Notice how the address field looks different. <strong>That's not what I want.</strong> I want the address data to look just like the address data input (no _t or _v fields). In other words, the part that ended up as the contents of _v is what I wanted to persist to the Mongo database as the value of the address field.

Now, if I was just consuming the Mongo database from my own C# code, this would probably be fine: if I were to deserialize this data structure, I assume (though I haven't yet verified) that Mongo would use the _t and _v fields to create instances of the right type (AddressData), and put them in the Address property of my Person instances. In which case, everything would be fine.

But I'm sharing this database with a PHP web app that is not expecting to see those _t and _v values in the address data, and won't know what to do with them. I need to tell Mongo "Please do <strong>not</strong> serialize the type of the Address property. Just assume that it's always going to be an AddressData instance, and just serialize its contents without any discriminators."

The code I'm currently using to persist the objects to Mongo looks like this:

public UpdateDefinition<TDocument> BuildUpdate<TDocument>(TDocument doc) {
    var builder = Builders<TDocument>.Update;
    UpdateDefinition<TDocument> update = null;
    foreach (PropertyInfo prop in typeof(TDocument).GetProperties())
    {
        if (prop.PropertyType == typeof(MongoDB.Bson.ObjectId))
            continue; // Mongo doesn't allow changing Mongo IDs
        if (prop.GetValue(doc) == null)
            continue; // If we didn't set a value, don't change existing one
        if (update == null)
            update = builder.Set(prop.Name, prop.GetValue(doc));
        else
            update = update.Set(prop.Name, prop.GetValue(doc));
    }
    return update;
}

public void WritePerson(Person person) {
    var update = BuildUpdate<Person>(person);
    var filter = Builders<Person>.Filter.Eq(
        "guid", person.Guid.ToString()
    );
    var collection = db.GetCollection<Person>("people");
    var updateResult = collection.FindOneAndUpdateAsync(
        filter, update
    ).Result;
}


Somewhere in there, I need to tell Mongo "I don't care about the _t field on the Address property, and I don't even want to see it. I know what type of objects I'm persisting into this field, and they'll always be the same." But I haven't yet found anything in the Mongo documentation to tell me how to do that. Any suggestions?

Answer1:

Thanks @rmunn for this question, it helped me a lot.

I was struggling with this same problem when I found this Q&A. After further digging I found that you can remove the switch statement in the accepted answer by using BsonDocumentWrapper.Create(). This is a link to where I found the tip.

Here's a example for anyone else looking:

public UpdateDefinition<TDocument> BuildUpdate<TDocument>(TDocument doc) { var builder = Builders<TDocument>.Update; var updates = new List<UpdateDefinition<TDocument>>(); foreach (PropertyInfo prop in typeof(TDocument).GetProperties()) { if (prop.PropertyType == typeof(MongoDB.Bson.ObjectId)) continue; // Mongo doesn't allow changing Mongo IDs if (prop.GetValue(doc) == null) continue; // If we didn't set a value, don't change existing one updates.add(builder.Set(prop.Name, BsonDocumentWrapper.Create(prop.PropertyType, prop.GetValue(doc)))); } return builder.Combine(updates); }

Answer2:

I figured it out. I was indeed having the problem described at https://groups.google.com/forum/#!topic/mongodb-user/QGctV4Hbipk where Mongo expects a base type but is given a derived type. The base type Mongo was expecting, given my code above, was actually object! I discovered that builder.Set() is actually a generic method, builder.Set<TField>, which can figure out its TField type parameter from the type of its second argument (the field data). Since I was using prop.GetValue(), which returns object, Mongo was expecting an object instance on my Address field (and the other fields that I left out of the question) and therefore putting _t on all those fields.

The answer was to explicitly cast the objects being returned from prop.GetValue(), so that builder.Set() could call the correct generic method (builder.Set<AddressData>() rather than builder.Set<object>()) in this case. The following was a bit ugly (I wish there was a way to get a specific generic function overload by reflection at runtime, as I could have converted that whole switch statement to a single reflection-based method call), but it worked:

public UpdateDefinition<TDocument> BuildUpdate<TDocument>(TDocument doc) { var builder = Builders<TDocument>.Update; var updates = new List<UpdateDefinition<TDocument>>(); foreach (PropertyInfo prop in typeof(TDocument).GetProperties()) { if (prop.PropertyType == typeof(MongoDB.Bson.ObjectId)) continue; // Mongo doesn't allow changing Mongo IDs if (prop.GetValue(doc) == null) continue; // If we didn't set a value, don't change existing one switch (prop.PropertyType.Name) { case "AddressData": updates.add(builder.Set(prop.Name, (AddressData)prop.GetValue(doc))); break; // Etc., etc. Many other type names here default: updates.add(builder.Set(prop.Name, prop.GetValue(doc))); break; } } return builder.Combine(updates); }

This resulted in the Address field, and all the other fields I was having trouble with in my real code, being persisted without any _t or _v fields, just like I wanted.

Answer3:

You can convert your object to JSON string and from that JSON string you can convert back to BsonArray (if list) or BsonDocument (if object)

Object that you want to update

public UpdateDefinition<T> getUpdate(T t) { PropertyInfo[] props = typeof(T).GetProperties(); UpdateDefinition<T> update = null; foreach (PropertyInfo prop in props) { if (t.GetType().GetProperty(prop.Name).PropertyType.Name == "List`1") { update = Builders<T>.Update.Set(prop.Name, BsonSerializer.Deserialize<BsonArray>(JsonConvert.SerializeObject(t.GetType().GetProperty(prop.Name).GetValue(t)))); } else if (t.GetType().GetProperty(prop.Name).PropertyType.Name == "object") { /* if its object */ update = Builders<T>.Update.Set(prop.Name, BsonSerializer.Deserialize<BsonDocument>(JsonConvert.SerializeObject(t.GetType().GetProperty(prop.Name).GetValue(t)))); } else { /*if its primitive data type */ update = Builders<T>.Update.Set(prop.Name, t.GetType().GetProperty(prop.Name).GetValue(t)); } } return update; }

This will update any type of object list, you just need to pass the object

Recommend

  • Alternatives to macro substitution in java
  • Regex: Filter out text before last occurrence
  • How do I declaratively bind 'SelectedValue' to datasource field?
  • How to find all matching numbers, that sums to 'N' in a given array
  • ListView_GetItem() macro & LV_GETITEMTEXT returns empty STRING while getting another APP listvie
  • How to get the “temp folder” in Windows 7?
  • std::function as sighandler_t
  • C++11 std::threads and waiting for threads to finish
  • Run all tests in namespace using Nunit3-console.exe
  • Magento Enterprise controller override
  • Connect to a password protected server with WCF over HTTP
  • ActionBar with appcompat library v7 (ava.lang.IllegalStateException: You need to use a Theme.AppComp
  • angularjs - ng-show doesn't update class when $interval triggers
  • docker-compose: connection refused between containers, but service accessible from host
  • Base Internationalization and “Could not find a storyboard named […]”
  • UITableView takes much longer to load when numberOfRows returns a large number
  • How to detect left mouse click but not when the click occur on a UI Button component [closed]
  • JConsole Main class
  • Arduino making decision according to a packet received from serial port
  • WP7 difficulties binding data to listbox itemssource - won't refresh
  • Ruby 1.8.6 Array#uniq not removing duplicate hashes
  • Configure Spring's MappingJacksonHttpMessageConverter
  • Android Database Error - getWriteableDatabase
  • How to make R's read_csv2() recognise the text characters properly
  • AppleScript : find open tab in safari by name and open it
  • Email format validation in mvc3 view
  • Is there a javascript serializer for JSON.Net?
  • Retrieving value from sql ExecuteScalar()
  • C# - Is there a limit to the size of an httpWebRequest stream?
  • What is Eclipse's Declaration View used for?
  • How to add date and time under each post in guestbook in google app engine
  • Javascript + PHP Encryption with pidCrypt
  • Jquery - Jquery Wysiwyg return html as a string
  • Apache 2.4 - remove | delete | uninstall
  • SVN: Merging two branches together
  • KeystoneJS: Relationships in Admin UI not updating
  • Hits per day in Google Big Query
  • coudnt use logback because of log4j
  • Append folder name and increment by 1 using batch script
  • Checking variable from a different class in C#