Serializing C# classes to MongoDB without using di

2019-07-13 05:08发布

问题:

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.)

So far, so good.

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. That's not what I want. 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 not 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?

回答1:

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


回答2:

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.



回答3:

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