Skip to content

Commit db5c9a2

Browse files
committed
feat: Add C# 14 MemoryExtensions.Contains support to BingTextSearch
Support detection of MemoryExtensions.Contains patterns in LINQ filters for C# 14 compatibility. In C# 14, Contains over arrays/collections now resolves to MemoryExtensions.Contains instead of Enumerable.Contains due to 'first-class spans' overload resolution changes. Since Bing Search API does not support OR logic across multiple values, collection Contains patterns (e.g., array.Contains(page.Property)) throw clear NotSupportedException explaining the limitation and suggesting alternatives. Changes: - Add System.Diagnostics.CodeAnalysis using for NotNullWhen attribute - Enhance Contains case to distinguish instance vs static method calls - Add IsMemoryExtensionsContains helper to detect C# 14 patterns - Throw NotSupportedException for both Enumerable and MemoryExtensions collection Contains patterns with clear actionable error messages - Add 2 tests: collection Contains exception + String.Contains regression Implements pattern awareness from PR #13263 by @roji Addresses feedback on PR #13188 from @roji about C# 14 compatibility Contributes to #10456 (LINQ filtering migration initiative) Fixes #12504 compatibility for text search implementations
1 parent 1f97986 commit db5c9a2

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

dotnet/src/Plugins/Plugins.UnitTests/Web/Bing/BingTextSearchTests.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,67 @@ public async Task GenericGetSearchResultsAsyncFilterTranslationPreservesBingWebP
871871
}
872872
}
873873

874+
[Fact]
875+
public async Task CollectionContainsFilterThrowsNotSupportedExceptionAsync()
876+
{
877+
// Arrange - Tests both Enumerable.Contains (C# 13-) and MemoryExtensions.Contains (C# 14+)
878+
// The same code array.Contains() resolves differently based on C# language version:
879+
// - C# 13 and earlier: Enumerable.Contains (LINQ extension method)
880+
// - C# 14 and later: MemoryExtensions.Contains (span-based optimization due to "first-class spans")
881+
// Our implementation handles both identically since Bing API doesn't support OR logic for either
882+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
883+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
884+
string[] languages = ["en", "fr"];
885+
886+
// Act & Assert - Verify that collection Contains pattern throws clear exception
887+
var searchOptions = new TextSearchOptions<BingWebPage>
888+
{
889+
Top = 5,
890+
Skip = 0,
891+
Filter = page => languages.Contains(page.Language!) // Enumerable.Contains (C# 13-) or MemoryExtensions.Contains (C# 14+)
892+
};
893+
894+
var exception = await Assert.ThrowsAsync<NotSupportedException>(async () =>
895+
{
896+
await textSearch.SearchAsync("test", searchOptions);
897+
});
898+
899+
// Assert - Verify error message explains the limitation clearly
900+
Assert.Contains("Collection Contains filters", exception.Message);
901+
Assert.Contains("not supported by Bing Search API", exception.Message);
902+
Assert.Contains("OR logic", exception.Message);
903+
}
904+
905+
[Fact]
906+
public async Task StringContainsStillWorksWithLINQFiltersAsync()
907+
{
908+
// Arrange - Verify that String.Contains (instance method) still works
909+
// String.Contains is NOT affected by C# 14 "first-class spans" - only arrays are
910+
this._messageHandlerStub.AddJsonResponse(File.ReadAllText(WhatIsTheSKResponseJson));
911+
ITextSearch<BingWebPage> textSearch = new BingTextSearch(apiKey: "ApiKey", options: new() { HttpClient = this._httpClient });
912+
913+
// Act - String.Contains should continue to work
914+
var searchOptions = new TextSearchOptions<BingWebPage>
915+
{
916+
Top = 5,
917+
Skip = 0,
918+
Filter = page => page.Name.Contains("Semantic") // String.Contains - instance method
919+
};
920+
921+
KernelSearchResults<string> result = await textSearch.SearchAsync("test", searchOptions);
922+
923+
// Assert - Should succeed without exception
924+
Assert.NotNull(result);
925+
Assert.NotNull(result.Results);
926+
var resultsList = await result.Results.ToListAsync();
927+
Assert.NotEmpty(resultsList);
928+
929+
// Verify the filter was translated correctly to intitle: operator
930+
var requestUris = this._messageHandlerStub.RequestUris;
931+
Assert.Single(requestUris);
932+
Assert.Contains("intitle%3ASemantic", requestUris[0]!.AbsoluteUri);
933+
}
934+
874935
#endregion
875936

