[CSHARP-3449] custom serializers sometimes not used in aggregation Created: 04/Mar/21  Updated: 28/Oct/23  Resolved: 17/Feb/22

Status: Closed
Project: C# Driver
Component/s: Linq, LINQ3
Affects Version/s: 2.11.3
Fix Version/s: 2.14.0

Type: Bug Priority: Major - P3
Reporter: Felix König Assignee: Dmitry Lukyanov (Inactive)
Resolution: Fixed Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Attachments: Text File MongoEnumTest-1.cs    
Issue Links:
Related
is related to CSHARP-4066 Only use regex filters against string... Closed
Epic Link: CSHARP-3615

 Description   

I am encountering a problem where custom serializers I have previously registered are not being used to convert types I use in an aggregation pipeline.

Assume I have these types:

 

public enum TestEnum { Foo, Bar, Baz }
public record TestEntity(string Id, string Category, TestEnum EnumValue);

where TestEnum will be my custom type. I am registering the type as follows:

 

BsonClassMap.RegisterClassMap<TestEntity>(cm =>
{
    cm.MapIdProperty(e => e.Id);
    cm.MapProperty(e => e.Category)
        .SetElementName("category");
    cm.MapProperty(e => e.EnumValue)
        .SetElementName("enum_value")
        .SetSerializer(new TestEnumSerializer());
});

TestEnumSerializer is a SerializerBase<TestEnum> that represents the enum values as the strings "my_foo", "my_bar" and "my_baz".

I am inserting some entities as follows:

 

IMongoCollection<TestEntity> collection = db.GetCollection<TestEntity>("test_collection");
collection.InsertOne(new TestEntity("1", "cat1", TestEnum.Foo));
collection.InsertOne(new TestEntity("2", "cat1", TestEnum.Bar));
collection.InsertOne(new TestEntity("3", "cat2", TestEnum.Bar));

and I can confirm that the entities get persisted as I expected:

 

 

/* 1 */
{
    "_id" : "1",
    "category" : "cat1",
    "enum_value" : "my_foo"
}/* 2 */
{
    "_id" : "2",
    "category" : "cat1",
    "enum_value" : "my_bar"
}/* 3 */
{
    "_id" : "3",
    "category" : "cat2",
    "enum_value" : "my_bar"
}

If I now perform the following aggregation:

 

 

var bars = collection.Aggregate()
    .Group(
        e => e.Category,
        group => new { NumBars = group.Count(e2 => e2.EnumValue == TestEnum.Bar) })
    .ToList();

I can see that the produced query looks as follows:

 

 

"pipeline": [
  {
    "$group": {
      "_id": "$category",
      "NumBars": {
        "$sum": {
          "$cond": [
            {
              "$eq": [
                "$enum_value",
                1
              ]
            },
            1,
            0
          ]
        }
      }
    }
  }
]

Note that it compares the $enum_value to 1 instead of "my_bar", which makes the match fail and return a wrong result.

 

The 1 is the enum's ordinal value, which probably gets used because of the implicit conversion added in https://jira.mongodb.org/browse/CSHARP-131

Since it appears to be using BsonType mappers instead of registered serializers, it is possible to add custom type mappers for types that do not have an implicit conversion using BsonTypeMapper.RegisterCustomTypeMapper. However, since the enum gets implicitly converted to an ordinal value before any custom type mappers are checked, I was unable to find a workaround.

I have added the full source code of the test class I used as an attachment.



 Comments   
Comment by Felix König [ 19/Feb/22 ]

Thank you! I tried using the new LINQ3 driver and have opened a follow-up issue CSHARP-4066

Comment by James Kovacs [ 17/Feb/22 ]

This issue has been fixed in the new LINQ provider (known as LINQ3), which is included in the 2.14 release.

Configure your MongoClientSettings to use LinqProvider.V3 if you want to use this functionality.

To configure a client to use the LINQ3 provider use code like the following

var connectionString = "mongodb://localhost";
var clientSettings = MongoClientSettings.FromConnectionString(connectionString);
clientSettings.LinqProvider = LinqProvider.V3;
var client = new MongoClient(clientSettings);

The following is a self-contained repro:

using System;
using System.Linq;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
 
BsonClassMap.RegisterClassMap<TestEntity>(cm =>
{
    cm.MapIdProperty(e => e.Id);
    cm.MapProperty(e => e.Category)
        .SetElementName("category");
    cm.MapProperty(e => e.EnumValue)
        .SetElementName("enum_value")
        .SetSerializer(new TestEnumSerializer());
});
 
var settings = new MongoClientSettings { LinqProvider = LinqProvider.V3 };
var client = new MongoClient(settings);
var db = client.GetDatabase("test");
var coll = db.GetCollection<TestEntity>("csharp3449");
 
var query = coll.Aggregate()
    .Group(
        e => e.Category,
        group => new { NumBars = group.Count(e2 => e2.EnumValue == TestEnum.Bar) });
Console.WriteLine(query);
 
public enum TestEnum { Foo, Bar, Baz }
public record TestEntity(string Id, string Category, TestEnum EnumValue);
 
public class TestEnumSerializer : SerializerBase<TestEnum>
{
    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, TestEnum value)
    {
        context.Writer.WriteString($"my_{value.ToString().ToLower()}");
    }
 
    public override TestEnum Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        // TODO: implement deserialization
        return base.Deserialize(context, args);
    }
}

This produces the output using the configured TestEnumSerializer. Note that due to CSHARP-4061, you won't be able to render the query to MQL without a small modification to the driver source code. We will fix this issue in a future release.

aggregate([{ "$group" : { "_id" : "$category", "_elements" : { "$push" : "$$ROOT" } } }, { "$project" : { "NumBars" : { "$size" : { "$filter" : { "input" : "$_elements", "as" : "e2", "cond" : { "$eq" : ["$$e2.enum_value", "my_bar"] } } } }, "_id" : 0 } }])

Comment by Felix König [ 09/Mar/21 ]

Thank you very much. That workaround, in addition to providing custom BSON mappers using BsonTypeMapper.RegisterCustomTypeMapper for some other types I use, is working nicely.

Comment by Dmitry Lukyanov (Inactive) [ 08/Mar/21 ]

Hello de.felix.koenig@gmail.com , thanks for your report.

Currently, we're actively working on the new LINQ implementation and will address this case there. As a workaround, currently, you can use this code with manual mapping "TestEnum.Bar" to the expected mapped value:

            var bars = collection.Aggregate()
                .Group(
                    e => e.Category,
                    group => new { NumBars = group.Count(e2 => e2.EnumValue.Equals("my_bar")) })
                .ToList();

 

Comment by Felix König [ 04/Mar/21 ]

Note that registering the serializer globally like this instead of just for the entity's property causes no change in behaviour:

BsonClassMap.RegisterClassMap<TestEntity>(cm =>
{
    cm.MapIdProperty(e => e.Id);
    cm.MapProperty(e => e.Category)
        .SetElementName("category");
    cm.MapProperty(e => e.EnumValue)
        .SetElementName("enum_value");
});
BsonSerializer.RegisterSerializer(new TestEnumSerializer());

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