From acaf89b350a1c5e4955dfeaa7cc5ffca28cab807 Mon Sep 17 00:00:00 2001 From: BorisDog Date: Wed, 26 Nov 2025 12:18:40 -0800 Subject: [PATCH 1/2] CSHARP-4040: Use AppContext switch to disable CSHARP-4040 validation (#1829) Co-authored-by: Robert Stam --- .../Serialization/BsonClassMap.cs | 30 ++- .../Jira/CSharp4040SwitchTests.cs | 184 ++++++++++++++++++ 2 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4040SwitchTests.cs diff --git a/src/MongoDB.Bson/Serialization/BsonClassMap.cs b/src/MongoDB.Bson/Serialization/BsonClassMap.cs index 0f524fe2326..3e257d2259f 100644 --- a/src/MongoDB.Bson/Serialization/BsonClassMap.cs +++ b/src/MongoDB.Bson/Serialization/BsonClassMap.cs @@ -1329,21 +1329,31 @@ internal IDiscriminatorConvention GetDiscriminatorConvention() if (discriminatorConvention != null) { - var conflictingMemberMap = _allMemberMaps.FirstOrDefault(memberMap => memberMap.ElementName == discriminatorConvention.ElementName); - - if (conflictingMemberMap != null) - { - var fieldOrProperty = conflictingMemberMap.MemberInfo is FieldInfo ? "field" : "property"; - - throw new BsonSerializationException( - $"The discriminator element name cannot be {discriminatorConvention.ElementName} " + - $"because it is already being used by the {fieldOrProperty} {conflictingMemberMap.MemberName} of type {_classType.FullName}"); - } + EnsureNoMemberMapConflicts(discriminatorConvention.ElementName); } } return discriminatorConvention; + void EnsureNoMemberMapConflicts(string elementName) + { + if (AppContext.TryGetSwitch("Switch.MongoDB.Driver.DisableDiscriminatorFieldConflictCheck", out bool disableConflictCheck) && disableConflictCheck) + { + return; + } + + var conflictingMemberMap = _allMemberMaps.FirstOrDefault(memberMap => memberMap.ElementName == elementName); + + if (conflictingMemberMap != null) + { + var fieldOrProperty = conflictingMemberMap.MemberInfo is FieldInfo ? "field" : "property"; + + throw new BsonSerializationException( + $"The discriminator element name cannot be {discriminatorConvention.ElementName} " + + $"because it is already being used by the {fieldOrProperty} {conflictingMemberMap.MemberName} of type {_classType.FullName}"); + } + } + IDiscriminatorConvention LookupDiscriminatorConvention() { var classMap = this; diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4040SwitchTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4040SwitchTests.cs new file mode 100644 index 00000000000..7a8a16cef30 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4040SwitchTests.cs @@ -0,0 +1,184 @@ +/* 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.Serialization; +using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver.Linq; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira; + +public class CSharp4040SwitchTests : LinqIntegrationTest +{ + public CSharp4040SwitchTests(ClassFixture fixture) + : base(fixture) + { + } + + [Fact] + public void Documents_should_serialize_as_expected() + { + var collection = Fixture.Collection; + + var seralizedDocuments = collection.AsQueryable().As(BsonDocumentSerializer.Instance).ToList(); + + seralizedDocuments.Count.Should().Be(2); + seralizedDocuments[0].Should().Be("{ _id : 1, TypeNames : ['C', 'D'] }"); + seralizedDocuments[1].Should().Be("{ _id : 2, TypeNames : ['C', 'D', 'E'] }"); + } + + [Fact] + public void OfType_C_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .OfType(); + + var stages = Translate(collection, queryable); + AssertStages(stages, []); + + var results = queryable.ToList(); + results.Select(x => x.Id).Should().Equal(1, 2); + } + + [Fact] + public void OfType_D_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .OfType(); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { TypeNames : 'D' } }"); + + var results = queryable.ToList(); + results.Select(x => x.Id).Should().Equal(1, 2); + } + + [Fact] + public void OfType_E_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .OfType(); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { TypeNames : 'E' } }"); + + var results = queryable.ToList(); + results.Select(x => x.Id).Should().Equal(2); + } + + [Fact] + public void Where_TypeNames_Contains_C_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Where(x => x.TypeNames.Contains("C")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { TypeNames : 'C' } }"); + + var results = queryable.ToList(); + results.Select(x => x.Id).Should().Equal(1, 2); + } + + [Fact] + public void Where_TypeNames_Contains_D_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Where(x => x.TypeNames.Contains("D")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { TypeNames : 'D' } }"); + + var results = queryable.ToList(); + results.Select(x => x.Id).Should().Equal(1, 2); + } + + [Fact] + public void Where_TypeNames_Contains_E_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Where(x => x.TypeNames.Contains("E")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { TypeNames : 'E' } }"); + + var results = queryable.ToList(); + results.Select(x => x.Id).Should().Equal(2); + } + + public abstract class C + { + public int Id { get; set; } + public virtual IReadOnlyList TypeNames => ["C"]; + } + + public class D : C + { + public override IReadOnlyList TypeNames => ["C", "D"]; + } + + public class E : D + { + public override IReadOnlyList TypeNames => ["C", "D", "E"]; + } + + public sealed class ClassFixture : MongoCollectionFixture + { + public ClassFixture() + { + AppContext.SetSwitch("Switch.MongoDB.Driver.DisableDiscriminatorFieldConflictCheck", true); + + var discriminatorConvention = new HierarchicalDiscriminatorConvention("TypeNames"); + BsonSerializer.RegisterDiscriminatorConvention(typeof(C), discriminatorConvention); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.SetIsRootClass(true); + cm.SetDiscriminatorIsRequired(true); + cm.MapMember(x => x.TypeNames).SetShouldSerializeMethod(_ => false); + }); + } + + protected override IEnumerable InitialData => + [ + new D { Id = 1 }, + new E { Id = 2 } + ]; + + public override void Dispose() + { + base.Dispose(); + AppContext.SetSwitch("Switch.MongoDB.Driver.DisableDiscriminatorFieldConflictCheck", false); + } + } +} From 2bf6a78373564fbfe0d6abd6bd0ecc85a16ec69e Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov <31327136+sanych-sun@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:28:24 -0800 Subject: [PATCH 2/2] CSHARP-5793: Map MemoryExtensions Contains and SequenceEqual with null comparer to Enumerable methods with no comparer parameter (#1828) (#1830) (cherry picked from commit 71233ea0494f5b91bf03b924ce3c88e85a3177cb) Co-authored-by: Damien Guard --- .../Misc/ClrCompatExpressionRewriter.cs | 46 ++++-------------- .../Jira/CSharp5749Tests.cs | 47 ++++++++++++++++--- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ClrCompatExpressionRewriter.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ClrCompatExpressionRewriter.cs index 13e6aa09307..fc38f9421ef 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ClrCompatExpressionRewriter.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ClrCompatExpressionRewriter.cs @@ -50,7 +50,11 @@ protected override Expression VisitMethodCall(MethodCallExpression node) static Expression VisitContainsMethod(MethodCallExpression node, MethodInfo method, ReadOnlyCollection arguments) { - if (method.IsOneOf(MemoryExtensionsMethod.ContainsWithReadOnlySpanAndValue, MemoryExtensionsMethod.ContainsWithSpanAndValue)) + var hasNoComparer = method.IsOneOf(MemoryExtensionsMethod.ContainsWithReadOnlySpanAndValue, MemoryExtensionsMethod.ContainsWithSpanAndValue); + var hasNullComparer = method.Is(MemoryExtensionsMethod.ContainsWithReadOnlySpanAndValueAndComparer) && arguments[2] is ConstantExpression { Value: null }; + + // C# 14 targets MemoryExtensionsMethod.Contains, rewrite it back to Enumerable.Contains + if (hasNoComparer || hasNullComparer) { var itemType = method.GetGenericArguments().Single(); var span = arguments[0]; @@ -65,29 +69,17 @@ static Expression VisitContainsMethod(MethodCallExpression node, MethodInfo meth [unwrappedSpan, value]); } } - else if (method.Is(MemoryExtensionsMethod.ContainsWithReadOnlySpanAndValueAndComparer)) - { - var itemType = method.GetGenericArguments().Single(); - var span = arguments[0]; - var value = arguments[1]; - var comparer = arguments[2]; - - if (TryUnwrapSpanImplicitCast(span, out var unwrappedSpan) && - unwrappedSpan.Type.ImplementsIEnumerableOf(itemType)) - { - return - Expression.Call( - EnumerableMethod.ContainsWithComparer.MakeGenericMethod(itemType), - [unwrappedSpan, value, comparer]); - } - } return node; } static Expression VisitSequenceEqualMethod(MethodCallExpression node, MethodInfo method, ReadOnlyCollection arguments) { - if (method.IsOneOf(MemoryExtensionsMethod.SequenceEqualWithReadOnlySpanAndReadOnlySpan, MemoryExtensionsMethod.SequenceEqualWithSpanAndReadOnlySpan)) + var hasNoComparer = method.IsOneOf(MemoryExtensionsMethod.SequenceEqualWithReadOnlySpanAndReadOnlySpan, MemoryExtensionsMethod.SequenceEqualWithSpanAndReadOnlySpan); + var hasNullComparer = method.IsOneOf(MemoryExtensionsMethod.SequenceEqualWithReadOnlySpanAndReadOnlySpanAndComparer, MemoryExtensionsMethod.SequenceEqualWithSpanAndReadOnlySpanAndComparer) && arguments[2] is ConstantExpression { Value: null }; + + // C# 14 targets MemoryExtensionsMethod.SequenceEquals, rewrite it back to Enumerable.SequenceEquals + if (hasNoComparer || hasNullComparer) { var itemType = method.GetGenericArguments().Single(); var span = arguments[0]; @@ -104,24 +96,6 @@ static Expression VisitSequenceEqualMethod(MethodCallExpression node, MethodInfo [unwrappedSpan, unwrappedOther]); } } - else if (method.IsOneOf(MemoryExtensionsMethod.SequenceEqualWithReadOnlySpanAndReadOnlySpanAndComparer, MemoryExtensionsMethod.SequenceEqualWithSpanAndReadOnlySpanAndComparer)) - { - var itemType = method.GetGenericArguments().Single(); - var span = arguments[0]; - var other = arguments[1]; - var comparer = arguments[2]; - - if (TryUnwrapSpanImplicitCast(span, out var unwrappedSpan) && - TryUnwrapSpanImplicitCast(other, out var unwrappedOther) && - unwrappedSpan.Type.ImplementsIEnumerableOf(itemType) && - unwrappedOther.Type.ImplementsIEnumerableOf(itemType)) - { - return - Expression.Call( - EnumerableMethod.SequenceEqualWithComparer.MakeGenericMethod(itemType), - [unwrappedSpan, unwrappedOther, comparer]); - } - } return node; } diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5749Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5749Tests.cs index 2ff2dcdb738..5015390b46e 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5749Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5749Tests.cs @@ -48,6 +48,20 @@ public void MemoryExtensions_Contains_in_Where_should_work() results.Select(x => x.Id).Should().Equal(2, 3); } + [Fact] + public void MemoryExtensions_Contains_in_Where_should_work_with_enum() + { + var collection = Fixture.Collection; + var daysOfWeek = new[] { DayOfWeek.Monday, DayOfWeek.Tuesday }; + + // Can't actually rewrite/fake these with MemoryExtensions.Contains overload with 3 args from .NET 10 + // This test will activate correctly on .NET 10+ + var queryable = collection.AsQueryable().Where(x => daysOfWeek.Contains(x.Day)); + + var results = queryable.ToArray(); + results.Select(x => x.Id).Should().Equal(2, 3); + } + [Fact] public void MemoryExtensions_Contains_in_Single_should_work() { @@ -93,6 +107,20 @@ public void MemoryExtensions_SequenceEqual_in_Where_should_work() results.Select(x => x.Id).Should().Equal(3); } + [Fact] + public void MemoryExtensions_SequenceEqual_in_Where_should_work_with_enum() + { + var collection = Fixture.Collection; + var daysOfWeek = new[] { DayOfWeek.Monday, DayOfWeek.Tuesday }; + + // Can't actually rewrite/fake these with MemoryExtensions.SequenceEqual overload with 3 args from .NET 10 + // This test will activate correctly on .NET 10+ + var queryable = collection.AsQueryable().Where(x => daysOfWeek.SequenceEqual(x.Days)); + + var results = queryable.ToArray(); + results.Select(x => x.Id).Should().Equal(1); + } + [Fact] public void MemoryExtensions_SequenceEqual_in_Single_should_work() { @@ -129,17 +157,19 @@ public void MemoryExtensions_SequenceEqual_in_Count_should_work() public class C { public int Id { get; set; } + public DayOfWeek Day { get; set; } public string Name { get; set; } public int[] Ratings { get; set; } + public DayOfWeek[] Days { get; set; } } public sealed class ClassFixture : MongoCollectionFixture { protected override IEnumerable InitialData => [ - BsonDocument.Parse("{ _id : 1, Name : \"One\", Ratings : [1, 2, 3, 4, 5] }"), - BsonDocument.Parse("{ _id : 2, Name : \"Two\", Ratings : [3, 4, 5, 6, 7] }"), - BsonDocument.Parse("{ _id : 3, Name : \"Three\", Ratings : [1, 9, 6] }") + BsonDocument.Parse("{ _id : 1, Name : \"One\", Day : 0, Ratings : [1, 2, 3, 4, 5], Days : [1, 2] }"), + BsonDocument.Parse("{ _id : 2, Name : \"Two\", Day : 1, Ratings : [3, 4, 5, 6, 7], Days: [1, 2, 3] }"), + BsonDocument.Parse("{ _id : 3, Name : \"Three\", Day : 2, Ratings : [1, 9, 6], Days: [2, 3, 4] }") ]; } @@ -175,10 +205,13 @@ static Expression VisitContainsMethod(MethodCallExpression node, MethodInfo meth if (source.Type.IsArray) { var readOnlySpan = ImplicitCastArrayToSpan(source, typeof(ReadOnlySpan<>), itemType); - return - Expression.Call( - MemoryExtensionsMethod.ContainsWithReadOnlySpanAndValue.MakeGenericMethod(itemType), - [readOnlySpan, value]); + + // Not worth checking for IEquatable and generating 3 args overload as that requires .NET 10 + // which if we had we could run the tests on natively without this visitor. + + return Expression.Call( + MemoryExtensionsMethod.ContainsWithReadOnlySpanAndValue.MakeGenericMethod(itemType), + [readOnlySpan, value]); } } else if (method.Is(EnumerableMethod.ContainsWithComparer))