[CSHARP-2860] custom serializer not being called Created: 25/Nov/19  Updated: 27/Oct/23  Resolved: 18/Dec/19

Status: Closed
Project: C# Driver
Component/s: BSON
Affects Version/s: 2.9.3
Fix Version/s: None

Type: Bug Priority: Major - P3
Reporter: Suraj Gupta Assignee: Mikalai Mazurenka (Inactive)
Resolution: Works as Designed Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Related
related to CSHARP-2877 Default BsonSerializationArgs sometim... Closed

 Description   

Below is a repro (in an xUnit test method) of a case where a custom serializer is being registered properly and used for serialization, but isn't getting used for deserialization:

namespace Test
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using MongoDB.Bson;
    using MongoDB.Bson.IO;
    using MongoDB.Bson.Serialization;
    using MongoDB.Bson.Serialization.Options;
    using MongoDB.Bson.Serialization.Serializers;
    using OBeautifulCode.Serialization.Bson;
    using Xunit;
 
    public static class CustomSerializerTest
    {
        [Fact]
        public static void Test()
        {
            // setup the serializer
            var classMap = new BsonClassMap<MyTestModel>();
            var memberInfo = typeof(MyTestModel).GetMember(nameof(MyTestModel.MyProperty)).Single();
            var memberMap = classMap.MapMember(memberInfo);
            var keySerializer = new MyDateTimeSerializer();
            var valueSerializer = new StringSerializer();
            var dictionarySerializer =
                new MyDictionarySerializer<IReadOnlyDictionary<DateTime, string>, DateTime, string>(
                    DictionaryRepresentation.ArrayOfDocuments, keySerializer, valueSerializer);
            var serializer =
                new MyListSerializer<IReadOnlyList<IReadOnlyDictionary<DateTime, string>>,
                    IReadOnlyDictionary<DateTime, string>>(dictionarySerializer);
            memberMap.SetSerializer(serializer);
            BsonClassMap.RegisterClassMap(classMap);
            var expected = new MyTestModel
            {
                MyProperty =
                    new List<IReadOnlyDictionary<DateTime, string>>
                    {
                        new Dictionary<DateTime, string>
                        {
                            {DateTime.Now, "whatever"},
                        },
                    },
            };
 
            // serialize
            var document = new BsonDocument();
            using (var writer = new BsonDocumentWriter(document))
            {
                BsonSerializer.Serialize(writer, expected.GetType(), expected);
                writer.Close();
            }
            
            // prove that our serializers are being called
            // you can also put breakpoints in all the Serialize methods and run in the debugger
            // all the breakpoints will be hit.
            var actualJson = document.ToJson();
            var expectedJson =
                "{ \"_t\" : \"MyTestModel\", \"MyProperty\" : { \"_t\" : \"ReadOnlyCollection`1\", \"_v\" : [[{ \"k\" : \"does-not-matter\", \"v\" : \"whatever\" }]] } }";
            Assert.Equal(expectedJson, actualJson);
            
            // de-serialize.  throws FormatException ("String was not recognized as a valid DateTime")
            // proves that MyDateTimeSerializer is NOT being called.  you can also put breakpoints
            // in all of the Deserialize methods and run in debugger - it hit MyListSerializer
            // but doesn't hit MyDictionarySerializer
            ObcBsonSerializerHelper.DeserializeFromDocument<MyTestModel>(document);
        }
 
        private class MyTestModel
        {
            public IReadOnlyList<IReadOnlyDictionary<DateTime, string>> MyProperty { get; set; }
        }
 
        private class MyListSerializer<TCollection, TElement> : SerializerBase<TCollection>
            where TCollection : class, IEnumerable<TElement>
        {
            private readonly ReadOnlyCollectionSerializer<TElement> underlyingSerializer;
 
            public MyListSerializer(IBsonSerializer<TElement> elementSerializer)
            {
                this.underlyingSerializer = new ReadOnlyCollectionSerializer<TElement>(elementSerializer);
            }
 
            /// <inheritdoc />
            public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args,
                TCollection value)
            {
                if (value == null)
                {
                    context.Writer.WriteNull();
                    return;
                }
 
                this.underlyingSerializer.Serialize(context, args, new ReadOnlyCollection<TElement>(value.ToList()));
            }
 
            /// <inheritdoc />
            public override TCollection Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
            {
                if (context.Reader.State != BsonReaderState.Type && context.Reader.CurrentBsonType == BsonType.Null)
                {
                    context.Reader.ReadNull();
                    return null;
                }
 
                var collection = this.underlyingSerializer.Deserialize(context, args);
                var result = collection.ToList() as TCollection;
                return result;
            }
        }
 
        private class MyDictionarySerializer<TDictionary, TKey, TValue> : SerializerBase<TDictionary>
            where TDictionary : class, IEnumerable<KeyValuePair<TKey, TValue>>
        {
            private readonly DictionaryInterfaceImplementerSerializer<Dictionary<TKey, TValue>> underlyingSerializer;
 
            public MyDictionarySerializer(DictionaryRepresentation dictionaryRepresentation,
                IBsonSerializer keySerializer, IBsonSerializer valueSerializer)
            {
                this.underlyingSerializer =
                    new DictionaryInterfaceImplementerSerializer<Dictionary<TKey, TValue>>(dictionaryRepresentation,
                        keySerializer, valueSerializer);
            }
 
            /// <inheritdoc />
            public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args,
                TDictionary value)
            {
                if (value == null)
                {
                    context.Writer.WriteNull();
                    return;
                }
 
                this.underlyingSerializer.Serialize(context, args,
                    ((IDictionary<TKey, TValue>) value).ToDictionary(_ => _.Key, _ => _.Value));
            }
 
            /// <inheritdoc />
            public override TDictionary Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
            {
                if ((context.Reader.State != BsonReaderState.Type) && (context.Reader.CurrentBsonType == BsonType.Null))
                {
                    context.Reader.ReadNull();
                    return null;
                }
 
                var dictionary = this.underlyingSerializer.Deserialize(context, args);
                var result = new ReadOnlyDictionary<TKey, TValue>(dictionary) as TDictionary;
                return result;
            }
        }
 
        private class MyDateTimeSerializer : SerializerBase<DateTime>
        {
            private const string DoesNotMatter = "does-not-matter";
 
            /// <inheritdoc />
            public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DateTime value)
            {
                context.Writer.WriteString(DoesNotMatter);
            }
 
            /// <inheritdoc />
            public override DateTime Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
            {
                var type = context.Reader.GetCurrentBsonType();
                if ((type == BsonType.String) && (context.Reader.ReadString() == DoesNotMatter))
                {
                    return DateTime.Now;
                }
 
                throw new NotSupportedException();
            }
        }
    }
}



 Comments   
