From 6c87bc25045d8ad31c65b169616c1f8515a4bd02 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:25:30 +0100 Subject: [PATCH] CSHARP-5757: The problem of filtering by derived types --- src/MongoDB.Driver/FilterDefinitionBuilder.cs | 6 - .../Translators/DiscriminatorAstFilter.cs | 5 + .../Jira/CSharp5757Tests.cs | 205 ++++++++++++++++++ 3 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 tests/MongoDB.Driver.Tests/Jira/CSharp5757Tests.cs diff --git a/src/MongoDB.Driver/FilterDefinitionBuilder.cs b/src/MongoDB.Driver/FilterDefinitionBuilder.cs index e07d9ac957c..d187178c96e 100644 --- a/src/MongoDB.Driver/FilterDefinitionBuilder.cs +++ b/src/MongoDB.Driver/FilterDefinitionBuilder.cs @@ -2212,12 +2212,6 @@ public override BsonDocument Render(RenderArgs args) throw new NotSupportedException(message); } - var discriminator = discriminatorConvention.GetDiscriminator(typeof(TDocument), typeof(TDerived)); - if (discriminator == null) - { - throw new NotSupportedException($"OfType requires that documents of type {BsonUtils.GetFriendlyTypeName(typeof(TDerived))} have a discriminator value."); - } - var discriminatorField = new AstFilterField(discriminatorConvention.ElementName); ofTypeFilter= discriminatorConvention switch { diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/DiscriminatorAstFilter.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/DiscriminatorAstFilter.cs index 76149481b73..32dc7a761d8 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/DiscriminatorAstFilter.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/DiscriminatorAstFilter.cs @@ -55,6 +55,11 @@ public static AstFilter TypeEquals(AstFilterField discriminatorField, IDiscrimin public static AstFieldOperationFilter TypeIs(AstFilterField discriminatorField, IHierarchicalDiscriminatorConvention discriminatorConvention, Type nominalType, Type actualType) { var discriminator = discriminatorConvention.GetDiscriminator(nominalType, actualType); + if (discriminator == null) + { + throw new NotSupportedException($"Hierarchical discriminator convention requires that documents of type {BsonUtils.GetFriendlyTypeName(actualType)} have a discriminator value."); + } + var lastItem = discriminator is BsonArray array ? array.Last() : discriminator; return AstFilter.Eq(discriminatorField, lastItem); // will match subclasses also } diff --git a/tests/MongoDB.Driver.Tests/Jira/CSharp5757Tests.cs b/tests/MongoDB.Driver.Tests/Jira/CSharp5757Tests.cs new file mode 100644 index 00000000000..d7d2090d4a9 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Jira/CSharp5757Tests.cs @@ -0,0 +1,205 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using MongoDB.Driver.TestHelpers; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Bson.Serialization.Serializers; +using Xunit; + +namespace MongoDB.Driver.Tests.Jira; + +public class CSharp5757Tests : LinqIntegrationTest +{ + static CSharp5757Tests() + { + var scalarDiscriminatorConvention = new AnimalDiscriminatorConvention(); + var hierarchicalDiscriminatorConvention = new PersonDiscriminatorConvention(); + BsonSerializer.RegisterDiscriminatorConvention(typeof(Animal), scalarDiscriminatorConvention); + BsonSerializer.RegisterDiscriminatorConvention(typeof(Person), hierarchicalDiscriminatorConvention); + } + + public CSharp5757Tests(ClassFixture fixture) + : base(fixture) + { + } + + [Fact] + public void HierarchicalDiscriminator_with_Filter_OfType_HealthCareWorker_should_throw() + { + var filter = Builders.Filter.OfType(); + + var renderedArgs = + new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); + + var exception = Record.Exception(() => filter.Render(renderedArgs)); + exception.Should().BeOfType(); + exception.Message.Should().Be("Hierarchical discriminator convention requires that documents of type HealthCareWorker have a discriminator value."); + } + + [Fact] + public void HierarchicalDiscriminator_with_Queryable_OfType_HealthCareWorker_should_throw() + { + var collection = Fixture.Database.GetCollection("person"); + var queryable = collection.AsQueryable() + .OfType(); + + + var exception = Record.Exception(() => Translate(collection, queryable)); + exception.Should().BeOfType(); + exception.Message.Should().Be("Hierarchical discriminator convention requires that documents of type HealthCareWorker have a discriminator value."); + } + + [Fact] + public void ScalarDiscriminator_with_Filter_OfType_Mammal_should_work() + { + var collection = Fixture.Collection; + var filter = Builders.Filter.OfType(); + + var renderedFilter = filter.Render(new RenderArgs(collection.DocumentSerializer, BsonSerializer.SerializerRegistry)); + renderedFilter.Should().Be("{ _t : { $in : ['Cat', 'Dog'] } }"); + + var results = collection.FindSync(filter).ToList(); + results.Select(x => x.Id).Should().Equal(1, 2); + } + + [Fact] + public void ScalarDiscriminator_with_Queryable_OfType_Mammal_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .OfType(); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { _t : { $in : ['Cat', 'Dog'] } } }"); + + var results = queryable.ToList(); + results.Select(x => x.Id).Should().Equal(1, 2); + } + + public abstract class Person + { + } + + public abstract class HealthCareWorker : Person + { + } + + public class Doctor : HealthCareWorker + { + } + + public class Nurse : HealthCareWorker + { + } + + public class PersonDiscriminatorConvention : IHierarchicalDiscriminatorConvention + { + public string ElementName => "_t"; + + public Type GetActualType(IBsonReader bsonReader, Type nominalType) + { + throw new NotImplementedException(); + } + + public BsonValue GetDiscriminator(Type nominalType, Type actualType) + => actualType.IsAbstract ? null : actualType.Name; + } + + public abstract class Animal + { + public int Id { get; set; } + } + + public abstract class Mammal : Animal + { + } + + public class Cat : Mammal + { + } + + public class Dog : Mammal + { + } + + public class AnimalDiscriminatorConvention : IScalarDiscriminatorConvention + { + public string ElementName => "_t"; + + public Type GetActualType(IBsonReader bsonReader, Type nominalType) + { + var discriminatorValue = ReadDiscriminatorValue(bsonReader); + return discriminatorValue switch + { + "Cat" => typeof(Cat), + "Dog" => typeof(Dog), + _ => throw new Exception($"Invalid discriminator value: {discriminatorValue}.") + }; + } + + public BsonValue GetDiscriminator(Type nominalType, Type actualType) + => actualType.IsAbstract ? null : actualType.Name; + + public BsonValue[] GetDiscriminatorsForTypeAndSubTypes(Type type) + => type.Name switch + { + "Animal" => ["Cat", "Dog"], + "Mammal" => ["Cat", "Dog"], + "Cat" => ["Cat"], + "Dog" => ["Dog"], + _ => throw new ArgumentException($"Invalid type: {type.Name}.") + }; + + private string ReadDiscriminatorValue(IBsonReader bsonReader) + { + string discriminatorValue = null; + + var bsonType = bsonReader.GetCurrentBsonType(); + if (bsonType == BsonType.Document) + { + var bookmark = bsonReader.GetBookmark(); + bsonReader.ReadStartDocument(); + if (bsonReader.FindElement("_t")) + { + var context = BsonDeserializationContext.CreateRoot(bsonReader); + if (BsonValueSerializer.Instance.Deserialize(context) is BsonString bsonString) + { + discriminatorValue = bsonString.Value; + } + } + bsonReader.ReturnToBookmark(bookmark); + } + + return discriminatorValue; + } + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData => + [ + new Cat { Id = 1 }, + new Dog { Id = 2 } + ]; + } +} \ No newline at end of file