Skip to content

Commit dfb589d

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 e8060e7 commit dfb589d

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
@@ -872,6 +872,67 @@ public async Task GenericGetSearchResultsAsyncFilterTranslationPreservesBingWebP
872872
}
873873
}
874874

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

877938
/// <inheritdoc/>

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

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

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

193219
default:
@@ -317,6 +343,24 @@ private static void ProcessContainsExpression(MethodCallExpression methodExpr, T
317343
}
318344
}
319345

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

0 commit comments

Comments
 (0)