Search code examples
c#mongodbmongodb-.net-driver

Mongo error when trying to update nested arrays: No array filter found for identifier


I am trying to update a document in Mongo that represents a community with the following scenario.

  • A community has a collection of blocks
  • A block has a collection of floors
  • A floor has a collection of doors
  • A door has a collection of label names

Given a document Id and information about the labels that must be placed into each door, I want to use the MongoDb C# driver v2.10.4 and mongo:latest to update nested lists (several levels). I've reading the documentation, about array filters, but I can't have it working.

I've created a repository from scratch to reproduce the problem, with instructions on the Readme on how to run the integration test and a local MongoDB with docker.

But as a summary, my method groupds the labels so that I can bulk place names on the desired door and then it iterates over these groups and updates on Mongo the specific document setting the desired value inside some levels deep nested object. I couldn't think of a more efficient way.

All the code in the above repo.

The DB document:

public class Community
{
    public Guid Id { get; set; }
    public IEnumerable<Block> Blocks { get; set; } = Enumerable.Empty<Block>();
}

public class Block
{
    public string Name { get; set; } = string.Empty;
    public IEnumerable<Floor> Floors { get; set; } = Enumerable.Empty<Floor>();
}

public class Floor
{
    public string Name { get; set; } = string.Empty;
    public IEnumerable<Door> Doors { get; set; } = Enumerable.Empty<Door>();
}

public class Door
{
    public string Name { get; set; } = string.Empty;
    public IEnumerable<string> LabelNames = Enumerable.Empty<string>();
}

The problematic method with array filters:

public async Task UpdateDoorNames(Guid id, IEnumerable<Label> labels)
{
    var labelsGroupedByHouse =
        labels
            .ToList()
            .GroupBy(x => new { x.BlockId, x.FloorId, x.DoorId })
            .ToList();

    var filter =
        Builders<Community>
            .Filter
            .Where(x => x.Id == id);

    foreach (var house in labelsGroupedByHouse)
    {
        var houseBlockName = house.Key.BlockId;
        var houseFloorName = house.Key.FloorId;
        var houseDoorName = house.Key.DoorId;
        var names = house.Select(x => x.Name).ToList();

        var update =
            Builders<Community>
                .Update
                .Set($"Blocks.$[{houseBlockName}].Floors.$[{houseFloorName}].Doors.$[{houseDoorName}].LabelNames", names);
        await _communities.UpdateOneAsync(filter, update);
    }
}

The exception is

MongoDB.Driver.MongoWriteException with the message "A write operation resulted in an error.
  No array filter found for identifier 'Block 1' in path 'Blocks.$[Block 1].Floors.$[Ground Floor].Doors.$[A].LabelNames'"

Here's a more visual sample on how the nested structure looks like in the database. Notice the value I want to update is the LabelNames, which is an array of string. enter image description here

I appreciate any help to have this working and suggestions on whether it's the right approach assuming that I cannot change the repository's method signature.


SOLUTION RESULT: Thanks for the quick answer @mickl, it works perfectly. Result at this repo's specific point of history exactly as suggested.


Solution

  • The $[{houseBlockName}] expects an identifier which acts as a placeholder and has a corresponding filter defined within arrayfilters (positional filtered). It seems like you're trying to pass the filter value directly which is incorrect.

    Your C# code can look like this:

    var houseBlockName = house.Key.BlockId;
    var houseFloorName = house.Key.FloorId;
    var houseDoorName = house.Key.DoorId;
    var names = house.Select(x => x.Name).ToList();
    
    var update = Builders<Community>.Update.Set("Blocks.$[block].Floors.$[floor].Doors.$[door].LabelNames", names);
    
    var arrayFilters = new List<ArrayFilterDefinition>();
    ArrayFilterDefinition<BsonDocument> blockFilter = new BsonDocument("block.Name", new BsonDocument("$eq", houseBlockName));
    ArrayFilterDefinition<BsonDocument> floorFilter = new BsonDocument("floor.Name", new BsonDocument("$eq", houseFloorName));
    ArrayFilterDefinition<BsonDocument> doorFilter = new BsonDocument("door.Name", new BsonDocument("$eq", houseDoorName));
    arrayFilters.Add(blockFilter);
    arrayFilters.Add(floorFilter);
    arrayFilters.Add(doorFilter);
    
    var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
    
    var result = _communities.UpdateOne(filter, update, updateOptions);