[CSHARP-1907] Type discriminators don't work when collection document type is an interface Created: 02/Feb/17  Updated: 22/Jan/24

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

Type: Bug Priority: Major - P3
Reporter: Peter Garafano (Inactive) Assignee: Robert Stam
Resolution: Unresolved Votes: 1
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified


 Description   

When the T of a collection is an interface, the type discriminators do not get added properly to a query. One of two things happens depending on how you query them

  1. The type discriminator isn't added at all
  2. The type discriminator added is incorrect.

The _t for each document in the database is correctly serialized, this appears to only be an issue for reading.

Reproduction script:

Reproduction

async Task Main()
{
	var client = new MongoClient();
	var db = client.GetDatabase("polymorphismTest");
	var col = db.GetCollection<IFoo>("iFoo");
	
	// Clean up any existing documents
	await col.DeleteManyAsync(Builders<IFoo>.Filter.Empty);
	
	// Insert 2 new test documents of different types, each implementing IFoo
	await col.InsertOneAsync(new Widget1() { Id = ObjectId.GenerateNewId(), SomeField = "ABC", Bar = 10 });
	await col.InsertOneAsync(new Widget2() { Id = ObjectId.GenerateNewId(), SomeField = "ABC", Bar = 10M });
	
	// Build a query using .OfType<> on the collection.
	var widget1QueryColOfType = col.OfType<Widget1>().Find(Builders<Widget1>.Filter.Eq(x => x.SomeField, "ABC"));
	var widget2QueryColOfType = col.OfType<Widget2>().Find(Builders<Widget2>.Filter.Eq(x => x.SomeField, "ABC"));
	Console.WriteLine("OfType<> on the Collection");
	Console.WriteLine(widget1QueryColOfType.ToString());
	Console.WriteLine(widget2QueryColOfType.ToString());
	Console.WriteLine();
	
	// Build a query using .OfType<> on the Builder to change from IFoo to the concrete type.
	var widget1QueryBuilderOfType = col.Find(Builders<IFoo>.Filter.OfType<Widget1>(Builders<Widget1>.Filter.Eq(x => x.SomeField, "ABC")));
	var widget2QueryBuilderOfType = col.Find(Builders<IFoo>.Filter.OfType<Widget2>(Builders<Widget2>.Filter.Eq(x => x.SomeField, "ABC")));
	Console.WriteLine("OfType<> on the Builder");
	Console.WriteLine(widget1QueryBuilderOfType.ToString());
	Console.WriteLine(widget2QueryBuilderOfType.ToString());
	Console.WriteLine();
	
	Console.WriteLine("Documents in the database");
	var rawDocsInDatabase = await db.GetCollection<BsonDocument>("iFoo").Find(Builders<BsonDocument>.Filter.Empty).ToListAsync();
	foreach(var doc in rawDocsInDatabase)
		Console.WriteLine(doc.ToJson());
}
 
public interface IFoo
{
	ObjectId Id { get; set;}
	string SomeField { get; set;}
}
 
public class Widget1 : IFoo
{
	public ObjectId Id {get;set;}
	public string SomeField { get; set; }
	public int Bar { get; set; }
}
 
public class Widget2 : IFoo
{
	public ObjectId Id {get;set;}
	public string SomeField { get; set; }
	public Decimal Bar { get; set; } 
}

Output

OfType<> on the Collection
find({ "SomeField" : "ABC" })
find({ "SomeField" : "ABC" })
 
OfType<> on the Builder
find({ "_t" : "UserQuery+Widget1, LINQPadQuery", "SomeField" : "ABC" })
find({ "_t" : "UserQuery+Widget2, LINQPadQuery", "SomeField" : "ABC" })
 
Documents in the database
{ "_id" : ObjectId("589347de303cd5d0ca4540e4"), "_t" : "Widget1", "SomeField" : "ABC", "Bar" : 10 }
{ "_id" : ObjectId("589347de303cd5d0ca4540e6"), "_t" : "Widget2", "SomeField" : "ABC", "Bar" : "10" }



 Comments   
Comment by James Kovacs [ 11/Mar/21 ]

Investigating further, the root cause of the problem is actually more subtle. IFoo uses an ObjectDiscriminatorConvention because BsonSerializer.LookupDiscriminatorConvention line 390 forces the use of this convention for interfaces. However this method is called for Widget1, we fall through the else on line 396, which uses the HierarchicalDiscriminatorConvention by default.

Thus if you access the collection via the interface, the discriminator is looked up using the ObjectDiscriminatorConvention - which uses _t: "ClassName, Assembly" - whereas if you access it through the class, the discriminator is looked up using the HierarchicalDiscriminatorConvention - which uses a discriminator of _t: "ClassName".

The root cause of the problem is that we use two different conventions - which use different default discriminator formats - to find the discriminator. This inconsistency is what leads to the observed issue.

We are discussing how best to resolve this issue and will update this ticket once we have a plan forward.

Comment by James Kovacs [ 05/Mar/21 ]

I did some quick investigation. FilterDefinitionBuilder.OfType<T> is calling ObjectDiscriminatorConvention.GetDiscriminator(Type nominalType, Type actualType) which delegates to TypeNameDiscriminator.GetDiscriminator(actualType). It is this latter method that is appending the assembly name of the type if it is available.

https://github.com/mongodb/mongo-csharp-driver/blob/d74848d44e045e95b4f46e48bd05ef13b49b3001/src/MongoDB.Bson/Serialization/TypeNameDiscriminator.cs#L154

TypeNameDiscriminator.GetDiscriminator(actualType) is only called from two places:

1. ObjectDiscriminatorConvention.GetDiscriminator(Type nominalType, Type actualType)
2. Recursively on itself for generic types

I do not believe that we append assembly names to type discriminators elsewhere. If that is true, we should be able to fix this bug by removing the assembly name lookup code from that method.

Comment by James Turner [ 30/Mar/18 ]

Is there any movement on this issue? I'm encountering the second issue listed here with `_t` arguments appearing the full type name + assembly where the records in the DB are saved with just the type name.

Happy to help with potential dev work required, just need some direction of where the issue likely is. I'm looking at the `OfTypeBinder` (both the "Pipeline" and "EmbeddedPipeline") but not sure if the issue is this high up or more comes down to BSON serialization of types?

Is it that really this `OfType` checks should be using the current discriminator?

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