Skip to content
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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

rstam
Copy link
Contributor

@rstam rstam commented Nov 27, 2024

No description provided.

@rstam rstam requested a review from a team as a code owner November 27, 2024 20:31
@@ -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();
Copy link
Contributor Author

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);
}

Copy link
Contributor Author

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))
Copy link
Contributor Author

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)
Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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

Copy link
Contributor Author

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)
Copy link
Contributor Author

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");
Copy link
Contributor Author

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);
Copy link
Contributor Author

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);
Copy link
Contributor Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure the list won't grow/be further discovered after being cached?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are sure because the class map is "frozen".

var context = BsonDeserializationContext.CreateRoot(bsonReader);
return BsonValueSerializer.Instance.Deserialize(context);
}
bsonReader.ReturnToBookmark(bookmark);
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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)) ?

Copy link
Contributor Author

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 : { } }");
Copy link
Contributor Author

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 } }");
Copy link
Contributor Author

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?

stages,
"{ $group : { _id : 1, _elements : { $push : '$$ROOT' } } }",
"{ $project : { A : '$_elements', _id : 0 } }",
"{ $project : { A : { $filter : { input : '$A', as : 'x', cond : { $not : true } } }, _id : 0 } }");
Copy link
Contributor Author

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 } }");
Copy link
Contributor Author

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 } }");
Copy link
Contributor Author

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.

@rstam rstam requested a review from damieng November 28, 2024 18:38
/// <exception cref="NotImplementedException"></exception>
public Type GetActualType(IBsonReader bsonReader, Type nominalType)
{
BsonSerializer.EnsureKnownTypesAreRegistered(nominalType);
Copy link
Contributor Author

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)
Copy link
Contributor Author

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)
Copy link
Contributor Author

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)
Copy link
Contributor Author

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)
Copy link
Contributor Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this support of OfType against the root object's type or do we support discriminators at every object in the document?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This particular translation code applies only to OfType nested inside a projection.

Top level OfType against the root are handled in OfTypeMethodToPipelineTranslator and result in different MQL involving a $match stage instead.

@@ -40,7 +40,7 @@ private class C : A
{
}

[BsonDiscriminator("D~", RootClass = true)]
[BsonDiscriminator("D~")]
Copy link
Contributor Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to detect that and throw in case some customers moving from 2.x incorrectly configured their attributes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how easy it is to detect.

We certainly can't detect it when configuring the class map, because we don't know what the eventual nominal type will be. So perhaps we could detect it later but I'm not sure it's worth it.

@@ -34,7 +34,6 @@ private class B : A
{
}

[BsonDiscriminator(RootClass = true)]
Copy link
Contributor Author

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);
Copy link
Contributor Author

@rstam rstam Nov 29, 2024

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 documents 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)]
Copy link
Contributor Author

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]
Copy link
Contributor Author

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("'", "\"");
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@rstam rstam requested a review from papafe December 2, 2024 18:49
@aradalvand
Copy link

Any timeframe for when this is likely to be released? Direly needed in 3.x since this used to work in 2.x.

@rstam
Copy link
Contributor Author

rstam commented Dec 10, 2024

Any timeframe for when this is likely to be released?

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.

Direly needed in 3.x since this used to work in 2.x.

Actually, it didn't work in 2.x. At least not reliably. You can read a full explanation of why here:

https://jira.mongodb.org/browse/CSHARP-5356?focusedCommentId=6914772&focusedId=6914772&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-6914772

What did work in 2.x was OfType/is queries when using HierarchicalDiscriminatorConvention. This has not changed and continues to work.

What did not work in 2.x was OfType/is queries when using ScalarDiscriminatorConvention. Depending on the scenario it might or might not work. In 3.x we made the decision that throwing an exception was preferable to silently returning incorrect results.

@aradalvand
Copy link

This has not changed and continues to work.

@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:

Expression not supported: test.events.Aggregate([]).OfType() because OfType is not supported with the configured discriminator convention.

@rstam
Copy link
Contributor Author

rstam commented Dec 13, 2024

@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 interface and not a class, and I think we have some issues with that.

For one, it doesn't matter what discriminator convention you register at a lower level (such as Foo or Bar), when OfType is translated it uses the discriminator convention it finds for IEvent, which won't be a HierarchicalDiscriminatorConvention.

Also, I don't know what it means, if anything, to have TWO roots, as you have configured both Foo and Bar to be roots.

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).

@rstam
Copy link
Contributor Author

rstam commented Dec 13, 2024

@aradalvand I can get your latest example to pass with the latest version of the driver if I also add:

BsonSerializer.RegisterDiscriminatorConvention(typeof(IEvent), convention);

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.

@aradalvand
Copy link

aradalvand commented Dec 16, 2024

@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?

Copy link
Contributor

@papafe papafe left a 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();
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I was just thinking we wanted a well defined order instead of a random order.

Order doesn't actually matter for the queries this will be used in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't order them the order will be somewhat random because they are coming from a dictionary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And would it be a problem if the order was random?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on your definition of "problem". The query would still return the correct results no matter what the order is, so in that sense it's not a problem if the order was random.

But I can think of several reasons to have a stable predictable order:

  • It's harder to write test assertions if the order is random
  • The server caches query execution plans and most likely if the order changes the query won't match the cached plan
  • If users look at (or log) the generated MQL they might ask us why we don't have a stable ordering

I don't see any problem with sorting them alphabetically. We could also define other orderings (like traversing the class hierarchy top-down/left-to-right), but other orderings would be harder to compute.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those are good point to keep the order, thanks for clarifying.

