Add a key to an existing dictionary in MongoDb while replacing other fields using C#

Viewed 209

I want to update an existing document by amending a new value to a dictionary in it, while replacing other fields. We can see it as changing the current address while retaining a list of previous whereabouts.

The class constituting the information looks like this.

class Thing
{
  public string Name { get; set; }
  public string Location { get; set; }
  public Dictionary<string, string> History { get; set; }
}

I'm doing the update using the following code.

Thing update = ...;

Thing output = await _dbContext.Things
    .FindOneAndUpdateAsync(
        Builders<Thing>.Filter.Where(a => a.Name == update.name),
        Builders<Thing>.Update.Set(
            b => b,
            update
        ), ...
    );

Obviously, I need to explain to the DB how the update should be done: adding the current value of Location to History with e.g. timestamp or index as a key). Googling gave me this blog suggesting alterantions on ReturnDocument like so.

Thing output = await _dbContext.Things
    .FindOneAndUpdateAsync(
        Builders<Thing>.Filter.Where(a => a.Name == update.name),
        Builders<Thing>.Update.Set(
            b => b,
            update
        ),
        new FindOneAndUpdateOptions<Thing>
        {
            ReturnDocument = ReturnDocument.Before
        }, ...
    );

Now, I guess (although not being sure) that I'm supposed to add the current value to the history. However, I can't even try that out because I'm unable to get to the Thing typed contents of the document. I can only obtain a BSON object and that gives me rather little. I've also found this answer but it replaces the current value of a key in the dictionary instead of amending an additional one (also, I have a slight preference for lambda expressions if such is a viable option). The usual check at the official docs led only to more confusion. The following pseudo-code shows what I'm trying to get to.

...,
Builders<Thing>.Update.Set(
  b => b,
  new Thing
  {
    Name = b.name,
    Location = update.location,
    History = History.Add(DateTime.Now, b.Location)
  }
), ...

How should I go about it?

1 Answers

firstly, you need to store your dictionary key/value pairs as an array of documents in the db with the following attribute:

[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)]
public Dictionary<string, string> History { get; set; }

then you need to run an aggregation pipeline update with something like the following query:

db.Thing.updateOne(
    { Name: "NAME" },
    [
        {
            $set: { Location: "NEW LOC" }
        },
        {
            $set: {
                History: {
                    $concatArrays: [
                        "$History",
                        [{ k: "NEW DATE", v: "$Location" }]
                    ]
                }
            }
        }
    ])

unfortunately, the c# driver has no strongly typed support for pipeline updates nor operators like $concatArray. so you'll have to resort to a string based solution like so:

var filter = Builders<Thing>.Filter.Where(t => t.Name == "NAME");

var pipeline = PipelineDefinition<Thing, Thing>.Create(@"
{
    $set: {
        History: {
            $concatArrays: [
                '$History',
                [{ k: 'NEW DATE', v: '$Location' }]
            ]
        }
    }
}");

await collection.UpdateOneAsync(filter, pipeline);

have a look at this article for an alternative solution for running advanced queries like this.

Related