18

I have a problem while upserting to mongo db using the official C# driver.

public abstract class AggregateRoot
{
    /// <summary>
    /// All mongoDb documents must have an id, we specify it here
    /// </summary>
    protected AggregateRoot()
    {
        Id = ObjectId.GenerateNewId();
    }

    [BsonId]
    public ObjectId Id { get; set; }
}

My entities already have the id-s but I had to create the mongo specific Id for it to work, as all the documents in a collection should have one. Now then I receive a new entity in my system a new Mongo Id is generated and I get the mongo cannot change _id of a document old exception. Is there some work-around?

Let me describe the design a bit. All the entities which would be stored as documents were inheriting from AggregateRoot which had the id generation in it. Every sub-document had its id generated automatically and I had no problem with this. The id in AggregateRoot was introduced to correct the problem when retrieving data from MongoCollection to List and the generation was introduced so the id-s are different. Now we can move that id generation to save methods because the new entity for update had a new id generation. But it would mean that every dev on the team must not forget generating id-s in repository which is risky. It would be nicer just to ignore the id than mapping from mongo if it is possible and not to have AggregateRoot class at all

Yurii Hohan
  • 4,021
  • 4
  • 40
  • 54
  • How are you saving your objects? The BsonId attribute should force MongoDB to use that field as your id. – Jeff Fritz Sep 02 '11 at 12:10
  • The idea is that the data comes from an external system for storage. It has its own id-s I have to store. And this is a fake id for the sake of Mongo compatibility. Every document inherits from AggregateRoot, so this thing is generated on receiving every object. It is clear that I might receive the same data but the generated mongo id is different. So the exception appears – Yurii Hohan Sep 02 '11 at 12:23

2 Answers2

82

I've encountered similar problem. I wanted to upsert documents using official C# driver. I had a class like this:

public class MyClass
{
    public ObjectId Id { get; set; }
    public int Field1 { get; set; }
    public string Field2 { get; set; }
}

In console I would write: db.collection.update({Field1: 3},{Field1: 3, Field2: "value"}) and it would work. In C# I wrote:

collection.Update(Query.EQ("Field1", 3),
                Update.Replace(new MyClass { Field1 = 3, Field2 = "value" }),
                UpdateFlags.Upsert);

and it didn't work! Because driver includes empty id in update statement and when I upsert second document with different value of Field1 exception E11000 duplicate key error index is thrown (in this case Mongo tries to insert a document with _id that already exists in db).

When I generated _id by myself (like topic starter) I've encountered the same exception (mongo cannot change _id of a document) on upserting objects with existing value of Field1.

Solution is to mark Id property by attribute [BsonIgnoreIfDefault] (and not initialize it). In this case driver omits _id field in update statement and MongoDb generates Id if it necessary.

Brian Reischl
  • 7,216
  • 2
  • 35
  • 46
renadeen
  • 1,741
  • 17
  • 16
  • 15
    You rule man, that [BsonIgnoreIfDefault] saved a ton of time. It is a shame I can vote more for you. – Sergio Vicente Jan 15 '13 at 19:26
  • 6
    Wasted about an hour trying to figure out why my ReplaceOneAsync call with the 2.0 driver wasn't working. Found this answer. Added [BsonIgnoreIfDefault] and problem solved. Thanks! – BrandonLWhite Mar 02 '15 at 00:46
  • 2
    I've spent hours trying to figure this out. THANKS! – bbrez1 Aug 11 '15 at 09:43
  • This is the solution – Daniel Cumings Aug 20 '18 at 21:32
  • 3
    There is one big "gotcha" here, this doesn't work if you filter on the _id: [Mongodb jira](https://jira.mongodb.org/browse/CSHARP-1805) `Just to reiterate, if the filter includes the _id then the server will always use that value as the _id of the new document if no existing document matches and the ReplaceOne call results in an upsert. This is true even if the value of the _id in the filter is null (or an empty ObjectId).` – Bassebus Dec 01 '18 at 06:43
  • This should be the solution. Thanks – Sajan Feb 05 '20 at 14:54
  • tnx bro,you saved my time – Sadeq Hatami Jun 13 '20 at 06:57
  • This is now the second time this answer saved me from a terrible headache. Thank you! – Adrian K. Nov 25 '21 at 16:48
8

Looks like you might be explicitly setting the Id value for both inserts and updates. That's fine for inserts, all new objects need an _id value, however for updates you're not allowed to change the value of _id on an existing document after it's created.

Try not setting the Id value at all. If you don't specify a value before inserting, the driver uses built-in IdGenerator classes to generate a new _id value, so if it's an ObjectId type it'll use the ObjectIdGenerator. Then both your inserts and updates work fine.

Chris Fulstow
  • 41,170
  • 10
  • 86
  • 110
  • If I remove the id property then really it is not a problem saving/updating an entity. The problem is that it throws exception when I try to search on the collection from the code because it cannot map the _id to an inexisting property – Yurii Hohan Sep 02 '11 at 14:21
  • 3
    You can use the `[BsonIgnoreExtraElements]` attribute on the class if the collection has fields that don't have matching properties. – Chris Fulstow Sep 03 '11 at 01:03
  • Also [BsonIgnoreIfDefault] must be used in addition to your solution as @renadeen explained – cahit beyaz Nov 25 '18 at 20:31