[CSHARP-2627] MongoDB C# Driver serializing the discriminator twice when discriminator is explicitly declared in the class Created: 05/Jun/19  Updated: 22/Nov/20  Resolved: 25/Jun/20

Status: Closed
Project: C# Driver
Component/s: Serialization
Affects Version/s: 2.8.1
Fix Version/s: None

Type: Bug Priority: Major - P3
Reporter: Sylvain Boissé Assignee: Wan Bachtiar
Resolution: Done Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified
Environment:

.NET Core 2.0



 Description   

I have a base class with a property named Type meant to be used as a discriminator, but that can be used in the C# code to tell what we are dealing with. Say we are dealing with vehicles:
 
    public class abstract Vehicle
   

{         public VehicleType Type \{get; set;}

    }
 
    public enum VehicleType
   

{         SmallCar,         BigTruck,     }

 
This class is inherited by other classes:
 
    public class Car: Vehicle
   

{     }
 
    public class Truck: Vehicle
    {     }

 
I have a collection of Vehicle in the database that can contain documents of any subclass that inherits from it.
 
    var collection = mongoDatabase.GetCollection<Vehicle>("myVehicles");
 
So I register a discriminator convention to be able to serialize or deserialize the documents properly, using the Type property of Vehicle*:*
     BsonSerializer.RegisterDiscriminatorConvention(typeof(Vehicle), new ScalarDiscriminatorConvention(nameof(Vehicle.Type)));
 
    // Note: serialization convention being used for enums is string.
    BsonSerializer.RegisterDiscriminator(typeof(Car), VehicleType.Car.ToString());
    BsonSerializer.RegisterDiscriminator(typeof(Truck), VehicleType.Truck.ToString());
 
Now if later on I want to deserialize documents from the database into C#, it works great. I get documents instantiated to C# objects in the correct type and everything is populated correctly inside of them.
 
But if I insert something into the collection:
 
    var myTruck = new Truck { Type = VehicleType.BigTruck };
    mongoDatabase.GetCollection<Vehicle>.InsertOneAsync(myTruck);
 
What gets inserted the database is the following:
 
   

{         Type: 'BigTruck',         Type: 'Truck'     }

 
The Type field is put twice. I have been integrating the source code of MongoDB C# driver v2.8.1 to investigate why.
 
What I found is in the class BsonClassMapSerializer<TClass>, no check is made if the class already contains a property with the same name as the discriminator:
 
    if (ShouldSerializeDiscriminator(args.NominalType))
   

{         SerializeDiscriminator(context, args.NominalType, document);     }

 
    [...]
 
    private bool ShouldSerializeDiscriminator(Type nominalType)
   

{         return (nominalType != _classMap.ClassType || _classMap.DiscriminatorIsRequired || _classMap.HasRootClass) && !_classMap.IsAnonymous;     }

 
So it seems to be expected by the serializer that if a discriminator is used, that this discriminator is not to be explicitly declared in the nominal type.
 
Because of that expectation, it gets serialized a first time because the property is there, just like any other property. But then it gets added again by the serializer because it determines the object requires discrimination.
 
I have figured out a few workarounds in order to be able to explicitly declare the discriminator without having it being serialized twice.
 
For example, from what I see here:
 
    private void SerializeDiscriminator(BsonSerializationContext context, Type nominalType, object obj)
    {
        var discriminatorConvention = _classMap.GetDiscriminatorConvention();
        if (discriminatorConvention != null)
        {
            var actualType = obj.GetType();
            var discriminator = discriminatorConvention.GetDiscriminator(nominalType, actualType);
            if (discriminator != null)
           

{                 context.Writer.WriteName(discriminatorConvention.ElementName);                 BsonValueSerializer.Instance.Serialize(context, discriminator);             }

        }
    }
 
I see that if I implement my own custom discriminator convention and make its GetDiscriminator() call to return null when the nominal type is Vehicle*, it will skip serializing the discriminator.*
 
But it seems to me like the serialization code of the driver should check for this to avoid serializing the discriminator twice in the same document.



 Comments   
Comment by Daigo Kobayashi [ 22/Nov/20 ]

We are trying a similar scenario and facing a same issue.

We also have a base class which has enum type field and implemented a custom IDiscriminatorConverntion class.

public abstract class User
{
  public UserType UserType { get; set; }
  public string Name { get; set; }
}
 
public class AuthenticatedUser : User
{
  public string Email { get; set; }
}
 
public class GuestUser : User
{
  public string PhoneNumber { get; set; }
}
 
public enum UserType
{
  Authenticated = 0,
  Guest = 1
}
 
public class UserClassDiscriminator : IDiscriminatorConvention
{
  public Type GetActualType(IBsonReader bsonReader, Type nominalType)
  {
    var bookmark = bsonReader.GetBookmark();
    bsonReader.ReadStartDocument();
    if (bsonReader.FindElement(ElementName))
    {
      var value = bsonReader.ReadInt32();
      var userType = (UserType) value;
      var actualType = userType switch
      {
        UserType.Authenticated => typeof(AuthenticatedUser),
        UserType.Guest => typeof(GuestUser),
        _ => throw new InvalidOperationException($"Unknown user type is detected. A user type is {userType}")
      };
      bsonReader.ReturnToBookmark(bookmark);
      return actualType;
    }
    throw new InvalidOperationException($"Could not find an element name of {ElementName}");
  }
 
  public BsonValue GetDiscriminator(Type nominalType, Type actualType)
  {
    if (actualType == typeof(AuthenticatedUser))
    {
      return (int)UserType.Authenticated;
    }
    else if (actualType == typeof(GuestUser))
    {
      return (int)UserType.Guest;
    }
    throw new InvalidOperationException($"Could not find an appropriate type:{actualType}");
  }
 
  public string ElementName { get; } = "UserType";
}

This works fine for read queries. For example, the following query issue a query like this.

var filter = Builders<User>.Filter.OfType<GuestUser>(g => g.PhoneNumber == "0123456789");
collection.Find(filter);

{ "find" : "Users", "filter" : { "userType" : 1 , "phoneNumber" : "0123456789"} }

However if we try to create/update a document, it throws "Duplicate element name ..." exception. Other user has already posted an issue:CSHARP-2132. The comment suggests return null on GetDiscriminator. So we've implemented to return null on GetDescriminator then create/update queries work perfect but read query throws following exception.

System.NotSupportedException: OfType requires that documents of type GuestUser have a discriminator value.

We've also checked the C# driver source code and the case we've faced is not considered on current version of the driver(2.11.4).

Any thoughts?

Comment by Rachelle Palmer [ 25/Jun/20 ]

Hi there, thank you for reaching out to MongoDB. As this sounds more like a support issue, I wanted to give you some resources to get this question answered more quickly:

  • our MongoDB support portal, located at support.mongodb.com
  • our MongoDB community forums, located here
  • If you are an Atlas customer, there is free support offered 24/7 in the lower right hand corner of the UI.

Thank you!
Rachelle

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