8

Problem

I have a collection of dynamic data. I want to get it back like this:

{
  _id: "58b454f20960a1788ef48ebb"
  ... 
}

Attempts

Here are a list of approaches that do not work:

This

await resources = _database.GetCollection<BsonDocument>("resources")
    .Find(Builders<BsonDocument>.Filter.Empty)
    .ToListAsync();

return Ok(resources);

Yields

[[{"name":"_id","value":{"bsonType":7,"timestamp":1488213234,"machine":614561,"pid":30862,"increment":16027323,"creationTime":"2017-02-27T16:33:54Z","rawValue":{"timestamp":1488213234,"machine":614561,"pid":30862,"increment":16027323,"creationTime":"2017-02-27T16:33:54Z"},"value":{"timestamp":1488213234,"machine":614561,"pid":30862,"increment":16027323,"creationTime":"2017-02-27T16:33:54Z"}}}]]

This

await resources = _database.GetCollection<BsonDocument>("resources")
    .Find(Builders<BsonDocument>.Filter.Empty)
    .ToListAsync();

return Ok(resources.ToJson());

Yields

[{ "_id" : ObjectId("58b454f20960a1788ef48ebb"), ... }]

This

await resources = _database.GetCollection<BsonDocument>("resources")
    .Find(Builders<BsonDocument>.Filter.Empty)
    .ToListAsync();

return Ok(resources.ToJson(new JsonWriterSettings() { OutputMode = JsonOutputMode.Strict }));

Yields

[{ "_id" : { "$oid" : "58b454f20960a1788ef48ebb" }, ... }]

This

await resources = _database.GetCollection<BsonDocument>("resources")
    .Find(Builders<BsonDocument>.Filter.Empty)
    .ToListAsync();

return Ok(Newtonsoft.Json.JsonConvert.SerializeObject(resources));

Yields

"Newtonsoft.Json.JsonSerializationException: Error getting value from 'AsBoolean' on 'MongoDB.Bson.BsonObjectId'. ---> System.InvalidCastException: Unable to cast object of type 'MongoDB.Bson.BsonObjectId' to type 'MongoDB.Bson.BsonBoolean'

And changing BsonDocument to dynamic yields the same results.

I have also tried registering a serializer according to the docs. I really like this solution since I always want my ObjectIds in a reasonable format and not in something unusable. I would like to get this working if possible.

This

_client = new MongoClient(clientSettings); 
_database = _client.GetDatabase(_settings.DatabaseName); 
BsonSerializer.RegisterSerializer(new ObjectIdSerializer());

...

class ObjectIdSerializer : SerializerBase<ObjectId>
{
    public override ObjectId Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        return context.Reader.ReadObjectId();
    }

    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, ObjectId value)
    {
        context.Writer.WriteString(value.ToString());
    }
}

Had no effect on any of the above results.

Matthew James Davis
  • 12,134
  • 7
  • 61
  • 90

3 Answers3

6

Use BsonDocument when saving to MongoDB

After trying a number of different configurations, the only way I was able to correctly save truly dynamic documents using the connector was to parse objects as BsonDocuments.

public ActionResult Post([FromBody]JObject resource)
{
    var document = BsonDocument.Parse(resource.ToString(Formatting.None));

    DbContext.Resources.InsertOne(document);
}

Register BsonDocument serializers with JSON.Net

The problem with the above approach initially was that when calling ToJson() the ISODate and ObjectId objects would be serialized into objects, which was undesirable. At the time of writing, there doesn't seem to be any extensibility points for overriding this behavior. The logic is baked into the MongoDB.Bson.IO.JsonWriter class, and you cannot register BsonSerializers for BsonValue types:

MongoDB.Bson.BsonSerializationException: A serializer cannot be registered for type BsonObjectId because it is a subclass of BsonValue.

At the time of writing, the only solution I've found is to explicitly custom JSON.Net converters. MongoDB C# Lead Robert Stam has created an unpublished library for this which community member Nathan Robinson has ported to .net-core.. I've created a fork that properly serializes the ObjectId and ISODate fields.

I've created a NuGet package from their work. To use it, include the following reference in your .csproj file:

<PackageReference Include="MongoDB.Integrations.JsonDotNet" Version="1.0.0" />

Then, explicitly register the converters:

Startup.cs

using MongoDB.Integrations.JsonDotNet.Converters;

