3

I am trying to update individual fields in a deeply nested array.

    public class Maker
    {
        public int Id { get; set; }
        public List<Vehicle> Vehicles { get; set; }
    }

    public class Vehicle
    {
        public int Id { get; set; }
        public int Price { get; set; }
        public List<Part> Parts { get; set; }
    }

    public class Part
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

Updating a field in a level-1 array works like a charm. Updating a field in a level-2 array fails.

 public void UpdateNestedArrayFields()
    {
        var client = new MongoClient();
        var db = client.GetDatabase("test");
        var makers = db.GetCollection<Maker>("makers");

        // Create a maker and add a vehicle
        var newPart = new Part() { Id = 34, Name = "Wheel" };
        var newVehicle = new Vehicle() { Id = 17, Price = 1000, Parts = new List<Part>() { newPart } };
        var newMaker = new Maker() { Id = 5, Vehicles = new List<Vehicle>() { newVehicle } };

        // Update vehicle's price (WORKS)
        makers.FindOneAndUpdate(
            m => m.Id == newMaker.Id && m.Vehicles.Any(v => v.Id == newVehicle.Id),
            Builders<Maker>.Update.Set(m => m.Vehicles[-1].Price, 2000));

        // Update part's name (**FAILS**)
        makers.FindOneAndUpdate(
            m => m.Id == newMaker.Id && m.Vehicles.Single(v => v.Id == newVehicle.Id).Parts.Any(p=> p.Id == newPart.Id),
            Builders<Maker>.Update.Set(m => m.Vehicles[-1].Parts[-1].Name, "Tire"));
    }       

I understand that multiple positional operators are not supported, so my last line (m.Vehicles[-1].Parts[-1].Name) is kind of crap.

Obviously I am not the only one having this problem and I found different solutions like this or that. However, none of them is type safe. The resulting code is pretty ugly and error prone.

So I was wondering: Is there any way to redesign my code so it is type safe?

I am using MongoDb 4.0.9 and C# driver 2.11.5.

Edit (01-11-2020): For anybody interested, I found this really nice extension by Massimiliano Kraus, which is a bit outdated (2017), but is still working and turned out to be a huge help. Thank you, Massimiliano! MongoDB.DeepUpdater.CSharp

2
  • Please try this and let me know, the Tire is not Maker type it is a Part type so if you change Maker to Part type and try (Builders<Maker> to Builders<Part>), it should works, let me know so I can investigate more or leave an answer. Commented Jan 4, 2021 at 5:45
  • @maytham: You want me to use Builders<Part>.Update... instead of Builders<Maker>.Update...in my last command? No, that won't even compile. I need to update a Part inside a Maker. I can't just update a "standalone" Part. Could you copy my code to a new console application and try your suggestion? Commented Jan 4, 2021 at 9:13

2 Answers 2

4
+100

in order to update nested arrays like this, you need to use the array filters feature of mongodb.

the needed mongo query is this:

db.Maker.update(
    {
        "_id": 5
    },
    {
        $set: { 'Vehicles.$[v].Parts.$[p].Name': 'Tyre' }
    },
    {
        arrayFilters: [
            { 'v._id': { $eq: 17 } },
            { 'p._id': { $eq: 34 } }
        ]
    }
)

the c# driver is not capable of generating the above in a strongly-typed fashion. have a look at this article for an alternative semi-typed solution to running advanced queries like the above.

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

3 Comments

Good morning and thank you for your contribution. The article you referred to shows a solution equal to the links I had already posted in my question. So I guess there is indeed no 100% type-safe solution, which is a pity. I wish the creatos of the C# driver could improve this at some point. Please let me wait a few more days to see if anyone has a nifty idea before I give you the bonus.
@Ingmar you could alternatively use a utility method like this to turn a typed member expression to a positional filter string that can be handed to the c# driver, but the resulting code would be less readable/ugly compared to the template/tag replacement idea on that article.
:Thank you again for your suggestions.I kept on searching for "built-in" ways to solve my problem type-safely. There really seems to be no way with the current c# driver. This is a pitty. However, I did find a really nice NuGet Package. I am going to add the link to my original post.
1

There is an other possible and more typed version of typed nested array update. it not fully typed, but it is closer to what u need.

its also using ArrayFilters but in more typed fashion

this code is coming straight from application that I'm working on.

var filter = Builders<EventModel>.Filter.Eq(s => s.Id, eventId);

var settingsPath = @$"{nameof(EventModel.EventLayout)}.{nameof(EventLayoutSetting.Pages)}.$[page].{nameof(Page.Components)}.$[component].{nameof(Component.Settings)}";
var update = Builders<EventModel>.Update.
                     Set(settingsPath, settings);

var dbResult = await eventsCollection.UpdateOneAsync(filter, update, new UpdateOptions 
            {
                ArrayFilters = new List<ArrayFilterDefinition> 
                {
                    new BsonDocumentArrayFilterDefinition<BsonDocument> (new BsonDocument($"page.{nameof(Page.PageId)}",pageId)),
                    new BsonDocumentArrayFilterDefinition<BsonDocument> (new BsonDocument($"component.{nameof(Component.ComponentId)}",componentId))
                }
            });

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.