Add section of existing document to a nested array in the same document as part of update

Hello Everyone,
I am fairly new to MongoDB so please excuse the question if it is fairly basic. I have been searching the issue for hours on google with no luck.

I am looking to maintain a history of a document as part of the document with the latest representation being the root. To do so in the C#, i have a list of the class itself as part of the class. Effectively whenever i update the main document i want to insert its current state (less the list and _id) into the array. This is easy enough to do on the application side but this is problematic because of terrible performance. The information will be coming from an external solution with no idea of the existing value (and its ID) and i don’t want to make a call to the database to find the record to then update it and make a second call to save the update due to performance concerns. I am scratching my head on how to do it as part of the update query to make this efficient in mass. Any pointers would be greatly appreciated.

public sealed class TextValue
{
    [BsonId]
    public required string Id { get; set; }

    public required string Value { get; set; }

    public required string Title { get; set; }

    public required DateTime UpdatedAt { get; set; }

    public required string UpdatedBy { get; set; }

    public List<TextValue>? PreviousVersions { get; set; }
}

Playing with the aggregation pipelines, it appears i can use $project to limit the fields i want down to what i need for the insert to the array but can’t seem to find how i could use that in conjunction with an update.

{
"$project":
  {
    "_id":0,
    "Title":1,
    "UpdatedAt":1,
    "UpdatedBy":1,
  }
}

The behavior i am lookin for is as follows:

I have an existing record as such:

{
  "_id":ObjectId('637a6d1781189a7076f5980b'),
  "Value":"SOMETHING",
  "Title":"Some random title",
  "UpdatedAt":2022-11-28T06:00:00.000+00:00,
  "UpdatedBy":"robo215",
  "PreviousVersions":[
    {
      "_id":ObjectId('637a6d1a81189a7076f5980c'),
      "Title":"The previous title",
      "UpdatedAt":2022-11-28T06:00:00.000+00:00,,
      "UpdatedBy":"jdoe",
    }
  ]
}

Then i receive a new value in the app dto such as:

{
  "Value":"SOMETHING",
  "Title":"This is something else"
}

My app will resolve the actioning user and time but what i am looking to do is find the record “SOMETHING” and update the titles and store the previous version in the history as:

{
  "_id":ObjectId('637a6d1781189a7076f5980b'),
  "Value":"SOMETHING",
  "Title":"This is something else",
  "UpdatedAt":2022-11-29T06:00:00.000+00:00,
  "UpdatedBy":"nguy",
  "PreviousVersions":[
    {
      "_id":ObjectId('637a6d1a81189a7076f5980c'),
      "Title":"The previous title",
      "UpdatedAt":2022-11-28T06:00:00.000+00:00,,
      "UpdatedBy":"jdoe",
    },
    {
      "_id":ObjectId('637a6d1a81189a7076f5980f'),
      "Title":"Some random title",
      "UpdatedAt":2022-11-28T06:00:00.000+00:00,
      "UpdatedBy":"robo215",
    }
  ]
}

Hi @Robo215 ,

I think that you should consider using the version design pattern rather than nesting it in a document:

But if you wish to go the aggregation update route, I did manage to come with something complex:

db.collection.updateOne({"title" : "concat"},
[{$addFields : {title : "concat1", previousVersion : { $concatArrays :[{$ifNull :["$$ROOT.previousVersion",[]]}, ["$$ROOT"]]}}},{$project : {"previousVersion.previousVersion" : 0}}])

This solution is using the agg pipeline within updates, and concat previous version $$ROOT document to the next (omitting the projection of previousVersion inside previousVersion itself).

So for the following example:

db.collection.findOne()

{ _id: ObjectId("637b372db68dcba8d6b608d8"),
  indexes: [ 'x', 'y' ],
  attributes: [ { k: 'x', v: 'v' } ],
  metrics: { a: 1, b: 1 },
  title: 'concat' }

db.collection.updateOne({"title" : "concat"},
[{$addFields : {title : "concat1", previousVersion : { $concatArrays :[{$ifNull :["$$ROOT.previousVersion",[]]}, ["$$ROOT"]]}}},{$project : {"previousVersion.previousVersion" : 0}}])

db.collection.findOne()

{ _id: ObjectId("637b372db68dcba8d6b608d8"),
  indexes: [ 'x', 'y' ],
  attributes: [ { k: 'x', v: 'v' } ],
  metrics: { a: 1, b: 1 },
  title: 'concat1',
  previousVersion: 
   [ { _id: ObjectId("637b372db68dcba8d6b608d8"),
       indexes: [ 'x', 'y' ],
       attributes: [ { k: 'x', v: 'v' } ],
       metrics: { a: 1, b: 1 },
       title: 'concat' } ] }

db.collection.updateOne({"title" : "concat1"},
[{$addFields : {title : "concat2", previousVersion : { $concatArrays :[{$ifNull :["$$ROOT.previousVersion",[]]}, ["$$ROOT"]]}}},{$project : {"previousVersion.previousVersion" : 0}}])


{ _id: ObjectId("637b372db68dcba8d6b608d8"),
  indexes: [ 'x', 'y' ],
  attributes: [ { k: 'x', v: 'v' } ],
  metrics: { a: 1, b: 1 },
  title: 'concat2',
  previousVersion: 
   [ { _id: ObjectId("637b372db68dcba8d6b608d8"),
       indexes: [ 'x', 'y' ],
       attributes: [ { k: 'x', v: 'v' } ],
       metrics: { a: 1, b: 1 },
       title: 'concat' },
     { _id: ObjectId("637b372db68dcba8d6b608d8"),
       indexes: [ 'x', 'y' ],
       attributes: [ { k: 'x', v: 'v' } ],
       metrics: { a: 1, b: 1 },
       title: 'concat1' } ] }

Thanks
Pavel