Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MongoDB C# driver type discriminators with generic class inheriting from non-generic base class

I'm trying to store a list of objects of a generic class that inherits from a non-generic base class in mongodb using the official C# driver.

My code looks like this:

abstract class MyAbstractClass {}

class MyGenericClass<T>: MyAbstractClass
{
    [BsonRequired]
    public List<T> Values = new List<T>();

    public MyGenericClass(IEnumerable<T> values) 
    {
        Values.AddRange(values);
    }
}

class MyContainerClass
{
    [BsonId]
    public string Id;

    [BsonRequired]
    public List<MyAbstractClass> ValueCollections = new List<MyAbstractClass>();

    public MyContainerClass()
    {
        Id = Guid.NewGuid().ToString();
    }
}

When testing, I create a container object and fill it with instances of the generic class, like so:

var container = new MyContainerClass();
container.ValueCollections.Add(new MyGenericClass<string>(new[]{"foo","bar","baz"}));

When I save this to the DB, the documents added look like this:

{
"_id": "c5cf5cd1-843f-4d5d-ba8f-5859ae62fd1d",
"ValueCollections": [
    {
        "_t": "MyGenericClass`1",
        "Values": [
            "foo",
            "bar",
            "baz"
        ]
    }
]
}

The type discriminator gets type "MyGenericClass'1" instead of "MyGenericClass'1[System.String]" which means that it can't possibly deserialize this again.

Also, when trying to load these objects from the DB I get an error message: Instances of abstract classes cannot be created. But the type discriminator (if it were correct) should allow the driver to see that it should not created objects of type MyAbstractClass but of MyGenericClass

So my questions are: 1. Why do I get this error? 2. Why doesn't it serialize the discriminator correctly?

Thank you for your time.

like image 778
DukeOf1Cat Avatar asked Sep 17 '14 13:09

DukeOf1Cat


3 Answers

After some experimenting, I found out that you can write your own discriminator conventions. I can't really understand why, but the default discriminator convention seems to use the Name property of the type class, instead of the FullName, which makes it useless for generic classes.

I ended up using this code instead:

class FooDiscriminatorConvention : IDiscriminatorConvention
{
    public string ElementName
    {
        get { return "_t"; }
    }

    public Type GetActualType(MongoDB.Bson.IO.BsonReader bsonReader, Type nominalType)
    {
        if(nominalType!=typeof(MyAbstractClass))
            throw new Exception("Cannot use FooDiscriminator for type " + nominalType);

        var ret = nominalType;

        var bookmark = bsonReader.GetBookmark();
        bsonReader.ReadStartDocument();
        if (bsonReader.FindElement(ElementName))
        {
            var value = bsonReader.ReadString();

            ret = Type.GetType(value);

            if(ret==null)
                throw new Exception("Could not find type " + value);

            if(!ret.IsSubclassOf(typeof(MyAbstractClass)))
                throw new Exception("Database type does not inherit from MyAbstractClass.");
        }

        bsonReader.ReturnToBookmark(bookmark);

        return ret;
    }

    public BsonValue GetDiscriminator(Type nominalType, Type actualType)
    {
        if (nominalType != typeof(MyAbstractClass))
            throw new Exception("Cannot use FooDiscriminator for type " + nominalType);

        return actualType.FullName;
    }
}

And registering it with

BsonSerializer.RegisterDiscriminatorConvention(typeof(MyGenericClass<>), new FooDiscriminatorConvention()); //is this needed?
BsonSerializer.RegisterDiscriminatorConvention(typeof(MyAbstractClass), new FooDiscriminatorConvention());

I also had to make the base class non-abstract, to avoid the "cannot crete instances of abstract classes" error. It would be nice to be able to have an abstract base class, but since the derived class is generic I cannot use BsonKnownTypes.

like image 96
DukeOf1Cat Avatar answered Oct 02 '22 22:10

DukeOf1Cat


Maybe it's a bit late but we were having the same issues with generic Types, they were saved as the type Name not the FullName. We were initially using BsonSerializer.RegisterDiscriminator for all the types in our project, even making all of them generic, but after a lot of test-and-fail we ended up with the BsonClassMap

Non working approach...

      KnownPayloadTypes
            .Concat(KnownMessageTypes)
            .ConcatOne(typeof(object))
            .ConcatOne(typeof(Message))
            .ForEach(t => BsonSerializer.RegisterDiscriminator(t, t.FullName));

Working...

    private static readonly IEnumerable<Type> KnownMessageTypes = KnownPayloadTypes
        .Select(t => typeof(Message<>).MakeGenericType(t));

      KnownPayloadTypes
            .Concat(KnownMessageTypes)
            .ConcatOne(typeof(object))
            .ConcatOne(typeof(Message))
            .ForEach(t =>
            {
                var bsonClassMap = new BsonClassMap(t);
                bsonClassMap.AutoMap();
                bsonClassMap.SetDiscriminator(t.FullName);
                BsonClassMap.RegisterClassMap(bsonClassMap);
            }
            );
like image 35
Francisco Avatar answered Oct 02 '22 22:10

Francisco


The above worked for us with a couple minor changes. Thank you so much for your help DukeOf1Cat! And thanks Jeff Rasmussen for helping sort this out on our end! We finally found this after trying to create custom serializers, different RegisterClassMap lambda's and so on.

Our changes to the above solution are here.

class IRestrictionDiscriminatorConvention : IDiscriminatorConvention
{
    public string ElementName
    {
        get { return "_t"; }
    }

    public Type GetActualType(BsonReader bsonReader, Type nominalType)
    {
        //Edit: added additional check for list
        if (nominalType != typeof(IRestriction) && nominalType != typeof(List<IRestriction>))
            throw new Exception("Cannot use IRestrictionDiscriminatorConvention for type " + nominalType);

        var ret = nominalType;

        var bookmark = bsonReader.GetBookmark();
        bsonReader.ReadStartDocument();
        if (bsonReader.FindElement(ElementName))
        {
            var value = bsonReader.ReadString();

            ret = Type.GetType(value);

            if (ret == null)
                throw new Exception("Could not find type from " + value);
            //Edit: doing the checking a little different
            if (!typeof(IRestriction).IsAssignableFrom(ret) && !ret.IsSubclassOf(typeof(IRestriction)))
                throw new Exception("type is not an IRestriction");
        }

        bsonReader.ReturnToBookmark(bookmark);

        return ret;
    }

    public BsonValue GetDiscriminator(Type nominalType, Type actualType)
    {
        if (nominalType != typeof(IRestriction) && nominalType != typeof(List<IRestriction>))
            throw new Exception("Cannot use FooDiscriminator for type " + nominalType);

        /*Edit: had to change this because we were getting Unknown discriminator value 
       'PackNet.Common.Interfaces.RescrictionsAndCapabilities.BasicRestriction`1[[System.String, ... */
        return actualType.AssemblyQualifiedName;
    }
}

The class we were having problems was this:

public class BasicRestriction<T> : IRestriction
{
    public T Value { get; set; }

    public BasicRestriction()
    {
    }
    public BasicRestriction(T value)
    {
        Value = value;
    }
}

And the class that contained a list of Irestrictions could never be deserialized because when it got to the list of I restrictions each had a _t discriminator value of "BasicRestriction`1" for the generic type:

public class Carton{ public List<IRestriction> Restrictions { get; set; }}
like image 38
Robel Robel Lingstuyl Avatar answered Oct 02 '22 22:10

Robel Robel Lingstuyl