Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange behavior of MongoDB LINQ provider for fields called "id"

Here's a JSON document where Mongo LINQ provider fails:

{"results":
     {"text":"@twitterapi  http://tinyurl.com/ctrefg",
     "to_user_id":396524,
     "to_user":"TwitterAPI",
     "from_user":"jkoum",
     "metadata":
     {
      "result_type":"popular",
      "recent_retweets": 109
     },
     "id":1478555574, 
     "from_user_id":1833773,
     "iso_language_code":"nl",
     "source":"<a href=\"http://twitter.com/\">twitter<\/a>",
     "profile_image_url":"http://s3.amazonaws.com/twitter_production/profile_images/118412707/2522215727_a5f07da155_b_normal.jpg",
     "created_at":"Wed, 08 Apr 2009 19:22:10 +0000",
     "since_id":0,
     "max_id":1480307926,
     "refresh_url":"?since_id=1480307926&q=%40twitterapi",
     "results_per_page":15,
     "next_page":"?page=2&max_id=1480307926&q=%40twitterapi",
     "completed_in":0.031704,
     "page":1,
     "query":"%40twitterapi"}
}

Note an "id" field. Here are related C# entity definitions:

class Twitter
{
    [BsonId]
    public ObjectId Id { get; set; }
    public Result results { get; set; }
}

private class Result
{
    public string text { get; set; }
    public int to_user_id { get; set; }
    public string to_user { get; set; }
    public string from_user { get; set; }
    public Metadata metadata { get; set; }
    public int id { get; set; }
    public int from_user_id { get; set; }
    public string iso_language_code { get; set; }
    public string source { get; set; }
    public string profile_image_url { get; set; }
    public string created_at { get; set; }
    public int since_id { get; set; }
    public int max_id { get; set; }
    public string refresh_url { get; set; }
    public int results_per_page { get; set; }
    public string next_page { get; set; }
    public double completed_in { get; set; }
    public int page { get; set; }
    public string query { get; set; }
}

class Metadata
{
    public string result_type { get; set; }
    public int recent_retweets { get; set; }
}

If I create a "Twitter" collection and save the document above, then when I query it using Mongo LINQ provider, it throws a FileFormatException exception: "Element 'id' does not match any field or property of class Mongo.Context.Tests.NativeTests+Result"

However there are two alternative workarounds to fix this problem:

  1. Rename Result "id" field, e.g. to "idd" both in JSON doc and the Result class. Then LINQ query works.
  2. Keep "id" field but in addition add a field "Id" to the Result class and mark it with attribute [BsonId]. Now the Result class contains both "Id" and "id" fields, but the query works!

I use Mongo API to query the collection, everything works fine, so I guess this must be a bug in a MongoDB LINQ provider. "id" in a nested JSON element should not be a reserved work, should it?

UPDATE: Here's the result of a native API query execution:

> db.Twitter.find().limit(1);

{ "_id" : ObjectId("50c9d3a4870f4f17e049332b"),
 "results" : { 
    "text" : "@twitterapi  http://tinyurl.com/ctrefg", 
    "to_user_id" : 396524, 
    "to_user" : "TwitterAPI", 
    "from_user" : "jkoum", 
    "metadata" : { "result_type" : "popular", "recent_retweets" : 109 }, 
    "id" : 1478555574, 
    "from_user_id" : 1833773, "
    iso_language_code" : "nl", 
    "source" : "<a href=\"http://twitter.com/\">twitter</a>", 
    "profile_image_url" : "http://s3.amazonaws.com/twitter_production/profile_images/118412707/2522215727_a5f07da155_b_normal.jpg", 
    "created_at" : "Wed, 08 Apr 2009 19:22:10 +0000", 
    "since_id" : 0, 
    "max_id" : 1480307926, 
    "refresh_url" : "?since_id=1480307926&q=%40twitterapi", "results_per_page" : 15,    "next_page" : "?page=2&max_id=1480307926&q=%40twitterapi", 
    "completed_in" : 0.031704, 
    "page" : 1, 
    "query" : "%40twitterapi" 
    } 
}
like image 934
Vagif Abilov Avatar asked Dec 13 '12 11:12

Vagif Abilov


1 Answers

MongoDB requires that every document stored in the database have a field (at the root level) called "_id".

The C# driver assumes that any field in your class called "Id", "id" or "_id" is meant to be mapped to the special "_id" field. This is a convention, one that can be overridden. The C# driver doesn't know that your Result class isn't meant to be used as the root document of a collection, so it finds your "id" field and maps it to "_id" in the database.

One way you can override this is to change the name of the field in your class (as you discovered). What you can then also do is use the [BsonElement] attribute to map your C# field name (e.g. "idd") to whatever name is actually being used in the database (e.g. "id"). For example:

public class Result
{
    [BsonElement("id")]
    public int idd; // matches "id" in the database
    // other fields
}

Another alternative is to override the convention that finds the "Id" member of a class to suppress the default behavior of the C# driver for your Result class. You can do this by registering a new ConventionProfile for your Result class. For example:

var noIdConventions= new ConventionProfile();
noIdConventions.SetIdMemberConvention(new NamedIdMemberConvention()); // no names
BsonClassMap.RegisterConventions(noIdConventions, t => t == typeof(Result));

You must be sure to do this very early in your program, before your Result class gets mapped.

like image 51
Robert Stam Avatar answered Nov 13 '22 12:11

Robert Stam