Comment by Robert Stam [ 18/Dec/19 ]

It is definitely possible to solve your scenario by composing a family of existing serializers configured correctly.

We are simply suggesting that there is an easier way.

Comment by Suraj Gupta [ 18/Dec/19 ]

I strongly disagree.  What you are basically saying is that I shouldn't think about custom serializers as building blocks that can be stacked/assembled together which violates the DRY principle.  Anyways, I got this to work by looking at the Bson source and identifying why my serializer isn't being called.  I'm forgetting the details, but here's what I have in my code, in the Serialize method of my collection serializer, in case it helps others folks...

 

// We HAVE to set the NominalType to ReadOnlyCollection<TElement>,
// otherwise the BSON framework serializes in a way that, upon deserialization,
// doesn't used the specified elementSerializer.
var argsNominalType = args.NominalType;
args.NominalType = typeof(ReadOnlyCollection<TElement>);

this.underlyingSerializer.Serialize(context, args, wrappedValue);

// restore the NominalType
args.NominalType = argsNominalType;

Comment by Mikalai Mazurenka (Inactive) [ 13/Dec/19 ]

Hi suraj@cometrics.com!

Thank you for posting this case!

Your scenario is rather complicated. It would be much simpler to write a single custom serializer to handle the entire MyProperty value rather than trying to correctly configure a family of serializers to achieve the intended result.

I recommend the following solution using a single custom serializer (MyPropertySerializer):

using System;
using System.Collections.Generic;
using System.Globalization;
using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using Xunit;
 
namespace Test
{
    public class CustomSerializerTest
    {
        [Fact]
        public void Test()
        {
            BsonClassMap.RegisterClassMap<MyTestModel>(cm =>
            {
                cm.AutoMap();
                cm.GetMemberMap(m => m.MyProperty).SetSerializer(new MyPropertySerializer());
            });
            var model = new MyTestModel
            {
                MyProperty = new List<IReadOnlyDictionary<DateTime, string>>
                {
                    new Dictionary<DateTime, string>
                    {
                        {DateTime.UtcNow, "whatever"}
                    }
                }
            };
 
            var json = model.ToJson();
            var rehydrated = BsonSerializer.Deserialize<MyTestModel>(json);
 
            Assert.Equal(model.MyProperty, rehydrated.MyProperty);
        }
 
        private class MyTestModel
        {
            public IReadOnlyList<IReadOnlyDictionary<DateTime, string>> MyProperty { get; set; }
        }
 
        private class MyPropertySerializer : SerializerBase<IReadOnlyList<IReadOnlyDictionary<DateTime, string>>>
        {
            /// <inheritdoc />
            public override IReadOnlyList<IReadOnlyDictionary<DateTime, string>> Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
            {
                var reader = context.Reader;
                var value = new List<IReadOnlyDictionary<DateTime, string>>();
                reader.ReadStartArray();
                while (reader.ReadBsonType() != 0)
                {
                    var dictionary = new Dictionary<DateTime, string>();
                    reader.ReadStartDocument();
                    while (reader.ReadBsonType() != 0)
                    {
                        var dictionaryKeyString = reader.ReadName();
                        var dictionaryKey = DateTime.Parse(dictionaryKeyString, null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
                        var dictionaryValue = reader.ReadString();
                        dictionary.Add(dictionaryKey, dictionaryValue);
                    }
                    reader.ReadEndDocument();
                    value.Add(dictionary);
                }
                reader.ReadEndArray();
 
                return value;
            }
 
            /// <inheritdoc />
            public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, IReadOnlyList<IReadOnlyDictionary<DateTime, string>> value)
            {
                var writer = context.Writer;
                writer.WriteStartArray();
                foreach (var item in value)
                {
                    writer.WriteStartDocument();
                    foreach (var keyValuePair in item)
                    {
                        writer.WriteName(keyValuePair.Key.ToUniversalTime().ToString("o"));
                        writer.WriteString(keyValuePair.Value);
                    }
                    writer.WriteEndDocument();
                }
                writer.WriteEndArray();
            }
        }
    }
}

Note: Currently C# driver prohibits writing dots "." into element names, so this particular model couldn't be saved into database because of DateTime serialization.

Please let me know if you have any further questions.

Comment by Suraj Gupta [ 25/Nov/19 ]

Sorry the formatting didn't persist when I pasted the code, but I can't figure out how to edit my original post.  I don't think I have permissions??  Could someone help?

Generated at Wed Feb 07 21:43:43 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.