Search code examples
mongodbmongodb-.net-driver

Using Mongo Bson Serialization, how to create a proper deserializer with polymorphic data schema with not-standard field for discriminator


Created a dotnetfiddle here: https://dotnetfiddle.net/dUbJss

Given data that needs to be deserialized that is already written to hundreds of mongo databases:

[
  {
    "_id": ObjectId(aa),
    "shape": {
      "_bsonName": "Company::Mapping::Geofences::GeoRectangle",
      "geoJson": ...,
      "bottom": "1", "left": 2, "right": 3, "top": 4
    }
  },
  {
    "_id": ObjectId(ab),
    "shape": {
      "_bsonName": "Company::Mapping::Geofences::GeoPolygon",
      "points": [[-79,36],[-80,37],[-79,38]]
  }
]

[DiscriminatorValue] can be one of:

  • "Company::Mapping::Geofences::GeoEllipse" -> c# type "EllipseGeoShape"
  • "Company::Mapping::Geofences::GeoRectangle" -> c# type "RectangleGeoShape"
  • "Company::Mapping::Geofences::GeoPolygon" -> c# type "PolygonGeoShape"
public class Geofence
{
   public ObjectId Id { get;set; }
   [BsonElement("shape")]
   public GeofenceShape Shape { get; set; }
}
public class GeofenceShape
{
  [BsonElement("_bsonName")]
  public string BsonName { get; set; }
}
public class RectangleGeoShape : GeofenceShape
{
  public decimal Bottom { get; set; }
  public decimal Left { get; set; }
  public decimal Right { get; set; }
  public decimal Top { get; set; }
}
public class PolygonGeoShape : GeofenceShape
{
  public decimal[][] Points { get; set; }
} 

Basically the _t field that mongo normally uses for discriminator needs to be "_bsonName" but I also have to read that _bsonName field and return our C# types: RectangleShape|EllipseShape|PolygonShape

So I created an IDiscriminatorConvention class

public class GeofenceShapeDiscriminatorConvention : IDiscriminatorConvention
{
    public string ElementName => "_bsonName";

    public Type GetActualType(IBsonReader bsonReader, Type nominalType)
    {
        var bookmark = bsonReader.GetBookmark();

        string bsonName = null;
        bsonReader.ReadStartDocument();
        if(bsonReader.FindElement("_bsonName"))
        {
            bsonName = bsonReader.ReadString();
        }

        bsonReader.ReturnToBookmark(bookmark);

        switch (bsonName)
        {
            case "Company::Mapping::Geofences::GeoEllipse":
                return typeof(EllipseGeofenceShape);
            case "Company::Mapping::Geofences::GeoRectangle":
                return typeof(RectangleGeofenceShape);
            case "Company::Mapping::Geofences::GeoPolygon":
                return typeof(PolygonGeofenceShape);
            default:
                throw new NotSupportedException($"Unexpected shape._bsonName of {bsonName}.");
        }
    }

    public BsonValue GetDiscriminator(Type nominalType, Type actualType)
    {
        switch(actualType)
        {
            case Type _ when actualType == typeof(EllipseGeofenceShape):
                return "Company::Mapping::Geofences::GeoEllipse";
            case Type _ when actualType == typeof(RectangleGeofenceShape):
                return "Company::Mapping::Geofences::GeoRectangle";
            case Type _ when actualType == typeof(PolygonGeofenceShape):
                return "Company::Mapping::Geofences::GeoPolygon";
            default:
                throw new ApplicationException($"Unexpected type '{actualType.FullName}' when serializing.");
        }
    }
}

unfortunately, this code fails with a StackOverflowException, where the Mongo code keeps calling GetActualType over and over and over again.

Looked through their source code, and theres not a lot of proper examples.

Of note, if I skip the bsonReader.ReturnToBookmark(bookmark); then I get a different error, basically failed to GetBsonType() on a node of type value (i forget the exact wording)


Solution

  • Instead of writing a discriminator convention from scratch, you could derive one from StandardDiscriminatorConvention and use the BsonDiscriminator attributes, e.g.:

    internal class GeofenceDiscriminatorConvention : StandardDiscriminatorConvention
    {
        public GeofenceDiscriminatorConvention() : base("_bsonName")
        {
        }
    
        public static void Register()
        {
            BsonSerializer.RegisterDiscriminatorConvention(typeof(GeofenceShape), new GeofenceDiscriminatorConvention());
        }
    
        public override BsonValue GetDiscriminator(Type nominalType, Type actualType)
        {
            switch (actualType)
            {
                case Type _ when actualType == typeof(EllipseGeofenceShape):
                    return "Company::Mapping::Geofences::GeoEllipse";
                case Type _ when actualType == typeof(RectangleGeofenceShape):
                    return "Company::Mapping::Geofences::GeoRectangle";
                case Type _ when actualType == typeof(PolygonGeofenceShape):
                    return "Company::Mapping::Geofences::GeoPolygon";
                default:
                    throw new ApplicationException($"Unexpected type '{actualType.FullName}' when serializing.");
            }
        }
    }
    

    On the GeofenceShape classes, you set the corresponding discriminator values like this:

    // Base class
    [BsonKnownTypes(typeof(EllipseGeofenceShape), typeof(PolygonGeofenceShape), typeof(RectangleGeofenceShape))]
    internal class GeofenceShape
    {
        [BsonRepresentation(MongoDB.Bson.BsonType.ObjectId)]
        public string Id { get; set; }
    }
    
    // Derived class
    [BsonDiscriminator("Company::Mapping::Geofences::GeoPolygon")]
    internal class PolygonGeofenceShape : GeofenceShape
    {
    }