Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MongoDB Deserialization With Discriminator Issue

I have the following classes:

[BsonIgnoreExtraElements]
public class WidgetCollection
{
    [BsonId]
    public int AccountId { get; set; }
    public ReadOnlyCollection<Widget> Widgets { get; set; }
}


[BsonIgnoreExtraElements]
[BsonDiscriminator(RootClass = true)]
[BsonKnownTypes(typeof(OtherObject1), ...)]
public class Widget
{
    public ObjectId Id { get; set; }
    public string Title { get; set; }
    public int Position { get; set; }
    public WidgetKind Kind { get; set; }
    public bool IsActive { get; set; }
}

An example of an instance of this in the DB:

{ "_id" : 2993644, "Widgets" : [        {       "_t" : "Widget",        "_id" : ObjectId("504797b327e10b1e80c838ac"),   "Title" : "My Notes",   "Position" : 1,         "Kind" : 0,     "IsActive" : true } ] }

I then have an aggregation command that filters out elements in the "Widgets" array to return only elements whose "IsActive" is true. In this case, the command just returns the whole object.

var command = new CommandDocument
                        {
                            {"aggregate", "myCollection" },
                            {"pipeline", commandArray }
                        };

        var result = database.RunCommandAs<AggregateResult<WidgetCollection>>(command).Result;
        return result;

This is the AggregateResult class:

public class AggregateResult<T> : CommandResult
{
    public T Result
    {
        get
        {
           var result = Response["result"].AsBsonArray.SingleOrDefault().AsBsonDocument;
           return BsonSerializer.Deserialize<T>(result);
         }
     }
 }

Yes, I know that SingleOrDefault().AsBsonDocument could case a NRE, but that's not the issue right now. I have debugged the code up to the point of deserialization and have verified that the "result" variable contains the exact same BSON as I showed above. I receive this message when trying to deserialize the result: "An error occurred while deserializing the Widgets property of class Community.Widgets.WidgetCollection: Expected element name to be '_v', not '_id'".

Why is the deserializer expected a '_v' element?

UPDATE
I fixed the above problem by changed the type of the Widgets property to ICollection. I am now receiving this error: Unknown discriminator value 'Widget'. To me, this makes no sense since the doc has the "_t" : "Widget" element.

I also tried inserting a derived class, after which the "_t" element's value was now "[ "Widget", "DerivedClass"] as expected, and I get the same error. Again, this does not happen when using database.FindOneAs<>(), only when explicitly using BsonSerializer.Deserialize<>(). I tried adding this code right before calling Deserialize():

BsonClassMap.RegisterClassMap<Widget>(cm => { cm.AutoMap(); cm.SetIsRootClass(true); });
BsonClassMap.RegisterClassMap<RssFeedWidget>();

But I am not exactly sure where that initialization code should go, and I thought it was not needed if I was using discriminator attributes on my classes.

Here is my aggregation command

BsonDocument documentMatch = new BsonDocument{{"$match", new BsonDocument{{"_id", documentId}}}};
BsonDocument unwindArray = new BsonDocument{{"$unwind", "$Widgets"}};
BsonDocument arrayMatch = new BsonDocument{{"$match", new BsonDocument{{"Widgets.IsActive", true}}}});

BsonArray commandArray = new BsonArray();
commandArray.Add(documentMatch);
commandArray.Add(unwindArray),
commandArray.Add(arrayMatch);
var command = new CommandDocument
                  {
                      {"aggregate", collectionName},
                      {"pipeline", commandArray}
                  }

var result = database.RunCommandAs<AggregateResult<WidgetCollection>>(command).Result;
like image 385
anthv123 Avatar asked Sep 05 '12 19:09

anthv123


1 Answers

I cannot reproduce this issue. I used the below program. So, since I'm using a relatively trivial pipeline here, perhaps your issue is that your aggregation document isn't returning when you have mapped. Could you post your aggregation command?

using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace TestConsole_Source
{
    class Program
    {
        [BsonIgnoreExtraElements]
        public class WidgetCollection
        {
            [BsonId]
            public int AccountId { get; set; }
            public ReadOnlyCollection<Widget> Widgets { get; set; }
        }


        [BsonIgnoreExtraElements]
        [BsonDiscriminator(RootClass = true)]
        public class Widget
        {
            public string Title { get; set; }
            public int Position { get; set; }
            public bool IsActive { get; set; }
        }

        static void Main(string[] args)
        {
            var server = MongoServer.Create();
            server.Connect();

            var db = server.GetDatabase("widgettest");
            var collection = db.GetCollection<WidgetCollection>("widgets");
            collection.Drop();

            var widgets = new WidgetCollection();
            var widget1 = new Widget { Title = "One", Position = 0, IsActive = true };
            var widget2 = new Widget { Title = "Two", Position = 1, IsActive = true };
            var widget3 = new Widget { Title = "Three", Position = 2, IsActive = false };
            widgets.Widgets = new List<Widget> { widget1, widget2, widget3 }.AsReadOnly();

            collection.Save(widgets);

            var command = new CommandDocument(
                new BsonElement("aggregate", "widgets"),
                new BsonElement("pipeline", new BsonArray(new [] {
                    new BsonDocument(
                        new BsonElement("$project", new BsonDocument("Widgets", 1)))})));

            var response = db.RunCommand(command);
            var rawDoc = response.Response["result"].AsBsonArray.SingleOrDefault().AsBsonDocument;

            var doc = BsonSerializer.Deserialize<WidgetCollection>(rawDoc);

            //Console.ReadKey();
        }
    }
}

Update: Using the new information above, I have corrected your aggregation query to work correctly. You needed to use a group in order to get the results back into the same format as the driver was expecting.

    var command = new CommandDocument(
        new BsonElement("aggregate", "widgets"),
        new BsonElement("pipeline", new BsonArray(new[] {
            new BsonDocument(
                new BsonElement("$unwind", "$Widgets")),
            new BsonDocument(
                new BsonElement("$match", new BsonDocument("Widgets.IsActive", true))),
            new BsonDocument(
                new BsonElement("$group", new BsonDocument(
                    new BsonElement("_id", "$_id"),
                    new BsonElement("Widgets", new BsonDocument("$addToSet", "$Widgets")))))})));

Even given that this works, I'd still suggest to not go through this process and simply filter the Widgets client side.

like image 137
Craig Wilson Avatar answered Oct 26 '22 09:10

Craig Wilson