如何使用C# MongoDb驱动程序在深度嵌套的数组中“类型安全”地更新字段?

3

我正在尝试更新深度嵌套数组中的单个字段。

    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; }
    }

更新一级数组中的字段非常顺利。更新二级数组中的字段失败了。
 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"));
    }       

我知道不支持多个定位操作符,因此我的最后一行代码(m.Vehicles[-1].Parts[-1].Name)有些烂。

显然,我不是唯一遇到这个问题的人,我找到了不同的解决方案,如thisthat,但是它们都不是类型安全的。生成的代码非常丑陋且容易出错。

所以我想知道:是否有任何方法重新设计我的代码使其类型安全?

我正在使用MongoDb 4.0.9和C#驱动程序2.11.5。

编辑(2020年1月11日): 对于任何感兴趣的人,我发现了这个非常好的扩展程序,由Massimiliano Kraus开发,虽然有点过时(2017年),但仍然有效,并且证明是一个巨大的帮助。谢谢你,Massimiliano!MongoDB.DeepUpdater.CSharp


请尝试一下并让我知道,轮胎不是制造商类型,而是零件类型,因此如果您将制造商更改为零件类型并尝试(Builders<Maker> to Builders<Part>),它应该可以工作,请告诉我,这样我就可以进一步调查或留下答案。 - Maytham Fahmi
@maytham:你想让我在最后一个命令中使用Builders<Part>.Update...而不是Builders<Maker>.Update...吗?不,那甚至无法编译。我需要更新制造商内的零件。我不能只更新“独立”的零件。你能把我的代码复制到一个新的控制台应用程序中并尝试你的建议吗? - Ingmar
2个回答

4
为了更新像这样的嵌套数组,您需要使用 MongoDB 的 array filters 功能进行操作。
所需的 MongoDB 查询如下:
db.Maker.update(
    {
        "_id": 5
    },
    {
        $set: { 'Vehicles.$[v].Parts.$[p].Name': 'Tyre' }
    },
    {
        arrayFilters: [
            { 'v._id': { $eq: 17 } },
            { 'p._id': { $eq: 34 } }
        ]
    }
)

C#驱动程序无法以强类型方式生成上述内容。请参考this article,了解一种替代的半类型化解决方案,以运行类似上述的高级查询。

1
早上好,感谢您的贡献。您提到的文章展示了与我在问题中已经发布的链接相同的解决方案。因此,我想确实没有100%类型安全的解决方案,这很遗憾。我希望C#驱动程序的创建者能够在某个时候改进这一点。在我给您奖金之前,请再等待几天,看看是否有什么巧妙的想法。 - Ingmar
@Ingmar,你也可以使用类似于这个的实用方法,将一个类型化的成员表达式转换为可传递给C#驱动程序的位置过滤器字符串,但是与该文章中的模板/标签替换想法相比,生成的代码会更难读/丑。 - Dĵ ΝιΓΞΗΛψΚ
1
再次感谢您的建议。我一直在寻找“内置”的方法来安全地解决我的问题。目前的C#驱动程序似乎真的没有办法。这很遗憾。但是,我确实找到了一个非常好的NuGet包。我将在原始帖子中添加链接。 - Ingmar

1

有另一种可能更加类型化的嵌套数组更新版本。 它不是完全类型化的,但更接近您所需的内容。

它还使用ArrayFilters,但以更类型化的方式。

这段代码直接来自我正在工作的应用程序。

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))
                }
            });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接