Search code examples
c#mongodbatomic

Safely insert or update subdocument in MongoDB


I have a MongoDB where I keep record of Items with arrays of SubItems. When adding or updating SubItems, I first Find the main Item, then I add the new SubItem to the array of SubItems and replace the entire Item. This worked "great" until I started batch inserting SubItems.

I think my problem is that the find and update are not one atomic operation and the result is that I'm losing SubItems.

I'm using the .NET MongoDB.Driver and my save method looks like this:

public Task Save(string itemId, SubItem subItem)
{
    var itemFilter = Builders<Item>.Filter.Eq(v => v.Id, itemId);
    var collection = _db.GetCollection<Item>("Items");

    var item = await collection.Find(itemFilter).SingleOrDefaultAsync();

    item.SubItems.Add(subItem);
    collection.ReplaceOneAsync(itemFilter, item, new UpdateOptions() { IsUpsert = true }).Wait();
    return Task.FromResult(0);
}

Here is my data model:

public class Item
{
    public string Id { get; set; }
    public List<SubItem> SubItems { get; set; }
}

public class SubItem
{
    public string Id { get; set; }
}

Is there a way to insert or update a SubItem in one operation so I can ensure I keep the entire Item document consistent even if I have multiple processes trying to update the document at the same time?


Solution

  • Have you looked at the AddToSet method, if you use this in combination with the update function instead of the replace one it should keep a better control of your atomicity.

    var updateBuilder = Builders<Item>.Update.AddToSet(items => items.SubItems, new SubItem());
    
    collection.UpdateOne(itemFilter, updateBuilder);
    

    Like so in your case.

        public Task Save(string itemId, SubItem subItem)
        {
            var itemFilter = Builders<Item>.Filter.Eq(v => v.Id, itemId);
            var collection = _db.GetCollection<Item>("Items");
    
            var updateBuilder = Builders<Item>.Update.AddToSet(items => items.SubItems, subItem);
    
            collection.UpdateOneAsync(itemFilter, updateBuilder, new UpdateOptions() { IsUpsert = true }).Wait();
        }