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

I am trying to keep a list of generic class objects 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, for example:

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

      

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

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

      

The discriminator type gets the type "MyGenericClass'1" instead of "MyGenericClass'1 [System.String]", which means it cannot deserialize this again.

Also, when trying to load these objects from the DB, I get an error: instances of abstract classes cannot be created. But the type discriminator (if he was right) should let the driver see that he should not create objects of type MyAbstractClass, but MyGenericClass

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

Thank you for your time.

+3


source to share


3 answers


After some experimentation, I found out that you can write your own discriminator conventions. I can't figure out why, but the default discriminator convention seems to be using the Name property of the type class rather than FullName, making it useless for generic classes.

I ended up using this code:

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 "can not crete examples of abstract classes" error. It would be nice to have an abstract base class, but since the derived class is generic, I cannot use BsonKnownTypes.

+6


source


The above worked for us with some minor minor changes. Thank you so much for helping DukeOf1Cat! And thanks to Jeff Rasmussen for helping me figure this out! We finally found this while trying to create custom serializers, various lambda RegisterClassMap, etc.

Our changes to the above solution are found 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 with had the following:



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

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

      

And the class containing the list of constraints can never be deserialized because when it got into the list of constraints, they each had the _t value of the discriminator "BasicRestriction`1" for the generic type:

public class Carton{ public List<IRestriction> Restrictions { get; set; }}

      

+1


source


It might be a little late, but we had the same problems with generic types, they were stored as type Name, not FullName. We originally used BsonSerializer.RegisterDiscriminator for all types in our project, even making all of them generic, but after a lot of tests and failures, we ended up with BsonClassMap

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

      

+1


source







All Articles