1

We are using MongoDb to store product attributes and have created a custom array serializer to take a dictionary and serialize it in a custom format. The format takes the key and value of each dictionary item and stores it as a single array item separated with a colon.

Example product class

public class Product
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; set; }

    public string Name { get; set; }

    [BsonSerializer(typeof(ProductAttributeSerializer))]
    public Dictionary<string, string> Attributes { get; set; } = new Dictionary<string, string>();
}

Example data with custom array format

{ 
    "_id" : ObjectId("56d94ee01992d148b4c10d59"), 
    "Name" : "My Product", 
    "Attributes" : [
        "Colour:Blue", 
        "Gender:Mens"
    ]
}

Here is our serializer

[BsonSerializer(typeof(ProductAttributeSerializer))]
    public class ProductAttributeSerializer : IBsonSerializer, IBsonArraySerializer
    {
        public Type ValueType { get { return typeof(Dictionary<string, string>); } }

        public object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
        {
            var type = context.Reader.GetCurrentBsonType();
            var attributes = new Dictionary<string, string>();

            switch (type)
            {
                case BsonType.Array:

                    context.Reader.ReadStartArray();

                    while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
                    {
                        string[] splitValues = context.Reader.ReadString().Split(':');
                        attributes.Add(splitValues[0], splitValues[1]);
                    }

                    context.Reader.ReadEndArray();

                    return attributes;

                default:
                    throw new NotImplementedException($"No implementation to deserialize {type}");
            }
        }

        public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value)
        {
            var attributes = value as Dictionary<string, string>;

            if (attributes != null)
            {
                context.Writer.WriteStartArray();

                foreach (KeyValuePair<string, string> attr in attributes)
                {
                    context.Writer.WriteString($"{attr.Key}:{attr.Value}");
                }

                context.Writer.WriteEndArray();
            }
        }

        public bool TryGetItemSerializationInfo(out BsonSerializationInfo serializationInfo)
        {
            string elementName = null;
            var serializer = BsonSerializer.LookupSerializer(typeof(string));
            var nominalType = typeof(string);
            serializationInfo = new BsonSerializationInfo(elementName, serializer, nominalType);
            return true;
        }
    }

All of the serialization of items works, however when trying to query the data through the driver and trying to do something equivalent to this db.Product.find({ Attributes : { $in : [ "Colour:Blue" ] } }) I am getting an error.

The original error indicated that the serializer needed to be an IBsonArraySerializer, now that is the case I need to implement the TryGetItemSerializationInfo method.

Based on the example above, what should this method do and set as its output?

I have tried looking up a string serializer and using that but it gives an error saying "Unable to cast object of type 'System.Char' to type 'System.String'". Trying a char serializer does not give an error but does not return any results. Looking in the logs for MongoDb seems to indicate the search was converted character by character to integers and then searched upon.

The code for the filter statement is also below

Database.GetCollection<Product>("Product").Find(Builders<Product>.Filter.AnyIn("Attributes", "Colour:Blue")).ToListAsync()
rrrr-o
  • 2,447
  • 2
  • 24
  • 52

1 Answers1

2

First off, great job on the serializer. I didn't need to change any code in there for this to work.

I found the problem. When you use the AnyIn method, the parameter you are passing "Colour:Blue" to wants an enumerable. Since .NET strings implement IEnumerable, that's why you saw a weird query translation. Changing it to new [] { "Colour:Blue" } will fix your problem. An alternative, for when you are only searching for a single key value pair, you can use AnyEq instead.

Finally, you can remove the BsonSerializer attribute from the ProductAttributeSerializer class. This attributes use is for indicating which serializer to use for a class or property, not for identifying serializers themselves.

Craig Wilson
  • 12,174
  • 3
  • 41
  • 45