Problems with a customer serializer meant to avoid _t being added to document collection

2,107 views
Skip to first unread message

Andy Eaton

unread,
Feb 20, 2015, 2:43:26 PM2/20/15
to mongod...@googlegroups.com
Situation: 
Language: C# using the C# Driver
I have a model that contains a List<BaseModelClass> 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();
        }
    }

Craig Wilson

unread,
Feb 20, 2015, 3:27:45 PM2/20/15
to mongod...@googlegroups.com
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...).

Let me know how it goes and if you need some further help.

Andy Eaton

unread,
Feb 20, 2015, 4:10:28 PM2/20/15
to mongod...@googlegroups.com
Thus far it looks like this is what I needed to begin with. However I am getting the following error on the Register Line. "There is already a discriminator convention registered for type EventManagement.Data.Model.SurveyDataRecord". Thoughts? I did see a way to lookup the current one, but nothing that would allow me to remove/replace it. There is nothing to my knowledge that is doing anything like registering in the current code base.

Craig Wilson

unread,
Feb 20, 2015, 4:17:06 PM2/20/15
to mongod...@googlegroups.com
You have to make sure this is done at the very beginning of the application, way before anything else is done with your entities. Make sure it's the very first thing in your configuration and hopefully that'll go away. If not, let me know and I'll probably need some repro code.

Andy Eaton

unread,
Feb 20, 2015, 5:38:17 PM2/20/15
to mongod...@googlegroups.com
I moved the register to the parent class and added some escape logic if it already existed. Once everything was said and done and unit tested this is what I ended up with:

        public Type GetActualType(BsonReader bsonReader, Type nominalType)
        {
            var bookmark = bsonReader.GetBookmark();
            bsonReader.ReadStartDocument();
            var actualType = nominalType;
            while (bsonReader.ReadBsonType() != BsonType.EndOfDocument)
            {
                bsonReader.SkipName();
                bsonReader.SkipValue();
                bsonReader.FindElement("value");
                
                var currentBsonType = bsonReader.CurrentBsonType;
                if (currentBsonType == BsonType.Int32)
                {
                    actualType = typeof(SurveyDataRecordInt);
                    break;
                }
                else if (currentBsonType == BsonType.DateTime)
                {
                    actualType = typeof(SurveyDataRecordDateTime);
                    break;
                }
                else
                {
                    actualType = typeof(SurveyDataRecordString);
                    break;
                }
            }
            bsonReader.ReturnToBookmark(bookmark);
            return actualType;

Andy Eaton

unread,
Feb 27, 2015, 10:44:01 AM2/27/15
to mongod...@googlegroups.com
To follow up on this. 

While in our integration testing this worked, in the actual application it didn't. We have Mongo tucked away inside of a repository library that also handles access to Sql, Teradata, and various Rest APIs. All of those data sources are hidden behind a standard set of interfaces and we have a strong desire to keep the executing application oblivious of the source of the data. In that repository layer we could never beat the HierarchialDiscriminatorConvention to the registration and we didn't want to do that registration inside of the executing application. There is also no way to remove/replace the current registered convention. At this point we are looking at a few different options both within mongo and outside of mongo.
Reply all
Reply to author
Forward
0 new messages