When attempting to perform an upsert operation in Mongo, I'd like to have it generate a GUID for the ID instead of an Object ID. In this case, I'm checking to make sure an object with specific properties doesn't already exist and actually throwing an exception if the update occurs.
Here's a stub of the class definition:
public class Event
{
[BsonId(IdGenerator = typeof(GuidGenerator) )]
[BsonRepresentation(BsonType.String)]
[BsonIgnoreIfDefault]
public Guid Id { get; set; }
// ... more properties and junk
}
And here is how we are performing the upsert operation:
// query to see if there are any pending operations
var keyMatchQuery = Query<Event>.In(r => r.Key, keyList);
var statusMatchQuery = Query<Event>.EQ(r => r.Status, "pending");
var query = Query.And(keyMatchQuery , statusMatchQuery );
var updateQuery = new UpdateBuilder();
var bson = request.ToBsonDocument();
foreach (var item in bson)
{
updateQuery.SetOnInsert(item.Name, item.Value);
}
var fields = Fields<Request>.Include(req => req.Id);
var args = new FindAndModifyArgs()
{
Fields = fields,
Query = query,
Update = updateQuery,
Upsert = true,
VersionReturned = FindAndModifyDocumentVersion.Modified
};
// Perform the upsert
var result = Collection.FindAndModify(args);
Doing it this way will generate the ID as an ObjectID rather than a GUID.
I can definitely get the behavior I want as a two step operation by performing a .FindOne first, and if it fails, doing a direct insert:
var existingItem = Collection.FindOneAs<Event>(query);
if (existingItem != null)
{
throw new PendingException(string.Format("Event already pending: id={0}", existingItem.Id));
}
var result = Collection.Insert(mongoRequest);
In this case, it correctly sets the GUID for the new item, but the operation is non-atomic. I was searching for a way to set the default ID generation mechanism at the driver level, and thought this would do it:
BsonSerializer.RegisterIdGenerator(typeof(Guid), GuidGenerator.Instance);
...but to no avail, and I assume that's because for the upsert, the ID field can't be included so there is no serialization happening and Mongo is doing all of the work. I also looked into implementing a convention, but that didn't make sense since there are separate generation mechanisms to handle that. Is there a different approach I should be looking at for this and/or am I just missing something?
I do realize that GUIDs are not always ideal in Mongo, but we are exploring using them due to compatibility with another system.
You seem to be following the recommended practice for this, but possibly this is bypassed with "upserts" somehow. The general problem seems to be that the operation does not actually know which "class" it is actually dealing with and has no way of knowing that it needs to call the custom Id generator.
Any value that you pass in to MongoDB for the
_id
field will always be honored in place of generating the default ObjectID. Therefore if that field is included in the update "document" portion of the statement it will be used.Probably the safest way to do this when expecting "upsert" behavior is to use the
$setOnInsert
modifier. Anything specified in here will only be set when an insert occurs from a related "upsert" operation. So in general terms:So anything within the
$set
( or other valid update operators ) will always be "updated" when the matching "query" condition is found. The$setOnInsert
fields will be applied when the "insert" actually occurs due to no match. Naturally any literal conditions used in the query portion to "match" are also set so that future "upserts" will issue an "update" instead.So as long as you structure your "update" BSON document to include your newly generated GUID in this way then you will always get the correct value in there.
Much of your code is on the right track, but you will need to invoke the method from your generator class and place value in the
$setOnInsert
portion of the statement, which you are already using, but just not including that_id
value yet.What's happening is that only the server knows whether the FindAndModify is going to end up being an upsert or not, and as currently written it is the server that is automatically generating the _id value, and the server can only assume that the _id value should be an ObjectId (the server knows nothing about your class declarations).
Here's a simplified example using the shell showing your scenario (minus all the C# code...):
We know this was an upsert because we ran it on an empty collection. Note that the server used the query as an initial template for the new document (that's where the "x" came from), applied the update specification (that's where the "y" came from), and because the document had no "_id" it generated a new ObjectId for it.
The trick is to generate the _id client side in case it turns out to be needed, but to put it in the update specification in such a way that it only applies if it's a new document. Here's the previous example using $setOnInsert for the _id:
Now we see that the server used the _id we supplied instead of generating an ObjectId.
In terms of your C# code, simply add the following to your updateQuery:
You should consider renaming your updateQuery variable to updateSpecification (or just update) because technically it's not a query.
There's a catch though... this technique is only going to work against the current 2.6 version of the server. See: https://jira.mongodb.org/browse/SERVER-9958