Skip to content

Instantly share code, notes, and snippets.

@JamisonWhite
Last active February 6, 2023 13:07
Show Gist options
  • Save JamisonWhite/cc0eae9e0ebfe8e62ec3 to your computer and use it in GitHub Desktop.
Save JamisonWhite/cc0eae9e0ebfe8e62ec3 to your computer and use it in GitHub Desktop.
C# MongoDb custom field and value discriminator
/// <summary>
/// Map a resource "keyType" to polymorphic classes.
/// </summary>
/// <remarks>
/// Followed example here:
/// http://pastebin.com/9UweEKBe
/// </remarks>
public class CustomFieldDiscriminatorConvention : IDiscriminatorConvention
{
private readonly string elementName;
private readonly Type defaultType;
private readonly Dictionary<string, Type> typeMap;
public CustomFieldDiscriminatorConvention(string elementName, Type defaultType, Dictionary<string, Type> typeMap)
{
this.elementName = elementName;
this.defaultType = defaultType;
this.typeMap = typeMap;
}
/// <summary>
/// Element Name
/// </summary>
public string ElementName
{
get { return elementName; }
}
/// <summary>
/// Return the type that matches the string
/// </summary>
/// <param name="bsonReader"></param>
/// <param name="nominalType"></param>
/// <returns></returns>
public Type GetActualType(MongoDB.Bson.IO.BsonReader bsonReader, Type nominalType)
{
Type type = defaultType;
var bookmark = bsonReader.GetBookmark();
bsonReader.ReadStartDocument();
if (bsonReader.FindElement(ElementName))
{
var value = bsonReader.ReadString();
if (typeMap.ContainsKey(value.ToLower()))
type = typeMap[value];
}
bsonReader.ReturnToBookmark(bookmark);
return type;
}
public BsonValue GetDiscriminator(Type nominalType, Type actualType)
{
return actualType.Name;
}
}
var dis = new CustomFieldDiscriminatorConvention(
"keyType",
typeof(Resource),
new Dictionary<string, Type>(3)
{
{"domain", typeof (DomainResource)},
{"ip", typeof (IpResource)},
{"uri", typeof (UriResource)}
}
);
BsonSerializer.RegisterDiscriminatorConvention(typeof(Resource), dis);
@y-lobau
Copy link

y-lobau commented Dec 1, 2019

Thanks for this! I applied the same code in my solution, but then when writing integration tests i faced with interesting problem:

If i write a document to DB and then read it in the same db context - this code doesn't work as expected. After a day struggling with this and debugging driver sources i found out that the following happens:

  1. When serializing, GetDiscriminator() method is called at first place. Engine writes to BsonWriter "keyType" = actualType.Name, which in fact you don't need, containing wrong discriminator value.
  2. Then, when serializing real object's properties, BsonWriter gets new line "keyType" == "domain", let's say.
  3. Then when it writes to DB - 2nd value of keyType "wins", that's why in DB all is fine - you have just keyType==domain.

BUT, when you read this data in same context - for some reason BsonReader still contains both lines for "keyType" - i can clearly see this. Which obviousely lead to unexpected result in here:

var value = bsonReader.ReadString();
if (typeMap.ContainsKey(value.ToLower()))
type = typeMap[value];

Just because at 1st place BsonReader finds a line "keyType" == actualType.Name.
I've fixed it with the following change:

public BsonValue GetDiscriminator(Type nominalType, Type actualType)
{
    return null;
}

In this case discriminator is not serialized. I hope this will help someone.

@razzeee
Copy link

razzeee commented Aug 18, 2020

Midway through implementing this, I found out that you can just do BsonSerializer.RegisterDiscriminatorConvention(typeof(YourType), new ScalarDiscriminatorConvention("NameOfYourField"));

Which seems to work fine for me

@benprime
Copy link

@razzeee That makes sense, but I couldn't find a way to provide a default value for documents saved to the database before the discriminators were implemented using ScalarDiscriminatorConvention.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment