Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MongoDB Custom Serializer to avoid _t being added collection, throws ReadEndArray Error?

Situation: Language: C# using the C# Driver I have a model that contains a List as a property. That List can contain one of 3 different models that all inherit the BaseModelClass. To assist in serialization of this situation Mongo adds _t to identify which of the models is actually being used. For us this is a problem due to the amount of space that _t is taking up. I am a lowly dev, I have asked for more space and ram and they have told me to solve it without the additional space. So I sat down to writing a custom serializer that handled the different types without writing a _t to the BSONDocument. I thought all was great until I started doing my unit testing of the serialization. I started getting "ReadEndArray can only be called when ContextType is Array, not when ContextType is Document."

Any advice or suggestions are greatly appreciated.

Here is the code I have thus far...

<---------Collection Model--------------------->

[BsonCollectionName("calls")]
[BsonIgnoreExtraElements]
public class Call
{
    [BsonId]
    public CallId _id { get; set; }

    [BsonElement("responses")]
    [BsonIgnoreIfNull]
    public IList<DataRecord> Responses { get; set; }
}

<----------Base Data Record------------------>

[BsonSerializer(typeof(DataRecordSerializer))]
public abstract class DataRecord
{
    [BsonElement("key")]
    public string Key { get; set; }
}

<-----------Examples of actual Data Records----------------->

[BsonSerializer(typeof(DataRecordSerializer))]
public class DataRecordInt : DataRecord
{
    [BsonElement("value")]
    public int Value { get; set; }
}

[BsonSerializer(typeof(DataRecordSerializer))]
public class DataRecordDateTime : DataRecord
{
    [BsonElement("value")]
    public DateTime? Value { get; set; }
}

<---------------Unit Test to trigger Deserializer----------------->

        //Arrange
        var bsonDocument = TestResources.SampleCallJson;

        //Act
        var result = BsonSerializer.Deserialize<Call>(bsonDocument);

        //Assert
        Assert.IsTrue(true);

<----------------Serializer----------------->

public class DataRecordSerializer : IBsonSerializer 
{
    public object Deserialize(BsonReader bsonReader, Type nominalType, IBsonSerializationOptions options)
    {
        //Entrance Criteria
        if(nominalType != typeof(DataRecord)) throw new BsonSerializationException("Must be of base type DataRecord.");
        if(bsonReader.GetCurrentBsonType() != BsonType.Document) throw new BsonSerializationException("Must be of type Document.");

        bsonReader.ReadStartDocument();
        var key = bsonReader.ReadString("key");
        bsonReader.FindElement("value");

        var bsonType = bsonReader.CurrentBsonType;

        if (bsonType == BsonType.DateTime)
        {
            return DeserializeDataRecordDateTime(bsonReader, key);
        }

        return bsonType == BsonType.Int32 ? DeserializeDataRecordInt(bsonReader, key) : DeserializeDataRecordString(bsonReader, key);
    }

