Scott
Scott

Reputation: 1094

MongoDB C# driver FindOneAndReplace() with IsUpsert option is not returning document ID on Insert

I'm trying to upsert a document into Mongo using the C# Driver with the following (simplified) method:

public MyDocumentType UpsertOne(MyDocumentType doc)
{
    var options = new FindOneAndReplaceOptions<MyDocumentType>
    {
        IsUpsert = true,
        ReturnDocument = ReturnDocument.After,
    };
    var filter = Builders<MyDocumentType>.Filter.Eq(d => d.Id, doc.Id);
    var upsertedDoc = _collection.FindOneAndReplace(filter, doc, options);
    return doc;
}

Neither the upsertedDoc nor doc objects have a nonzero objectID.

However, When I insert the doc with InsertOne() Mongo generates a docID as expected:

public MyDocumentType InsertOne(MyDocumentType doc)
{
    _collection.InsertOne(doc);
    return doc;
}

The ObjectId field in my document class is nonnullable, and initializes to zeros, but on InsertOne it gets proper documentID but when using FindOneAndReplace with the IsUpsert=true option it simply inserts a doc with a zero'd Id.

Here is how I'm calling the various methods, and the output:

var db = new MongoService();
db.Connect();
var documentWithZeroedId = db.UpsertOne(document);
Console.WriteLine($"Upsert Document ID: {documentWithZeroedId.Id}");

var documentWithValidId = db.InsertOne(document);
Console.WriteLine($"Insert Document ID: {documentWithValidId.Id}");

// Output:
Connected to Mongo!
Upsert Document ID: 000000000000000000000000
Insert Document ID: 6620b59ca4421c3c294c0b09

What is the proper means to upsert a doc and when inserting, get a valid non-zeroed doc ID?

The complete console app demonstrating this issue is:

using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Security.Cryptography.X509Certificates;
using System.Xml.Xsl;

public class DictionaryValue
{
    public int Id;
    public string Value;
}

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class BsonCollectionAttribute : Attribute
{
    public string CollectionName { get; }

    public BsonCollectionAttribute(string collectionName)
    {
        CollectionName = collectionName;
    }
}

[BsonCollection("Documents")]
public class MyDocumentType 
{
    [BsonId]
    [BsonRepresentation(MongoDB.Bson.BsonType.ObjectId)]
    public ObjectId Id;
    public DateTime CreatedAt => Id.CreationTime;

    public string TopLevelField;
    public Dictionary<string, DictionaryValue> Values;

}
class MongoService { 
    private MongoClient _client;
    private IMongoDatabase _db;
    private IMongoCollection<MyDocumentType> _collection;

    public void Connect()
    {
        try
        {
            var settings = MongoClientSettings.FromConnectionString("mongodb://localhost:27018");
            _client = new MongoClient(settings);
            _db = _client.GetDatabase("dictionaryTest");
            Console.WriteLine("Connected to Mongo!");
            _collection = _db.GetCollection<MyDocumentType>("Documents");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error on connect to Mango database: {error}", ex);
            throw;
        }
    }

    public MyDocumentType UpsertOne(MyDocumentType doc)
    {
        var options = new FindOneAndReplaceOptions<MyDocumentType>
        {
            IsUpsert = true,
            ReturnDocument = ReturnDocument.After,
        };

        var filter = Builders<MyDocumentType>.Filter.Eq(d => d.Id, doc.Id);
        var upsertedDoc = _collection.FindOneAndReplace(filter, doc, options);
        return doc;
    }

    public MyDocumentType UpsertOneSlow(MyDocumentType doc)
    {
        var existing = _collection.Find(d => d.Id == doc.Id).FirstOrDefault();
        if (existing != null)
        {
            _collection.ReplaceOne(d => d.Id == doc.Id, doc);
            return doc;
        } else
        {
            _collection.InsertOne(doc);
            return doc;
        }
    }

    public MyDocumentType InsertOne(MyDocumentType doc)
    {
        _collection.InsertOne(doc);
        return doc;
    }
}

class Program
{
    static int Main(String[] args)
    {
        var document = new MyDocumentType();
        document.TopLevelField = "Dictionary of Integers";
        document.Values = new Dictionary<string, DictionaryValue>()
        {
            { "1", new DictionaryValue {Id = 1, Value = "1"} },
            { "2", new DictionaryValue {Id = 2, Value = "1"} },
            { "3", new DictionaryValue {Id = 3, Value = "2"} },
        };

        var db = new MongoService();
        db.Connect();

        var documentWithZeroedId = db.UpsertOne(document);
        Console.WriteLine($"Upsert Document ID: {documentWithZeroedId.Id}");

        var documentWithValidId = db.InsertOne(document);
        Console.WriteLine($"Insert Document ID: {documentWithValidId.Id}");

        return 0;
    }
}

Upvotes: 1

Views: 277

Answers (1)

Markus
Markus

Reputation: 22456

In your sample, you first generate a new document with an empty ObjectId. This id is initialized with all zeros. When inserting the document, the driver recognizes the empty ObjectId and lets MongoDB assign a new id value.

When upserting, however, you explicitely look for a document with an id with a zero value (valid, but very uncommon). The driver handles the situation differently and inserts the document with the all zero ObjectId.

Instead of relying on the database server to assign the id, I have found it is best to assign the ids in C# code in order to avoid situations where you sometimes end up with an unassigned id. You could change your code like this to assign the id directly in the code:

public class MyDocumentType
{
    public ObjectId Id { get; set; } = ObjectId.GenerateNewId();
    public DateTime CreatedAt => Id.CreationTime;

    public string TopLevelField;
    public Dictionary<string, DictionaryValue> Values;
}

The property initializer asserts that the id is set directly when creating an instance; of course, you can assign another one later on.

Please note that after the change, the sample will raise an error for the InsertOne operation as the code tries to insert another document with the same id.

Upvotes: 0

Related Questions