-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CSHARP-5356: Support OfType
and is
with scalar discriminators when using class mapped serializers.
#1559
base: main
Are you sure you want to change the base?
Conversation
…n using class mapped serializers.
@@ -61,6 +62,9 @@ public class BsonClassMap | |||
private int _extraElementsMemberIndex = -1; | |||
private List<Type> _knownTypes = new List<Type>(); | |||
|
|||
private ConcurrentDictionary<BsonValue, Type> _discriminatorToTypeMap = new(); | |||
private ConcurrentDictionary<Type, BsonValue> _typeToDiscriminatorMap = new(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the new information we need to know to implement an IScalarDiscriminatorConvention
for BsonClassMapSerializer
.
{ | ||
AddKnownDiscriminator(_discriminator, _classType); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the moment we Freeze
a class map is the safest time to inform the base class maps of our existence.
@@ -1140,6 +1153,37 @@ public void SetExtraElementsMember(BsonMemberMap memberMap) | |||
_extraElementsMemberMap = memberMap; | |||
} | |||
|
|||
internal void AddKnownDiscriminator(BsonValue discriminator, Type type) | |||
{ | |||
if (!_classType.IsAssignableFrom(type)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sanity check. Shouldn't happen.
throw new ArgumentException($"Type {type} is not assignable to {_classType}.", nameof(type)); | ||
} | ||
|
||
if (_classType == typeof(object) || _classType == typeof(ValueType) || _classType.IsInterface) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somewhat arbitrary choice to not track discriminators for object
.
I'm concerned about conflicts with the ObjectSerializer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also not sure about interfaces. Maybe we do want to store known discriminators for interface class maps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding this point, this ticket here seems to be possibly related: https://jira.mongodb.org/browse/CSHARP-1907
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it does seem related.
@@ -1331,31 +1375,37 @@ internal IDiscriminatorConvention GetDiscriminatorConvention() | |||
|
|||
IDiscriminatorConvention LookupDiscriminatorConvention() | |||
{ | |||
var classMap = this; | |||
while (classMap != null) | |||
if (_discriminatorConvention != null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed implementation from a loop to recursive.
|
||
if (_baseClassMap.ClassType == typeof(object)) | ||
{ | ||
return new BsonClassMapScalarDiscriminatorConvention(this, "_t"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where we decide to use a BsonClassMapScalarDiscriminatorConvention
.
We decide this because we got here without seeing a root class, which would have resulted in choosing a StandardDiscriminatorConvention.Hierarchical
before getting here.
{ | ||
// in this case LookupDiscriminatorConvention below will find it | ||
break; | ||
return new BsonClassMapScalarDiscriminatorConvention(this, discriminatorConvention.ElementName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normally we'd just use the inherited discriminator convention but to make this all work we need a new convention at each level that will have its own _classMap
reference.
/// <exception cref="NotImplementedException"></exception> | ||
public BsonValue[] GetDiscriminatorsForTypeAndSubTypes(Type type) | ||
{ | ||
return _typeToDiscriminatorsForTypeAndSubTypesMap.GetOrAdd(type, MapTypeToDiscriminatorsForTypeAndSubTypes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could compute the list from scratch every time but it seems worth caching.
var context = BsonDeserializationContext.CreateRoot(bsonReader); | ||
return BsonValueSerializer.Instance.Deserialize(context); | ||
} | ||
bsonReader.ReturnToBookmark(bookmark); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary given it's also in finally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True!
Removed.
|
||
namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira | ||
{ | ||
public class CSharp5356Tests : Linq3IntegrationTest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth adding some .Where(x => x is Type) tests here too? and perhaps .Where(x => !(x is Type)) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
.Where(x => x is Animal); | ||
|
||
var stages = Translate(collection, queryable); | ||
AssertStages(stages, "{ $match : { } }"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried optimizing this stage away but that broke a lot of other tests.
Might be worth discussing and if we want to do it create a new ticket just for that.
stages, | ||
"{ $group : { _id : 1, _elements : { $push : '$$ROOT' } } }", | ||
"{ $project : { A : '$_elements', _id : 0 } }", | ||
"{ $project : { A : { $filter : { input : '$A', as : 'x', cond : true } }, _id : 0 } }"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential MQL optimization here since cond : true
.
Is it worth it?
} | ||
|
||
[Fact] | ||
public void Array_Where_is_Animal_should_work() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried adding tests for array OfType
but that is not currently supported.
I could implement support in this ticket or I could create a new ticket. @BorisDog?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have implemented support for OfType
in aggregation expressions.
stages, | ||
"{ $group : { _id : 1, _elements : { $push : '$$ROOT' } } }", | ||
"{ $project : { A : '$_elements', _id : 0 } }", | ||
"{ $project : { A : { $filter : { input : '$A', as : 'x', cond : { $not : true } } }, _id : 0 } }"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was surprised to see { $not : true }
instead of false
.
Is it worth simplifying?
stages, | ||
"{ $group : { _id : 1, _elements : { $push : '$$ROOT' } } }", | ||
"{ $project : { A : '$_elements', _id : 0 } }", | ||
"{ $project : { A : { $filter : { input : '$A', as : 'x', cond : { $not : { $in : ['$$x._t', ['Cat', 'Dog', 'Mammal']] } } } }, _id : 0 } }"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was expecting to see $nin
but turns out the server doesn't have $nin
in aggregation expressions (it does have it in filters).
stages, | ||
"{ $group : { _id : 1, _elements : { $push : '$$ROOT' } } }", | ||
"{ $project : { A : '$_elements', _id : 0 } }", | ||
"{ $project : { A : { $filter : { input : '$A', as : 'x', cond : { $not : { $eq : ['$$x._t', 'Cat'] } } } }, _id : 0 } }"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think $not/$eq
could be simplified to $ne
.
We'd have to convince ourselves that there are no edge cases where the results would differ.
/// <exception cref="NotImplementedException"></exception> | ||
public Type GetActualType(IBsonReader bsonReader, Type nominalType) | ||
{ | ||
BsonSerializer.EnsureKnownTypesAreRegistered(nominalType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added call to EnsureKnownTypesAreRegistered
,
return actualType; | ||
} | ||
|
||
if (nominalType == typeof(object) && discriminator.IsString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This if
is to play nice with the ObjectSerializer
.
{ | ||
// the BsonReader is sitting at the value whose actual type needs to be found | ||
var bsonType = bsonReader.GetCurrentBsonType(); | ||
if (bsonType == BsonType.Document) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a check that we're sitting at a document and not something else.
@@ -277,6 +280,28 @@ elemMatchOperation.Filter is AstFieldOperationFilter elemFilter && | |||
} | |||
} | |||
|
|||
public override AstNode VisitFilterExpression(AstFilterExpression node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added several new simplifications to the AstSimplifier
.
QueryableMethod.OfType | ||
}; | ||
|
||
public static AggregationExpression Translate(TranslationContext context, MethodCallExpression expression) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added support for OfType
in aggregation expressions.
@@ -40,7 +40,7 @@ private class C : A | |||
{ | |||
} | |||
|
|||
[BsonDiscriminator("D~", RootClass = true)] | |||
[BsonDiscriminator("D~")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RootClass = true
in the middle of the class hierarchy makes no sense.
This must have been an oversight.
@@ -34,7 +34,6 @@ private class B : A | |||
{ | |||
} | |||
|
|||
[BsonDiscriminator(RootClass = true)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RootClass = true
in the middle of the class hierarchy makes no sense.
This must have been an oversight.
@@ -47,7 +47,7 @@ public void Contains_with_string_constant_and_char_constant_should_work() | |||
|
|||
var stages = Translate(collection, queryable); | |||
|
|||
AssertStages(stages, "{ $match : { } }"); | |||
AssertStages(stages); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a result of a new pipeline simplification any { $match : { } }
stages are removed as they do nothing (they let all stages through).
@@ -301,7 +301,7 @@ public void Contains_with_string_field_and_string_constant_and_comparisonType_sh | |||
#if !NETFRAMEWORK | |||
[Theory] | |||
[InlineData(StringComparison.CurrentCulture, "{ $match : { _id : { $type : -1 } } }")] | |||
[InlineData(StringComparison.CurrentCultureIgnoreCase, "{ $match : { } }")] | |||
[InlineData(StringComparison.CurrentCultureIgnoreCase, null)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A value of null
for expectedStage
means "no stage expected".
results.Select(x => x.Id).Should().Equal(3); | ||
} | ||
|
||
[Fact] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a whole bunch of new tests, some of which required implementing support for missing features.
@@ -329,7 +329,7 @@ public void TestSerializeGAsObject() | |||
{ | |||
G g = new G { P = "x" }; | |||
var json = g.ToJson<object>(); | |||
var expected = ("{ '_t' : ['D~', 'G~'], 'P' : 'x' }").Replace("'", "\""); | |||
var expected = ("{ '_t' : 'G~', 'P' : 'x' }").Replace("'", "\""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose this means that this will be a breaking change, won't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. We're only changing how the test classes are configured here.
Any timeframe for when this is likely to be released? Direly needed in 3.x since this used to work in 2.x. |
We plan to release the implementation of this new feature in 3.2. It is currently in code review and didn't quite make it into the 3.1 release.
Actually, it didn't work in 2.x. At least not reliably. You can read a full explanation of why here: What did work in 2.x was OfType/is queries when using What did not work in 2.x was OfType/is queries when using |
@rstam Actually, it doesn't seem to: using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using var client = new MongoClient();
var db = client.GetDatabase("test");
var events = db.GetCollection<IEvent>("events");
var query = events.AsQueryable().OfType<Foo>();
Console.WriteLine(query.ToString());
var objectSerializer = new ObjectSerializer(type => true);
BsonSerializer.RegisterSerializer(objectSerializer);
var convention = new HierarchicalDiscriminatorConvention("_t");
BsonSerializer.RegisterDiscriminatorConvention(typeof(Foo), convention);
BsonClassMap.RegisterClassMap<Foo>(doc =>
{
doc.AutoMap();
doc.SetDiscriminatorConvention(convention);
doc.SetIsRootClass(true);
});
BsonSerializer.RegisterDiscriminatorConvention(typeof(Bar), convention);
BsonClassMap.RegisterClassMap<Bar>(doc =>
{
doc.AutoMap();
doc.SetDiscriminatorConvention(convention);
doc.SetIsRootClass(true);
});
public interface IEvent;
public record Foo(int X) : IEvent;
public record Bar(int Y) : IEvent; <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>mongodb_driver_test</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="3.1.0" />
</ItemGroup>
</Project> This throws:
|
@aradalvand I think you are running into a completely different type of problem in your latest example, which is that the document type of your collection is an For one, it doesn't matter what discriminator convention you register at a lower level (such as Also, I don't know what it means, if anything, to have TWO roots, as you have configured both While your example is interesting and we would like to find a way to make it work, I don't think the work we are doing in this ticket will address your latest example because the work in this ticket is about the scenario where the collection document type is a class (and not an interface). |
@aradalvand I can get your latest example to pass with the latest version of the driver if I also add:
to register the hierarchical discriminator convention for the interface also. I'm still not entirely sure whether there will be any issues stemming from having TWO root classes, since we have always assumed there would be a single root class. Nevertheless, your example works if I add that discriminator convention registration for the interface. |
@rstam Ah, thanks. You're right. I wish this stuff was better documented though, really hard to find out this type of thing intuitively. Would you want me to create a separate issue for the interface thing? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall it makes sense for me, just added some comments. And of course there are some missing xml docs.
Also, it seems that we use the current ScalarDicriminatorConvention
as the discriminator convention of EnumerableSerializerBase
. Do we need to worry about the new discriminator convention there too?
} | ||
} | ||
|
||
return discriminators.OrderBy(x => x).ToArray(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we order the discriminators?
/// <summary> | ||
/// | ||
/// </summary> | ||
public class BsonClassMapScalarDiscriminatorConvention : IScalarDiscriminatorConvention |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be deriving from StandardDiscriminatorConvention
?
return discriminators.OrderBy(x => x).ToArray(); | ||
} | ||
|
||
private BsonValue ReadDiscriminator(IBsonReader bsonReader) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we make this class derive from StandardDiscriminatorConvention
, this is something that could be put on the base class, given that this is almost the same that is done inside StandardDiscriminatorConvention.GetActualType
.
return null; | ||
} | ||
|
||
if (_classMap.TypeToDiscriminatorMap.TryGetValue(actualType, out BsonValue discriminator)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to consider preserving the ScalarDiscriminatorConvention
implementation here?
var classMap = BsonClassMap.LookupClassMap(actualType);
return classMap.Discriminator;
No description provided.