14
votes

I am developing a project that uses MongoDB (with C# driver) and DDD.

I have a class (aggregate) which have a property which type is an interface. In another class, I have implemented this interface. This class has another property which type is an interface and is setted with another implemented class.

The code below explains better:

// Interfaces
public interface IUser {
    Guid Id { get; set;}
    IPartner Partner{ get; set; }
}

public interface IPartner {
    IPhone Mobile { get; set; }
}

public interface IPhone {
    string number { get; set; }
}

// Implemented Classes
public class User: IUser {
    [BsonId(IdGenerator = typeof(GuidGenerator))]
    public Guid Id { get; set; }

    [BsonIgnoreIfNull]
    public IPartner Partner { get; set; }
}

public struct Partner : IPartner {
    public IPhone Mobile { get; set; }
}

public struct Phone : IPhone {
    public string Number { get; set; }
}

Well, when I call the MongoCollection<User>.Insert() method, it throws two exceptions:

System.IO.FileFormatException: An error occurred while deserializing the Partner property of class .User: An error occurred while deserializing the Phone property of class .Partner: Value class .Mobile cannot be deserialized. ---> System.IO.FileFormatException: An error occurred while deserializing the Mobile property of class .Partner: Value class .Phone cannot be deserialized. ---> MongoDB.Bson.BsonSerializationException: Value class .Phone cannot be deserialized.

Then, I searched the internet for discover how to deserialize the type as an interface, and I think I have to ways to do it: mapping the property with a cast, using the BsonClassMap.RegisterClassMap or writing a custom BSON serializer.

I need to know which of these two ways is better and how to implement it.

Note: I need a solution that does not modify the interfaces, because theirs project cannot contain any external reference.

4
I'd try them both out and see which one is easier. Obviously, writing a custom serializer will be difficult. We are looking into figuring out how to deserialize this type of structure better.Craig Wilson
Thank you again, Craig. I will try to register the class map before try the custom serializer. So, the driver does not have this feature to desserialize to interfaces? If it is planned, can you have me a link from MongoDB Jira for following, like the LinQ projection?Gustavo Gondim
Thank you very much. I am voting and following this issue by now.Gustavo Gondim

4 Answers

14
votes

Well, I have found a lot of problems when trying to get this answer.

First of all, the MongoDB C# Driver, does have some problems when deserializing interfaces, like said by Craig Wilson in this question comments, and as described in the issue page.

The secure implementation for this problem, like I said before, really may be a custom BSON serializer or a specific class map, using BsonClassMap.RegisterClassMap.

So, I have implemented the class map and the problem persisted.

Looking forward with the problem, I have found that exception is related to another issue of the driver: the problem when deserializing structs.

I have rolled back the project to the initial state (without classes map or custom serializers) and changed the struct type to class type, and it worked.

In resume, this exception error is related to structs deserialization, not with interfaces deserialization.


Anyway, it is a real problem, and the second issue needs to be considered more a bug than a improvement, like the first issue is.

You can find the issues at these links:

9
votes

[BsonSerializer(typeof(ImpliedImplementationInterfaceSerializer<IReviewExpert, ReviewExpert>))] public IReviewExpert Expert { get; set; }

works for me

1
votes

We are on the 1.x branch of mongo drivers and that sadly doesn't have the ImpliedImplementationInterfaceSerializer that was suggested by Robert Baker which seems to be otherwise a good solution. To this end I created my own serializer that allows you to specify a confcrete type for an interface member.

public class ConcreteTypeSerializer<TInterface, TImplementation> : BsonBaseSerializer where TImplementation : TInterface
{
    private readonly Lazy<IBsonSerializer> _lazyImplementationSerializer;

    public ConcreteTypeSerializer()
    {
        var serializer = BsonSerializer.LookupSerializer(typeof(TImplementation));

        _lazyImplementationSerializer = new Lazy<IBsonSerializer>(() => serializer);
    }

    public override object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options)
    {
        if (bsonReader.GetCurrentBsonType() == BsonType.Null)
        {
            bsonReader.ReadNull();
            return default(TInterface);
        }
        else
        {
            return _lazyImplementationSerializer.Value.Deserialize(bsonReader, nominalType, typeof(TImplementation), options);
        }
    }

    public override void Serialize(BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options)
    {
        if (value == null)
        {
            bsonWriter.WriteNull();
        }
        else
        {
            var actualType = value.GetType();
            if (actualType == typeof(TImplementation))
            {
                _lazyImplementationSerializer.Value.Serialize(bsonWriter, nominalType, (TImplementation)value, options);
            }
            else
            {
                var serializer = BsonSerializer.LookupSerializer(actualType);
                serializer.Serialize(bsonWriter, nominalType, value, options);
            }
        }
    }
}

Usage is as follows:

[BsonSerializer(typeof(ConcreteTypeSerializer<IMyInterface,MyClass>))]
public IMyInterface MyProperty {get; set;}

A few notes on the code - all it really does is lazily loads the serializer for the appropriate concrete type and then passes on all the serialize/deserialize calls to that with the appropriate concrete type instead of the interface.

It also checks that the type is actually of the expected type and if not just finds the default serializer for the type.

0
votes

Registering one concrete serializer did not work for me as the object I wanted to store included a list of an interface that had like 6 different implementations. Using CosmosDB this stored fine with some Newtonsoft settings. It serialized the type of the implementation into the object and called it $type. So, I figured I´ll just do the same to make it work in MongoDB.

public class BsonTypeSerializer<T> : IBsonSerializer<T>
{
    public Type ValueType { get => typeof(T); }

    public T Deserialize(
        BsonDeserializationContext context, 
        BsonDeserializationArgs args)
    {
        var document = BsonSerializer.Deserialize<BsonDocument>(context.Reader);
        var typeStr = document.GetValue("$type").AsString;
        var type = Type.GetType(typeStr);
        var result = (T) BsonSerializer.Deserialize(document, type);
        return result;
    }

    public void Serialize(
        BsonSerializationContext context, 
        BsonSerializationArgs args,
        T value)
    {
        var typeStr = value.GetType().FullName;
        BsonDocument document = value.ToBsonDocument();
        document.Add(new BsonElement("$type", BsonValue.Create(typeStr)));
        BsonSerializer.Serialize(context.Writer, typeof(BsonDocument), document);
    }

    public void Serialize(
        BsonSerializationContext context,
        BsonSerializationArgs args,
        object value)
        => Serialize(context, args, (T) value);

    object IBsonSerializer.Deserialize(
        BsonDeserializationContext context,
        BsonDeserializationArgs args)
        => Deserialize(context, args);
}

You would give this your interface as T when registering it. The concrete type will be stored in the Document.

This would however break if you refractor your class names or even the namespace they are in. So, not a good idea to store everything like this. I´ve used it for a one off configuration file.