/// <summary>
///
/// </summary>
public class BsonClassMapScalarDiscriminatorConvention : IScalarDiscriminatorConvention
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be. I hadn't considered that. Will look into it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a good suggestion. This class now derives from StandardDiscriminatorConvention.

return discriminators.OrderBy(x => x).ToArray();
}

private BsonValue ReadDiscriminator(IBsonReader bsonReader)
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable. I will look into refactoring StandardDiscriminatorConvnetion to factor out the code that reads the discriminator value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion. Done.

return null;
}

if (_classMap.TypeToDiscriminatorMap.TryGetValue(actualType, out BsonValue discriminator))
Copy link
Contributor

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;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new discriminator convention depends on the TypeToDiscriminatorMap being initialized correctly, so we only want to return a value IF and only IF we find it in the map.

Also, we need to remove any reference to class maps in ScalarDiscriminatorConvention.

Our entire implementation of polymorphism has been wonky in the sense that we assumed that only class mapped serializers were involved. I suppose we might have to invest some more time in this PR in figuring out what happens now with classes that are NOT class mapped.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to be calling BsonClassMap.LookupClassMap.

We want to use the knowledge we have of the derived types.

@rstam rstam requested a review from adelinowona December 19, 2024 16:20
Copy link
Contributor Author

@rstam rstam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't re-requested your review yet because I still need to look into Ferdinando's suggestion to derive BsonClassMapScalarDiscriminatorConvention from StandardDiscriminatorConvention and reuse the code for reading the discriminator value.

return null;
}

if (_classMap.TypeToDiscriminatorMap.TryGetValue(actualType, out BsonValue discriminator))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new discriminator convention depends on the TypeToDiscriminatorMap being initialized correctly, so we only want to return a value IF and only IF we find it in the map.

Also, we need to remove any reference to class maps in ScalarDiscriminatorConvention.

Our entire implementation of polymorphism has been wonky in the sense that we assumed that only class mapped serializers were involved. I suppose we might have to invest some more time in this PR in figuring out what happens now with classes that are NOT class mapped.

}
}

return discriminators.OrderBy(x => x).ToArray();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I was just thinking we wanted a well defined order instead of a random order.

Order doesn't actually matter for the queries this will be used in.

/// <summary>
///
/// </summary>
public class BsonClassMapScalarDiscriminatorConvention : IScalarDiscriminatorConvention
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be. I hadn't considered that. Will look into it.

return discriminators.OrderBy(x => x).ToArray();
}

private BsonValue ReadDiscriminator(IBsonReader bsonReader)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable. I will look into refactoring StandardDiscriminatorConvnetion to factor out the code that reads the discriminator value.

bsonReader.ReturnToBookmark(bookmark);
return actualType;
}
var discriminator = ReadDiscriminator(bsonReader);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code to read the discriminator value has been factored out to a new method.

/// <param name="nominalType">The nominal type.</param>
/// <param name="discriminator">The discriminator.</param>
/// <returns>The actual type.</returns>
protected virtual Type GetActualType(Type nominalType, BsonValue discriminator)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new method is virtual so that BsonClassMapScalarDiscriminatorConvention can override it.

/// </summary>
/// <param name="bsonReader">The bsonReader.</param>
/// <returns>The discriminator, or null if no discriminator was found.</returns>
protected BsonValue ReadDiscriminator(IBsonReader bsonReader)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved up from BsonClassMapScalarDiscriminatorConvention to the base class where it can be reused.

/// <summary>
/// A scalar discriminator convention for class mapped types.
/// </summary>
public class BsonClassMapScalarDiscriminatorConvention : StandardDiscriminatorConvention, IScalarDiscriminatorConvention
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now derives from StandardDiscriminatorConvention.

/// <param name="nominalType">The nominal type.</param>
/// <param name="discriminator">The discriminator value.</param>
/// <returns>The actual type.</returns>
protected override Type GetActualType(Type nominalType, BsonValue discriminator)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BsonClassMapScalarDiscriminatorConvention overrides this method to use the information it has about derived types.

@rstam rstam requested review from BorisDog and papafe January 27, 2025 23:26
Copy link
Member

@damieng damieng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few comments mostly around quotes in exceptions plus whether we want to throw if we detect the RootClass attribute misconfigured and one question about caching but none are blockers.

{
if (!_classType.IsAssignableFrom(type))
{
throw new ArgumentException($"Type {type} is not assignable to {_classType}.", nameof(type));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Missing quotes round type name in order to be consistent with other exceptions added.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}

throw new BsonSerializationException($"No type found for discriminator value: {discriminator}.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should quote this too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discriminator value is a BsonValue. If the value is a string it will have quotes around it if not it won't.

I want to avoid the following:

No type found for discriminator value: ""abc"".

return discriminator;
}

throw new BsonSerializationException($"No discriminator value found for type: {actualType}.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quotes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. But I'm actually not sure when to use quotes and when not to...

/// <exception cref="NotImplementedException"></exception>
public BsonValue[] GetDiscriminatorsForTypeAndSubTypes(Type type)
{
return _typeToDiscriminatorsForTypeAndSubTypesMap.GetOrAdd(type, MapTypeToDiscriminatorsForTypeAndSubTypes);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure the list won't grow/be further discovered after being cached?

QueryableMethod.OfType
};

public static AggregationExpression Translate(TranslationContext context, MethodCallExpression expression)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this support of OfType against the root object's type or do we support discriminators at every object in the document?

@@ -40,7 +40,7 @@ private class C : A
{
}

[BsonDiscriminator("D~", RootClass = true)]
[BsonDiscriminator("D~")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to detect that and throw in case some customers moving from 2.x incorrectly configured their attributes?

@rstam rstam requested a review from papafe January 29, 2025 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants