diff --git a/dotnet/src/VectorData/AzureAISearch/AzureAISearchFilterTranslator.cs b/dotnet/src/VectorData/AzureAISearch/AzureAISearchFilterTranslator.cs index df8ab46e3d15..f6b724eb1340 100644 --- a/dotnet/src/VectorData/AzureAISearch/AzureAISearchFilterTranslator.cs +++ b/dotnet/src/VectorData/AzureAISearch/AzureAISearchFilterTranslator.cs @@ -187,9 +187,42 @@ private void TranslateMethodCall(MethodCallExpression methodCall) this.TranslateContains(source, item); return; + // C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"); + // this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above). + // MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove. + // See https://github.com/dotnet/runtime/issues/109757 for more context. + // Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when + // it's null. + case { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains + when contains.Method.DeclaringType == typeof(MemoryExtensions) + && (contains.Arguments.Count is 2 + || (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null })) + && TryUnwrapSpanImplicitCast(spanArg, out var source): + this.TranslateContains(source, item); + return; + default: throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}"); } + + static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) + { + if (expression is UnaryExpression + { + NodeType: ExpressionType.Convert, + Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, + Operand: var unwrapped + } + && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) + { + result = unwrapped; + return true; + } + + result = null; + return false; + } } private void TranslateContains(Expression source, Expression item) diff --git a/dotnet/src/VectorData/Common/SqlFilterTranslator.cs b/dotnet/src/VectorData/Common/SqlFilterTranslator.cs index 086efc010f55..1cd25d808dfe 100644 --- a/dotnet/src/VectorData/Common/SqlFilterTranslator.cs +++ b/dotnet/src/VectorData/Common/SqlFilterTranslator.cs @@ -239,9 +239,42 @@ private void TranslateMethodCall(MethodCallExpression methodCall, bool isSearchC this.TranslateContains(source, item); return; + // C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"); + // this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above). + // MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove. + // See https://github.com/dotnet/runtime/issues/109757 for more context. + // Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when + // it's null. + case { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains + when contains.Method.DeclaringType == typeof(MemoryExtensions) + && (contains.Arguments.Count is 2 + || (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null })) + && TryUnwrapSpanImplicitCast(spanArg, out var source): + this.TranslateContains(source, item); + return; + default: throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}"); } + + static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) + { + if (expression is UnaryExpression + { + NodeType: ExpressionType.Convert, + Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, + Operand: var unwrapped + } + && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) + { + result = unwrapped; + return true; + } + + result = null; + return false; + } } private void TranslateContains(Expression source, Expression item) diff --git a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs index d14c6b37a73f..6f53a9a9e525 100644 --- a/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs +++ b/dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs @@ -155,7 +155,8 @@ private BsonDocument TranslateNot(UnaryExpression not) } private BsonDocument TranslateMethodCall(MethodCallExpression methodCall) - => methodCall switch + { + return methodCall switch { // Enumerable.Contains() { Method.Name: nameof(Enumerable.Contains), Arguments: [var source, var item] } contains @@ -171,11 +172,45 @@ private BsonDocument TranslateMethodCall(MethodCallExpression methodCall) }, Object: Expression source, Arguments: [var item] - } when declaringType.GetGenericTypeDefinition() == typeof(List<>) => this.TranslateContains(source, item), + } when declaringType.GetGenericTypeDefinition() == typeof(List<>) + => this.TranslateContains(source, item), + + // C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"); + // this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above). + // MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove. + // See https://github.com/dotnet/runtime/issues/109757 for more context. + // Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when + // it's null. + { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains + when contains.Method.DeclaringType == typeof(MemoryExtensions) + && (contains.Arguments.Count is 2 + || (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null })) + && TryUnwrapSpanImplicitCast(spanArg, out var source) + => this.TranslateContains(source, item), _ => throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}") }; + static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) + { + if (expression is UnaryExpression + { + NodeType: ExpressionType.Convert, + Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, + Operand: var unwrapped + } + && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) + { + result = unwrapped; + return true; + } + + result = null; + return false; + } + } + private BsonDocument TranslateContains(Expression source, Expression item) { switch (source) diff --git a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlFilterTranslator.cs b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlFilterTranslator.cs index deb629f94813..4ac5715d237a 100644 --- a/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlFilterTranslator.cs +++ b/dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlFilterTranslator.cs @@ -230,9 +230,42 @@ private void TranslateMethodCall(MethodCallExpression methodCall) this.TranslateContains(source, item); return; + // C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"); + // this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above). + // MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove. + // See https://github.com/dotnet/runtime/issues/109757 for more context. + // Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when + // it's null. + case { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains + when contains.Method.DeclaringType == typeof(MemoryExtensions) + && (contains.Arguments.Count is 2 + || (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null })) + && TryUnwrapSpanImplicitCast(spanArg, out var source): + this.TranslateContains(source, item); + return; + default: throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}"); } + + static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) + { + if (expression is UnaryExpression + { + NodeType: ExpressionType.Convert, + Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, + Operand: var unwrapped + } + && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) + { + result = unwrapped; + return true; + } + + result = null; + return false; + } } private void TranslateContains(Expression source, Expression item) diff --git a/dotnet/src/VectorData/Directory.Build.props b/dotnet/src/VectorData/Directory.Build.props index 44a8810c0a8e..6e99a038778f 100644 --- a/dotnet/src/VectorData/Directory.Build.props +++ b/dotnet/src/VectorData/Directory.Build.props @@ -3,6 +3,7 @@ + latest $(NoWarn);MEVD9000,MEVD9001 diff --git a/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs b/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs index 98ae37a4311a..50f7082050c6 100644 --- a/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs +++ b/dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs @@ -161,7 +161,8 @@ private BsonDocument TranslateNot(UnaryExpression not) } private BsonDocument TranslateMethodCall(MethodCallExpression methodCall) - => methodCall switch + { + return methodCall switch { // Enumerable.Contains() { Method.Name: nameof(Enumerable.Contains), Arguments: [var source, var item] } contains @@ -179,9 +180,42 @@ private BsonDocument TranslateMethodCall(MethodCallExpression methodCall) Arguments: [var item] } when declaringType.GetGenericTypeDefinition() == typeof(List<>) => this.TranslateContains(source, item), + // C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"); + // this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above). + // MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove. + // See https://github.com/dotnet/runtime/issues/109757 for more context. + // Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when + // it's null. + { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains + when contains.Method.DeclaringType == typeof(MemoryExtensions) + && (contains.Arguments.Count is 2 + || (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null })) + && TryUnwrapSpanImplicitCast(spanArg, out var source) + => this.TranslateContains(source, item), + _ => throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}") }; + static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) + { + if (expression is UnaryExpression + { + NodeType: ExpressionType.Convert, + Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, + Operand: var unwrapped + } + && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) + { + result = unwrapped; + return true; + } + + result = null; + return false; + } + } + private BsonDocument TranslateContains(Expression source, Expression item) { switch (source) diff --git a/dotnet/src/VectorData/Pinecone/PineconeFilterTranslator.cs b/dotnet/src/VectorData/Pinecone/PineconeFilterTranslator.cs index 34c5fb39d7d7..bfe02e9b013a 100644 --- a/dotnet/src/VectorData/Pinecone/PineconeFilterTranslator.cs +++ b/dotnet/src/VectorData/Pinecone/PineconeFilterTranslator.cs @@ -158,7 +158,8 @@ private Metadata TranslateNot(UnaryExpression not) } private Metadata TranslateMethodCall(MethodCallExpression methodCall) - => methodCall switch + { + return methodCall switch { // Enumerable.Contains() { Method.Name: nameof(Enumerable.Contains), Arguments: [var source, var item] } contains @@ -176,9 +177,42 @@ private Metadata TranslateMethodCall(MethodCallExpression methodCall) Arguments: [var item] } when declaringType.GetGenericTypeDefinition() == typeof(List<>) => this.TranslateContains(source, item), + // C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"); + // this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above). + // MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove. + // See https://github.com/dotnet/runtime/issues/109757 for more context. + // Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when + // it's null. + { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains + when contains.Method.DeclaringType == typeof(MemoryExtensions) + && (contains.Arguments.Count is 2 + || (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null })) + && TryUnwrapSpanImplicitCast(spanArg, out var source) + => this.TranslateContains(source, item), + _ => throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}") }; + static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) + { + if (expression is UnaryExpression + { + NodeType: ExpressionType.Convert, + Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, + Operand: var unwrapped + } + && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) + { + result = unwrapped; + return true; + } + + result = null; + return false; + } + } + private Metadata TranslateContains(Expression source, Expression item) { switch (source) diff --git a/dotnet/src/VectorData/Qdrant/QdrantFilterTranslator.cs b/dotnet/src/VectorData/Qdrant/QdrantFilterTranslator.cs index bc82b366e7b0..1cf26de9c072 100644 --- a/dotnet/src/VectorData/Qdrant/QdrantFilterTranslator.cs +++ b/dotnet/src/VectorData/Qdrant/QdrantFilterTranslator.cs @@ -267,7 +267,8 @@ private Filter TranslateNot(Expression expression) #endregion Logical operators private Filter TranslateMethodCall(MethodCallExpression methodCall) - => methodCall switch + { + return methodCall switch { // Enumerable.Contains() { Method.Name: nameof(Enumerable.Contains), Arguments: [var source, var item] } contains @@ -286,9 +287,42 @@ private Filter TranslateMethodCall(MethodCallExpression methodCall) } when declaringType.GetGenericTypeDefinition() == typeof(List<>) => this.TranslateContains(source, item), + // C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"); + // this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above). + // MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove. + // See https://github.com/dotnet/runtime/issues/109757 for more context. + // Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when + // it's null. + { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains + when contains.Method.DeclaringType == typeof(MemoryExtensions) + && (contains.Arguments.Count is 2 + || (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null })) + && TryUnwrapSpanImplicitCast(spanArg, out var source) + => this.TranslateContains(source, item), + _ => throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}") }; + static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) + { + if (expression is UnaryExpression + { + NodeType: ExpressionType.Convert, + Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, + Operand: var unwrapped + } + && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) + { + result = unwrapped; + return true; + } + + result = null; + return false; + } + } + private Filter TranslateContains(Expression source, Expression item) { switch (source) diff --git a/dotnet/src/VectorData/Redis/RedisFilterTranslator.cs b/dotnet/src/VectorData/Redis/RedisFilterTranslator.cs index eec5ae6f3da5..f71873e8f87f 100644 --- a/dotnet/src/VectorData/Redis/RedisFilterTranslator.cs +++ b/dotnet/src/VectorData/Redis/RedisFilterTranslator.cs @@ -173,9 +173,42 @@ private void TranslateMethodCall(MethodCallExpression methodCall) this.TranslateContains(source, item); return; + // C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"); + // this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above). + // MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove. + // See https://github.com/dotnet/runtime/issues/109757 for more context. + // Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when + // it's null. + case { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains + when contains.Method.DeclaringType == typeof(MemoryExtensions) + && (contains.Arguments.Count is 2 + || (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null })) + && TryUnwrapSpanImplicitCast(spanArg, out var source): + this.TranslateContains(source, item); + return; + default: throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}"); } + + static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) + { + if (expression is UnaryExpression + { + NodeType: ExpressionType.Convert, + Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, + Operand: var unwrapped + } + && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) + { + result = unwrapped; + return true; + } + + result = null; + return false; + } } private void TranslateContains(Expression source, Expression item) diff --git a/dotnet/src/VectorData/Weaviate/WeaviateFilterTranslator.cs b/dotnet/src/VectorData/Weaviate/WeaviateFilterTranslator.cs index 4d2f1177ba4e..610f77869b9b 100644 --- a/dotnet/src/VectorData/Weaviate/WeaviateFilterTranslator.cs +++ b/dotnet/src/VectorData/Weaviate/WeaviateFilterTranslator.cs @@ -213,9 +213,42 @@ private void TranslateMethodCall(MethodCallExpression methodCall) this.TranslateContains(source, item); return; + // C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"); + // this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above). + // MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove. + // See https://github.com/dotnet/runtime/issues/109757 for more context. + // Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when + // it's null. + case { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains + when contains.Method.DeclaringType == typeof(MemoryExtensions) + && (contains.Arguments.Count is 2 + || (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null })) + && TryUnwrapSpanImplicitCast(spanArg, out var source): + this.TranslateContains(source, item); + return; + default: throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}"); } + + static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) + { + if (expression is UnaryExpression + { + NodeType: ExpressionType.Convert, + Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, + Operand: var unwrapped + } + && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) + { + result = unwrapped; + return true; + } + + result = null; + return false; + } } private void TranslateContains(Expression source, Expression item) diff --git a/dotnet/test/VectorData/Directory.Build.props b/dotnet/test/VectorData/Directory.Build.props index e58e3eb681b5..1553ed4cb332 100644 --- a/dotnet/test/VectorData/Directory.Build.props +++ b/dotnet/test/VectorData/Directory.Build.props @@ -3,6 +3,8 @@ + latest + $(NoWarn);MEVD9000,MEVD9001 $(NoWarn);CA1515 $(NoWarn);CA1707 diff --git a/dotnet/test/VectorData/Redis.ConformanceTests/Filter/RedisBasicFilterTests.cs b/dotnet/test/VectorData/Redis.ConformanceTests/Filter/RedisBasicFilterTests.cs index 80efcf42978b..0f144cdae20c 100644 --- a/dotnet/test/VectorData/Redis.ConformanceTests/Filter/RedisBasicFilterTests.cs +++ b/dotnet/test/VectorData/Redis.ConformanceTests/Filter/RedisBasicFilterTests.cs @@ -123,6 +123,19 @@ public override Task Contains_over_field_string_array() public override Task Contains_over_field_string_List() => Assert.ThrowsAsync(() => base.Contains_over_field_string_List()); + public override Task Contains_with_Enumerable_Contains() + => Assert.ThrowsAsync(() => base.Contains_with_Enumerable_Contains()); + +#if !NETFRAMEWORK + public override Task Contains_with_MemoryExtensions_Contains() + => Assert.ThrowsAsync(() => base.Contains_with_MemoryExtensions_Contains()); +#endif + +#if NET10_0_OR_GREATER + public override Task Contains_with_MemoryExtensions_Contains_with_null_comparer() + => Assert.ThrowsAsync(() => base.Contains_with_MemoryExtensions_Contains_with_null_comparer()); +#endif + [Obsolete("Legacy filter support")] public override Task Legacy_AnyTagEqualTo_array() => Assert.ThrowsAsync(() => base.Legacy_AnyTagEqualTo_array()); diff --git a/dotnet/test/VectorData/Redis.ConformanceTests/Filter/RedisBasicQueryTests.cs b/dotnet/test/VectorData/Redis.ConformanceTests/Filter/RedisBasicQueryTests.cs index 9eb77d58ee71..9cf5eac40e14 100644 --- a/dotnet/test/VectorData/Redis.ConformanceTests/Filter/RedisBasicQueryTests.cs +++ b/dotnet/test/VectorData/Redis.ConformanceTests/Filter/RedisBasicQueryTests.cs @@ -123,6 +123,19 @@ public override Task Contains_over_field_string_array() public override Task Contains_over_field_string_List() => Assert.ThrowsAsync(() => base.Contains_over_field_string_List()); + public override Task Contains_with_Enumerable_Contains() + => Assert.ThrowsAsync(() => base.Contains_with_Enumerable_Contains()); + +#if !NETFRAMEWORK + public override Task Contains_with_MemoryExtensions_Contains() + => Assert.ThrowsAsync(() => base.Contains_with_MemoryExtensions_Contains()); +#endif + +#if NET10_0_OR_GREATER + public override Task Contains_with_MemoryExtensions_Contains_with_null_comparer() + => Assert.ThrowsAsync(() => base.Contains_with_MemoryExtensions_Contains_with_null_comparer()); +#endif + public new class Fixture : BasicQueryTests.QueryFixture { public override TestStore TestStore => RedisTestStore.HashSetInstance; diff --git a/dotnet/test/VectorData/SqlServer.ConformanceTests/Filter/SqlServerBasicFilterTests.cs b/dotnet/test/VectorData/SqlServer.ConformanceTests/Filter/SqlServerBasicFilterTests.cs index aa065e5104b6..d6656cf9d2a4 100644 --- a/dotnet/test/VectorData/SqlServer.ConformanceTests/Filter/SqlServerBasicFilterTests.cs +++ b/dotnet/test/VectorData/SqlServer.ConformanceTests/Filter/SqlServerBasicFilterTests.cs @@ -42,6 +42,19 @@ public override Task Contains_over_field_string_array() public override Task Contains_over_field_string_List() => Assert.ThrowsAsync(() => base.Contains_over_field_string_List()); + public override Task Contains_with_Enumerable_Contains() + => Assert.ThrowsAsync(() => base.Contains_with_Enumerable_Contains()); + +#if !NETFRAMEWORK + public override Task Contains_with_MemoryExtensions_Contains() + => Assert.ThrowsAsync(() => base.Contains_with_MemoryExtensions_Contains()); +#endif + +#if NET10_0_OR_GREATER + public override Task Contains_with_MemoryExtensions_Contains_with_null_comparer() + => Assert.ThrowsAsync(() => base.Contains_with_MemoryExtensions_Contains_with_null_comparer()); +#endif + [Fact(Skip = "Not supported")] [Obsolete("Legacy filters are not supported")] public override Task Legacy_And() => throw new NotSupportedException(); diff --git a/dotnet/test/VectorData/SqlServer.ConformanceTests/Filter/SqlServerBasicQueryTests.cs b/dotnet/test/VectorData/SqlServer.ConformanceTests/Filter/SqlServerBasicQueryTests.cs index 866b7fe0da26..b3c1acc13f60 100644 --- a/dotnet/test/VectorData/SqlServer.ConformanceTests/Filter/SqlServerBasicQueryTests.cs +++ b/dotnet/test/VectorData/SqlServer.ConformanceTests/Filter/SqlServerBasicQueryTests.cs @@ -42,6 +42,19 @@ public override Task Contains_over_field_string_array() public override Task Contains_over_field_string_List() => Assert.ThrowsAsync(() => base.Contains_over_field_string_List()); + public override Task Contains_with_Enumerable_Contains() + => Assert.ThrowsAsync(() => base.Contains_with_Enumerable_Contains()); + +#if !NETFRAMEWORK + public override Task Contains_with_MemoryExtensions_Contains() + => Assert.ThrowsAsync(() => base.Contains_with_MemoryExtensions_Contains()); +#endif + +#if NET10_0_OR_GREATER + public override Task Contains_with_MemoryExtensions_Contains_with_null_comparer() + => Assert.ThrowsAsync(() => base.Contains_with_MemoryExtensions_Contains_with_null_comparer()); +#endif + public new class Fixture : BasicQueryTests.QueryFixture { private static readonly string s_uniqueName = Guid.NewGuid().ToString(); diff --git a/dotnet/test/VectorData/VectorData.ConformanceTests/Filter/BasicFilterTests.cs b/dotnet/test/VectorData/VectorData.ConformanceTests/Filter/BasicFilterTests.cs index b429059b73b2..2bded06ec7e1 100644 --- a/dotnet/test/VectorData/VectorData.ConformanceTests/Filter/BasicFilterTests.cs +++ b/dotnet/test/VectorData/VectorData.ConformanceTests/Filter/BasicFilterTests.cs @@ -333,6 +333,37 @@ public virtual Task Contains_over_captured_string_array() r => array.Contains(r["String"])); } +#pragma warning disable RCS1196 // Call extension method as instance method + + // C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"); + // this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above). + // See https://github.com/dotnet/runtime/issues/109757 for more context. + // The following tests the various Contains variants directly, without using extension syntax, to ensure everything's + // properly supported. + [ConditionalFact] + public virtual Task Contains_with_Enumerable_Contains() + => this.TestFilterAsync( + r => Enumerable.Contains(r.StringArray, "x"), + r => ((string[])r["StringArray"]!).Contains("x")); + +#if !NETFRAMEWORK + [ConditionalFact] + public virtual Task Contains_with_MemoryExtensions_Contains() + => this.TestFilterAsync( + r => MemoryExtensions.Contains(r.StringArray, "x"), + r => ((string[])r["StringArray"]!).Contains("x")); +#endif + +#if NET10_0_OR_GREATER + [ConditionalFact] + public virtual Task Contains_with_MemoryExtensions_Contains_with_null_comparer() + => this.TestFilterAsync( + r => MemoryExtensions.Contains(r.StringArray, "x", comparer: null), + r => ((string[])r["StringArray"]!).Contains("x")); +#endif + +#pragma warning restore RCS1196 // Call extension method as instance method + #endregion Contains #region Variable types