2

I have a User class:

public class User
{
    public Guid? Id { get; set; }
    public String? Name { get; set; }
    public Address? Address { get; set; }
}

and Address class:

public class Address
{
    public String? Street { get; set; }
    public String? City { get; set; }
    public String? State { get; set; }
}

I am trying to implement partial updating. This is done dynamically, but let's say someone sends to update a User's City and State, I do:

var filter = Builders<BsonDocument>.Filter.Eq("Id", id);
var updates = new List<UpdateDefinition<BsonDocument>>();
updates.Add(Builders<BsonDocument>.Update.Set("Address.City", "New City Value"));
updates.Add(Builders<BsonDocument>.Update.Set("Address.State", "New State Value"));
var update = Builders<BsonDocument>.Update.Combine(updates);
var bsonDocument = await collection.FindOneAndUpdateAsync(filter, update, new FindOneAndUpdateOptions<BsonDocument>
{
    ReturnDocument = ReturnDocument.After
});

This works well, except if a User's Address is null. In that case, I get the error:

MongoDB.Driver.MongoCommandException: Command findAndModify failed: Cannot create field 'City' in element {Address: null}.

Is there any way to ensure the Address object is created so that the City and State properties get set? I would like to do it without getting the current object from the database.

ScubaSteve
  • 7,724
  • 8
  • 52
  • 65

1 Answers1

3

Think that you may work with update with aggregation pipeline to update the Address field dynamically.

Work with $cond operator to check whether Address is null.

If yes, set the whole object to Address.

If no, merge the current Address value with the document.

The query below may looks complex:

db.collection.update({
  Id: /* Id */
},
[
  {
    $set: {
      Address: {
        $cond: {
          if: {
            $eq: [
              "$Address",
              null
            ]
          },
          then: {
            "City": "New City Value",
            "State": "New State Value"
          },
          else: {
            $mergeObjects: [
              "$Address",
              {
                "City": "New City Value",
                "State": "New State Value"
              }
            ]
          }
        }
      }
    }
  }
])
var addressDocument = new BsonDocument
{
    { "City", "New City Value" },
    { "State", "New State Value" }
};

var update = Builders<BsonDocument>.Update.Pipeline(new PipelineStagePipelineDefinition<BsonDocument, BsonDocument>
(
    new PipelineStageDefinition<BsonDocument, BsonDocument>[]
    {
        new BsonDocument("$set", 
            new BsonDocument("Address", 
                new BsonDocument("$cond", 
                    new BsonDocument
                    {
                        { 
                            "if", 
                            new BsonDocument("$eq", 
                                BsonArray.Create(new object[] { "$Address", null })) 
                        },
                        {
                            "then",
                            addressDocument
                        },
                        {
                            "else",
                            new BsonDocument("$mergeObjects", 
                                BsonArray.Create(new object[] { "$Address", addressDocument }))
                        }
                    }
                )
            )
        )
    }
));
Yong Shun
  • 35,286
  • 4
  • 24
  • 46
  • I am also possibly updating properties at the User level like Name. So, if Name, City, and State come in as updates, how would I include Name in this as well? – ScubaSteve Sep 07 '22 at 23:59
  • Yes, you can include the Name field in the `$set` stage. Example: https://gist.github.com/yongshun950824/b9d69545e7379b55c859a50e62481a57 – Yong Shun Sep 08 '22 at 00:08
  • Ok, I'll try to work this into my code where the properties getting updated are dynamic. – ScubaSteve Sep 08 '22 at 00:21
  • I have been trying to get this to work with my app, but because the properties to be updated are dynamic, I have to utilize a builder. Unfortunately, I do not see how to utilize builders and combine like I do with Update. – ScubaSteve Sep 14 '22 at 02:18
  • @ScubaSteve If you want to use builders you can try "var pipeline = EmptyPipelineDefinition<>" and use above mentioned aggregation in it. Then you can do "var update = Builders<>.Update.Pipeline(pipeline);" and finally "coll.UpdateOne(filter, update);" https://www.mongodb.com/community/forums/t/update-with-aggregation-pipeline-in-c-net/154146/2 – GPuri Feb 20 '23 at 08:25