public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().AddJsonOptions(options =>
        {
            // Adds automatic json parsing to BsonDocuments.
            options.SerializerSettings.Converters.Add(new BsonArrayConverter());
            options.SerializerSettings.Converters.Add(new BsonMinKeyConverter());
            options.SerializerSettings.Converters.Add(new BsonBinaryDataConverter());
            options.SerializerSettings.Converters.Add(new BsonNullConverter());
            options.SerializerSettings.Converters.Add(new BsonBooleanConverter());
            options.SerializerSettings.Converters.Add(new BsonObjectIdConverter());
            options.SerializerSettings.Converters.Add(new BsonDateTimeConverter());
            options.SerializerSettings.Converters.Add(new BsonRegularExpressionConverter());
            options.SerializerSettings.Converters.Add(new BsonDocumentConverter());
            options.SerializerSettings.Converters.Add(new BsonStringConverter());
            options.SerializerSettings.Converters.Add(new BsonDoubleConverter());
            options.SerializerSettings.Converters.Add(new BsonSymbolConverter());
            options.SerializerSettings.Converters.Add(new BsonInt32Converter());
            options.SerializerSettings.Converters.Add(new BsonTimestampConverter());
            options.SerializerSettings.Converters.Add(new BsonInt64Converter());
            options.SerializerSettings.Converters.Add(new BsonUndefinedConverter());
            options.SerializerSettings.Converters.Add(new BsonJavaScriptConverter());
            options.SerializerSettings.Converters.Add(new BsonValueConverter());
            options.SerializerSettings.Converters.Add(new BsonJavaScriptWithScopeConverter());
            options.SerializerSettings.Converters.Add(new BsonMaxKeyConverter());
            options.SerializerSettings.Converters.Add(new ObjectIdConverter());
        }); 
    }
}

Now, you can serialize using the default serializer:

return Created($"resource/{document["_id"].ToString()}", document);
Matthew James Davis
  • 12,134
  • 7
  • 61
  • 90
3

You can make your last attempt work by registering custom ObjectIdConverter with NewtonSoft.

await resources = _database.GetCollection<dynamic>("resources")
    .Find(Builders<dynamic>.Filter.Empty)
    .ToListAsync();

return Ok(Newtonsoft.Json.JsonConvert.SerializeObject(resources, new ObjectIdConverter()));

Converter:

class ObjectIdConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value.ToString());

    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(ObjectId).IsAssignableFrom(objectType);
    }
}

Note: The above converter converts from ObjectId to String after the BSONSerailzers have converted bson value to ObjectId.

You'll still need to use parse to convert string id to ObjectIds for queries and register the ObjectIdConverter globally.

Reference: https://stackoverflow.com/a/16693462/2683814

s7vr
  • 73,656
  • 11
  • 106
  • 127
  • how do I register the ID globally? I'm used to C# being super verbose, but writing `Ok(Newtonsoft.Json.JsonConvert.SerializeObject(resources, new ObjectIdConverter()));` in every callback when I always want the same behavior and no other behavior makes sense seems quite verbose. there's got to be some way i can at best use jsconvert.serializeobject with objectidconverter as the default for all requests, or at least to tell jsonconvert to always use the objectidconverter? – Matthew James Davis May 22 '17 at 20:21
  • That was just an example. tbh, I haven`t worked much with the .Net json library. There are so many links out there. Here is one such link https://stackoverflow.com/questions/19510532/registering-a-custom-jsonconverter-globally-in-json-net and some other https://stackoverflow.com/questions/41088492/json-net-contractresolver-vs-jsonconverter/41094764#41094764 – s7vr May 22 '17 at 20:26
  • Btw I'm not sure how is your application and json.net is interfacing and how is your configuration set up. You can define the configuration for json.net to use ObjectId converter at the controller level in a mvc application and json.net should take over when serializing the response. I'll look around too. – s7vr May 22 '17 at 22:20
  • appreciate it. i opened a big bounty because i've been searching and nothing has worked. this is a .net core app. – Matthew James Davis May 23 '17 at 09:53
0

A "terrible" way of solving this is to convert your BsonDocument to a Dictionary in case your object is a plain object.

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var items = (await collection.Find(new BsonDocument()).ToListAsync());

        var obj = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(items.ToJson());

        return Ok(obj);
    }

This method is simple to code but I see a lot of overhead for conversions.

The best way is to change the Asp.Net serializer to return the "items.ToJson()" as the response Content without trying to parse it.

The old (but gold) HttpRequestMessage enables it. (I can't have time to create a sample to share here now)

Murilo Maciel Curti
  • 2,677
  • 1
  • 21
  • 26