876937
/// <inheritdoc/>

dotnet/src/Plugins/Plugins.Web/Bing/BingTextSearch.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,34 @@ private static void ProcessExpression(Expression expression, TextSearchFilter fi
184184
break;
185185

186186
case MethodCallExpression methodExpr when methodExpr.Method.Name == "Contains":
187-
// Handle Contains: page => page.Name.Contains("Microsoft")
188-
ProcessContainsExpression(methodExpr, filter);
187+
// Distinguish between instance method (String.Contains) and static method (Enumerable/MemoryExtensions.Contains)
188+
if (methodExpr.Object is MemberExpression)
189+
{
190+
// Instance method: page.Name.Contains("value") - SUPPORTED
191+
ProcessContainsExpression(methodExpr, filter);
192+
}
193+
else if (methodExpr.Object == null)
194+
{
195+
// Static method: could be Enumerable.Contains (C# 13-) or MemoryExtensions.Contains (C# 14+)
196+
// Bing API doesn't support OR logic, so collection Contains patterns are not supported
197+
if (methodExpr.Method.DeclaringType == typeof(Enumerable) ||
198+
(methodExpr.Method.DeclaringType == typeof(MemoryExtensions) && IsMemoryExtensionsContains(methodExpr)))
199+
{
200+
throw new NotSupportedException(
201+
"Collection Contains filters (e.g., array.Contains(page.Property)) are not supported by Bing Search API. " +
202+
"Bing's advanced search operators do not support OR logic across multiple values. " +
203+
"Supported pattern: Property.Contains(\"value\") for string properties like Name, Snippet, or Url. " +
204+
"For multiple value matching, consider alternative approaches or use a different search provider.");
205+
}
206+
207+
throw new NotSupportedException(
208+
$"Contains() method from {methodExpr.Method.DeclaringType?.Name} is not supported.");
209+
}
210+
else
211+
{
212+
throw new NotSupportedException(
213+
"Contains() must be called on a property (e.g., page.Name.Contains(\"value\")).");
214+
}
189215
break;
190216

191217
default:
@@ -315,6 +341,24 @@ private static void ProcessContainsExpression(MethodCallExpression methodExpr, T
315341
}
316342
}
317343

344+
/// <summary>
345+
/// Determines if a MethodCallExpression is a MemoryExtensions.Contains call (C# 14 "first-class spans" feature).
346+
/// </summary>
347+
/// <param name="methodExpr">The method call expression to check.</param>
348+
/// <returns>True if this is a MemoryExtensions.Contains call with supported parameters; otherwise false.</returns>
349+
private static bool IsMemoryExtensionsContains(MethodCallExpression methodExpr)
350+
{
351+
// MemoryExtensions.Contains has 2-3 parameters:
352+
// - Contains<T>(ReadOnlySpan<T> span, T value)
353+
// - Contains<T>(ReadOnlySpan<T> span, T value, IEqualityComparer<T>? comparer)
354+
// We only support when comparer is null or omitted
355+
return methodExpr.Method.Name == nameof(MemoryExtensions.Contains) &&
356+
methodExpr.Arguments.Count >= 2 &&
357+
methodExpr.Arguments.Count <= 3 &&
358+
(methodExpr.Arguments.Count == 2 ||
359+
(methodExpr.Arguments.Count == 3 && methodExpr.Arguments[2] is ConstantExpression { Value: null }));
360+
}
361+
318362
/// <summary>
319363
/// Maps BingWebPage property names to Bing API filter field names for equality operations.
320364
/// </summary>

0 commit comments

Comments
 (0)