    public object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options)
    {
        //Entrance Criteria
        if (nominalType != typeof (DataRecord)) throw new BsonSerializationException("Must be of base type DataRecord.");
        if (bsonReader.GetCurrentBsonType() != BsonType.Document) throw new BsonSerializationException("Must be of type Document.");

        bsonReader.ReadStartDocument(); // Starts Reading and is able to pull data fine through this and the next few lines of code.
        var key = bsonReader.ReadString("key");

        if (actualType == typeof(DataRecordDateTime))
        {
            return DeserializeDataRecordDateTime(bsonReader, key);
        }

        return actualType == typeof(DataRecordInt) ? DeserializeDataRecordInt(bsonReader, key) : DeserializeDataRecordString(bsonReader, key); // Once it tries to return I am getting the following Error: ReadEndArray can only be called when ContextType is Array, not when ContextType is Document.
    }

    public IBsonSerializationOptions GetDefaultSerializationOptions()
    {
        return new DocumentSerializationOptions
        {
            AllowDuplicateNames = false,
            SerializeIdFirst = false
        };
    }

    public void Serialize(BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options)
    {
        var currentType = value.GetType();
        if (currentType == typeof (DataRecordInt))
        {
            SerializeDataRecordInt(bsonWriter, value);
            return;
        }

        if (currentType == typeof(DataRecordDateTime))
        {
            SerializeDataRecordDateTime(bsonWriter, value);
            return;
        }

        if (currentType == typeof(DataRecordString))
        {
            SerializeDataRecordString(bsonWriter, value);
        }
    }

    private static object DeserializeDataRecordString(BsonReader bsonReader, string key)
    {
        var stringValue = bsonReader.ReadString();
        var isCommentValue = false;
        if (bsonReader.FindElement("iscomment"))
        {
            isCommentValue = bsonReader.ReadBoolean();
        }

        return new DataRecordString
        {
            Key = key,
            Value = stringValue,
            IsComment = isCommentValue
        };
    }

    private static object DeserializeDataRecordInt(BsonReader bsonReader, string key)
    {
        var intValue = bsonReader.ReadInt32();

        return new DataRecordInt
        {
            Key = key,
            Value = intValue
        };
    }

    private static object DeserializeDataRecordDateTime(BsonReader bsonReader, string key)
    {
        var dtValue = bsonReader.ReadDateTime();
        var dateTimeValue = new BsonDateTime(dtValue).ToUniversalTime();

        return new DataRecordDateTime
        {
            Key = key,
            Value = dateTimeValue
        };
    }

    private static void SerializeDataRecordString(BsonWriter bsonWriter, object value)
    {
        var stringRecord = (DataRecordString) value;
        bsonWriter.WriteStartDocument();

        var keyValue = stringRecord.Key;
        bsonWriter.WriteString("key", string.IsNullOrEmpty(keyValue) ? string.Empty : keyValue);

        var valueValue = stringRecord.Value;
        bsonWriter.WriteString("value", string.IsNullOrEmpty(valueValue) ? string.Empty : valueValue);

        bsonWriter.WriteBoolean("iscomment", stringRecord.IsComment);
        bsonWriter.WriteEndDocument();
    }

    private static void SerializeDataRecordDateTime(BsonWriter bsonWriter, object value)
    {
        var dateRecord = (DataRecordDateTime) value;
        var millisecondsSinceEpoch = dateRecord.Value.HasValue
            ? BsonUtils.ToMillisecondsSinceEpoch(new DateTime(dateRecord.Value.Value.Ticks, DateTimeKind.Utc))
            : 0;

        bsonWriter.WriteStartDocument();
        var keyValue = dateRecord.Key;
        bsonWriter.WriteString("key", string.IsNullOrEmpty(keyValue) ? string.Empty : keyValue);

        if (millisecondsSinceEpoch != 0)
        {
            bsonWriter.WriteDateTime("value", millisecondsSinceEpoch);
        }
        else
        {
            bsonWriter.WriteString("value", string.Empty);
        }

        bsonWriter.WriteEndDocument();
    }

    private static void SerializeDataRecordInt(BsonWriter bsonWriter, object value)
    {
        var intRecord = (DataRecordInt) value;
        bsonWriter.WriteStartDocument();

        var keyValue = intRecord.Key;
        bsonWriter.WriteString("key", string.IsNullOrEmpty(keyValue) ? string.Empty : keyValue);

        bsonWriter.WriteInt32("value", intRecord.Value);

        bsonWriter.WriteEndDocument();
    }
}
like image 551
aeatonDev Avatar asked Feb 20 '15 19:02

aeatonDev


2 Answers

Also asked here: https://groups.google.com/forum/#!topic/mongodb-user/iOeEXbUYbo4

I think your better bet in this situation is to use a custom discriminator convention. You can see an example of this here: https://github.com/mongodb/mongo-csharp-driver/blob/v1.x/MongoDB.DriverUnitTests/Samples/MagicDiscriminatorTests.cs. While this example is based on whether a field exists in the document, you could easily base it on what type the field is (BsonType.Int32, BsonType.Date, etc...).

like image 162
Craig Wilson Avatar answered Oct 02 '22 19:10

Craig Wilson


Basing on @Craig Wilson answer, to get rid off all discriminators, you can:

public class NoDiscriminatorConvention : IDiscriminatorConvention
    {
        public string ElementName => null;

        public Type GetActualType(IBsonReader bsonReader, Type nominalType) => nominalType;

        public BsonValue GetDiscriminator(Type nominalType, Type actualType) => null;
    }

and register it:

BsonSerializer.RegisterDiscriminatorConvention(typeof(BaseEntity), new NoDiscriminatorConvention());
like image 22
darthkurak Avatar answered Oct 02 '22 17:10

darthkurak