3

I am trying to have one db call to either add an element to a nested array or if that element already exists increase a counter on it. In this case we are trying to see if there is a matching SubDocument, if none is found we want to add that sub document to the SubDocs list if one is found we want to increment the Count prop on the SubDocument found in that list.

The reason we are looking for one call to do this is because we will have several services potentially trying to do the same thing at the same time.

    public class Document
    {
        [BsonId]
        [BsonIgnoreIfDefault]
        [BsonRepresentation(BsonType.ObjectId)]
        public string DocumentID { get; set; }

        public int Date { get; set; }
        public List<SubDocument> SubDocs { get; set; }
    }

    public class SubDocument
    {
        public string Count { get; set; }
        public string Name { get; set; }
    }

Any help would be greatly appreciated. We are using the .NET driver 2.8.1

I was able to do this using a find, and if its found update the counter. But testing so far shows counts will get missed if multiple services are doing this.

2
  • 1
    Theoretically it should be possible to achieve with findAndModify, but I don't remember how to add a child document with it and there can be some caveat with the query. Have you tried such an approach? Commented Aug 1, 2019 at 19:52
  • 1
    pls check my answer below. i think i've cracked it... Commented Aug 3, 2019 at 10:19

2 Answers 2

2

it could be done with a single bulkWrite command which combines 2 update commands like this:

db.Document.bulkWrite([
    { updateOne: {
            filter: {
                "_id": ObjectId("5d455b8f2d686e2980829d1b"),
                "SubDocs": {
                    "$not": {
                        "$elemMatch": {
                            "Name": "iron man" } } } },
            update: {
                "$push": {
                    "SubDocs": {
                        "Count": NumberInt("0"),
                        "Name": "iron man" } } } }
    },
    { updateOne: {
            filter: {
                "_id": ObjectId("5d455b8f2d686e2980829d1b"),
                "SubDocs": {
                    "$elemMatch": {
                        "Name": "iron man"
                    } } },
            update: {
                "$inc": {
                    "SubDocs.$.Count": NumberInt("1")
                } } } }])

here's a multi-threaded test program which doesn't seem to be having any concurrency issues in my dev environment.

using MongoDB.Entities; // PM> Install-Package MongoDB.Entities
using System.Linq;
using System.Threading.Tasks;

namespace StackOverflow
{
    public class Document : Entity
    {
        public SubDocument[] SubDocs { get; set; } = new SubDocument[] { };
    }

    public class SubDocument
    {
        public int Count { get; set; }
        public string Name { get; set; }
    }

    public class Program
    {
        private static void Main(string[] args)
        {
            new DB("test");

            var doc = new Document();
            doc.Save();

            var subDoc = new SubDocument { Name = "iron man" };

            Parallel.ForEach(Enumerable.Range(1, 20), _ =>
            {
                var bulk = DB.Update<Document>();

                bulk.Match(d =>
                           d.ID == doc.ID &&
                          !d.SubDocs.Any(s => s.Name == subDoc.Name))
                    .Modify(b => b.Push(d => d.SubDocs, subDoc))
                    .AddToQueue();

                bulk.Match(d =>
                          d.ID == doc.ID &&
                          d.SubDocs.Any(s => s.Name == subDoc.Name))
                    .Modify(b =>
                           b.Inc(d => d.SubDocs[-1].Count, 1))
                    .AddToQueue();

                bulk.Execute();
            });
        }
    }
}

Sign up to request clarification or add additional context in comments.

Comments

0

I believe Mongo 4 has support for atomic transactions, so a path to explore would be to try and use those.

If you want to do it on the C# side, when you're doing the logic of checking if the document exists/updating the counter, one way to ensure that only one service updates the counter at a time would be to wrap the logic in a critical section (i.e. lock).

Check out https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statement for examples of how to do that.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.