[CSHARP-1805] Collection.ReplaceOne() with upsert flag does not auto-generate Id Created: 19/Oct/16  Updated: 08/Jan/18  Resolved: 08/Jan/18

Status: Closed
Project: C# Driver
Component/s: API
Affects Version/s: 2.3
Fix Version/s: None

Type: Bug Priority: Minor - P4
Reporter: Todd Behunin Assignee: Robert Stam
Resolution: Done Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified
Environment:

windows10, .net46, .netCore



 Description   

Given an entity defined as such:

public class Foo
{
public ObjectId Id

{ get; set; }
public string Bar { get; set; }

}

I want the ability to "upsert" a document with an object having that type, where a new record would have an auto-generated "_id". After executing the following:

var foo = new Foo

{ Bar = "hello world" }

;
collection.ReplaceOne(x => x.Id.Equals(foo.Id), foo, new UpdateOptions

{ IsUpsert = true }

);

The mongo shell, however, reports the following:
> db.foo.find()

{ "_id" : ObjectId("000000000000000000000000"), "Bar" : "hello world" }

The _id inserted is equivalent to ObjectId.Empty. Even applying the [BsonIgnoreIfDefault] attribute atop the Id property has no effect.

Expected Behavior:

The .ReplaceOne() and .ReplaceOneAsync() methods would recognize that the Id has not been explicitly initialized and will "insert" with an auto-generated _id (when used in conjunction with the "upsert" flag) instead of performing, what it appears to be, an "update".



 Comments   
Comment by Robert Stam [ 08/Jan/18 ]

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

It doesn't really make sense to try and do an upsert when your filter is { _id : null }. If _id is null you should just use InsertOne instead.

Comment by Kevin Versfeld [ 01/Jun/17 ]

Same problem here: I am trying to use ReplaceOne as a single place to upsert items, which may or may not exist yet - and the only thing I have in code to know this is the Id. As a workaround, I'm basically doing a manual check against the Id on my side, and if it is null/empty, I pass in x => false as the filter predicate to ReplaceOne..... Watching this issue for a fix or alternative.

Comment by Jeremy Stafford [ 25/Feb/17 ]

I'm having the same problem when I use a string type for the Id. The ID never gets generated and is inserted as null. Using 2.4, myself.

    [BsonIgnoreExtraElements]
    public class User
    {
        [BsonId(IdGenerator = typeof(StringObjectIdGenerator))]
        [BsonIgnoreIfDefault]
        public string Id { get; set; }
 
        public string FirstName { get; set; }
 
        public string LastName { get; set; }
 
        public int Age { get; set; }
    }

Comment by Todd Behunin [ 27/Oct/16 ]

If you want the server to generate a brand new ObjectId when the ReplaceOne results in an upsert then your filter should not include the _id.

The problem with that is, ObjectId is typed as a struct, which means it's not instantiated with the new keyword. Therefore, it will always have a "default" value unless explicitly assigned.

In addition, correct me if I'm wrong, but the filter in the call to ReplaceOne() can't be null/empty. It is used to determine which record I intend to update (or insert). So, having a FilterDefinition of x => x.Id.Equals(foo.Id) should do the trick. However, it still gets inserted as all zeros, even if I add [BsonIgnoreIfDefault] to the ObjectId property.

The use case scenario is, I have an object of type Foo (see description above). It has an Id property of type ObjectId. I don't know if it's in the db or not but I want it saved regardless - and have an automatic id generated. How does one call the ReplaceOne() or ReplaceOneAsync() to make this happen?

Comment by Robert Stam [ 26/Oct/16 ]

When the filter includes the _id the server uses the _id value from the filter as the _id of the upserted document.

Your C# code is equivalent to the following shell code (assuming you put [BsonIgnoreIfDefault] on the Id property):

> var id = new ObjectId("000000000000000000000000")
> db.foo.replaceOne({ _id : id }, { Bar : "hello world" }, { upsert : true })
{
        "acknowledged" : true,
        "matchedCount" : 0,
        "modifiedCount" : 0,
        "upsertedId" : ObjectId("000000000000000000000000")
}

which results in the following document being inserted (the collection was empty when I ran the shell command above):

> db.foo.find()
{ "_id" : ObjectId("000000000000000000000000"), "Bar" : "hello world" }
>

If you want the server to generate a brand new ObjectId when the ReplaceOne results in an upsert then your filter should not include the _id.

Generated at Wed Feb 07 21:40:43 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.