@@ -134,39 +134,168 @@ private static TextSearchOptions ConvertToLegacyOptions(TextSearchOptions<BingWe
134134
135135 /// <summary>
136136 /// Converts a LINQ expression to a TextSearchFilter compatible with Bing API.
137- /// Only supports simple property equality expressions that map to Bing's filter capabilities .
137+ /// Supports equality, inequality, Contains() method calls, and logical AND operator .
138138 /// </summary>
139139 /// <param name="linqExpression">The LINQ expression to convert.</param>
140140 /// <returns>A TextSearchFilter with equivalent filtering.</returns>
141141 /// <exception cref="NotSupportedException">Thrown when the expression cannot be converted to Bing filters.</exception>
142142 private static TextSearchFilter ConvertLinqExpressionToBingFilter < TRecord > ( Expression < Func < TRecord , bool > > linqExpression )
143143 {
144- if ( linqExpression . Body is BinaryExpression binaryExpr && binaryExpr . NodeType == ExpressionType . Equal )
144+ var filter = new TextSearchFilter ( ) ;
145+ ProcessExpression ( linqExpression . Body , filter ) ;
146+ return filter ;
147+ }
148+
149+ /// <summary>
150+ /// Recursively processes LINQ expression nodes and builds Bing API filters.
151+ /// </summary>
152+ private static void ProcessExpression ( Expression expression , TextSearchFilter filter )
153+ {
154+ switch ( expression )
155+ {
156+ case BinaryExpression binaryExpr when binaryExpr . NodeType == ExpressionType . AndAlso :
157+ // Handle AND: page => page.Language == "en" && page.Name.Contains("AI")
158+ ProcessExpression ( binaryExpr . Left , filter ) ;
159+ ProcessExpression ( binaryExpr . Right , filter ) ;
160+ break ;
161+
162+ case BinaryExpression binaryExpr when binaryExpr . NodeType == ExpressionType . OrElse :
163+ // Handle OR: Currently not directly supported by TextSearchFilter
164+ // Bing API supports OR via multiple queries, but TextSearchFilter doesn't expose this
165+ throw new NotSupportedException (
166+ "Logical OR (||) is not supported by Bing Text Search filters. " +
167+ "Consider splitting into multiple search queries." ) ;
168+
169+ case UnaryExpression unaryExpr when unaryExpr . NodeType == ExpressionType . Not :
170+ // Handle NOT: page => !page.Language.Equals("en")
171+ throw new NotSupportedException (
172+ "Logical NOT (!) is not directly supported by Bing Text Search advanced operators. " +
173+ "Consider restructuring your filter to use positive conditions." ) ;
174+
175+ case BinaryExpression binaryExpr when binaryExpr . NodeType == ExpressionType . Equal :
176+ // Handle equality: page => page.Language == "en"
177+ ProcessEqualityExpression ( binaryExpr , filter , isNegated : false ) ;
178+ break ;
179+
180+ case BinaryExpression binaryExpr when binaryExpr . NodeType == ExpressionType . NotEqual :
181+ // Handle inequality: page => page.Language != "en"
182+ // Implemented via Bing's negation syntax (e.g., -language:en)
183+ ProcessEqualityExpression ( binaryExpr , filter , isNegated : true ) ;
184+ break ;
185+
186+ case MethodCallExpression methodExpr when methodExpr . Method . Name == "Contains" :
187+ // Handle Contains: page => page.Name.Contains("Microsoft")
188+ ProcessContainsExpression ( methodExpr , filter ) ;
189+ break ;
190+
191+ default :
192+ throw new NotSupportedException (
193+ $ "Expression type '{ expression . NodeType } ' is not supported for Bing API filters. " +
194+ "Supported patterns: equality (==), inequality (!=), Contains(), and logical AND (&&). " +
195+ "Available Bing operators: " + string . Join ( ", " , s_advancedSearchKeywords ) ) ;
196+ }
197+ }
198+
199+ /// <summary>
200+ /// Processes equality and inequality expressions (property == value or property != value).
201+ /// </summary>
202+ /// <param name="binaryExpr">The binary expression to process.</param>
203+ /// <param name="filter">The filter to update.</param>
204+ /// <param name="isNegated">True if this is an inequality (!=) expression.</param>
205+ private static void ProcessEqualityExpression ( BinaryExpression binaryExpr , TextSearchFilter filter , bool isNegated )
206+ {
207+ if ( binaryExpr . Left is MemberExpression memberExpr && binaryExpr . Right is ConstantExpression constExpr )
145208 {
146- // Handle simple equality: record.PropertyName == "value"
147- if ( binaryExpr . Left is MemberExpression memberExpr && binaryExpr . Right is ConstantExpression constExpr )
209+ string propertyName = memberExpr . Member . Name ;
210+ object ? value = constExpr . Value ;
211+
212+ string ? bingFilterName = MapPropertyToBingFilter ( propertyName ) ;
213+ if ( bingFilterName != null && value != null )
214+ {
215+ if ( isNegated )
216+ {
217+ // For inequality, use Bing's negation syntax by prepending '-' to the filter name
218+ // Example: -language:en excludes pages in English
219+ filter . Equality ( $ "-{ bingFilterName } ", value ) ;
220+ }
221+ else
222+ {
223+ filter . Equality ( bingFilterName , value ) ;
224+ }
225+ }
226+ else if ( value == null )
227+ {
228+ throw new NotSupportedException (
229+ $ "Null values are not supported in Bing API filters for property '{ propertyName } '.") ;
230+ }
231+ else
232+ {
233+ throw new NotSupportedException (
234+ $ "Property '{ propertyName } ' cannot be mapped to Bing API filters. " +
235+ "Supported properties: Language, Url, DisplayUrl, Name, Snippet, IsFamilyFriendly." ) ;
236+ }
237+ }
238+ else
239+ {
240+ throw new NotSupportedException (
241+ "Equality expressions must be in the form 'property == value' or 'property != value'. " +
242+ "Complex expressions on the left or right side are not supported." ) ;
243+ }
244+ }
245+
246+ /// <summary>
247+ /// Processes Contains() method calls on string properties.
248+ /// Maps to Bing's advanced search operators like intitle:, inbody:, url:.
249+ /// </summary>
250+ private static void ProcessContainsExpression ( MethodCallExpression methodExpr , TextSearchFilter filter )
251+ {
252+ // Contains can be called on a property: page.Name.Contains("value")
253+ // or on a collection: page.Tags.Contains("value")
254+
255+ if ( methodExpr . Object is MemberExpression memberExpr )
256+ {
257+ string propertyName = memberExpr . Member . Name ;
258+
259+ // Extract the search value from the Contains() argument
260+ if ( methodExpr . Arguments . Count == 1 && methodExpr . Arguments [ 0 ] is ConstantExpression constExpr )
148261 {
149- string propertyName = memberExpr . Member . Name ;
150262 object ? value = constExpr . Value ;
263+ if ( value == null )
264+ {
265+ return ; // Skip null values
266+ }
151267
152- // Map BingWebPage properties to Bing API filter names
153- string ? bingFilterName = MapPropertyToBingFilter ( propertyName ) ;
154- if ( bingFilterName != null && value != null )
268+ // Map property to Bing filter with Contains semantic
269+ string ? bingFilterOperator = MapPropertyToContainsFilter ( propertyName ) ;
270+ if ( bingFilterOperator != null )
271+ {
272+ // Use Bing's advanced search syntax: intitle:"value", inbody:"value", etc.
273+ filter . Equality ( bingFilterOperator , value ) ;
274+ }
275+ else
155276 {
156- return new TextSearchFilter ( ) . Equality ( bingFilterName , value ) ;
277+ throw new NotSupportedException (
278+ $ "Contains() on property '{ propertyName } ' is not supported by Bing API filters. " +
279+ "Supported properties for Contains: Name (maps to intitle:), Snippet (maps to inbody:), Url (maps to url:)." ) ;
157280 }
158281 }
282+ else
283+ {
284+ throw new NotSupportedException (
285+ "Contains() must have a single constant value argument. " +
286+ "Complex expressions as arguments are not supported." ) ;
287+ }
288+ }
289+ else
290+ {
291+ throw new NotSupportedException (
292+ "Contains() must be called on a property (e.g., page.Name.Contains(\" value\" )). " +
293+ "Collection Contains patterns are not yet supported." ) ;
159294 }
160-
161- throw new NotSupportedException (
162- "LINQ expression '" + linqExpression + "' cannot be converted to Bing API filters. " +
163- "Only simple equality expressions like 'page => page.Language == \" en\" ' are supported, " +
164- "and only for properties that map to Bing API parameters: " +
165- string . Join ( ", " , s_queryParameters . Concat ( s_advancedSearchKeywords ) ) ) ;
166295 }
167296
168297 /// <summary>
169- /// Maps BingWebPage property names to Bing API filter field names.
298+ /// Maps BingWebPage property names to Bing API filter field names for equality operations .
170299 /// </summary>
171300 /// <param name="propertyName">The BingWebPage property name.</param>
172301 /// <returns>The corresponding Bing API filter name, or null if not mappable.</returns>
@@ -180,16 +309,35 @@ private static TextSearchFilter ConvertLinqExpressionToBingFilter<TRecord>(Expre
180309 "DISPLAYURL" => "site" , // Maps to site: search
181310 "NAME" => "intitle" , // Maps to title search
182311 "SNIPPET" => "inbody" , // Maps to body content search
312+ "ISFAMILYFRIENDLY" => "safeSearch" , // Maps to safe search parameter
183313
184314 // Direct API parameters (if we ever extend BingWebPage with metadata)
185315 "MKT" => "mkt" , // Market/locale
186316 "FRESHNESS" => "freshness" , // Date freshness
187- "SAFESEARCH" => "safeSearch" , // Safe search setting
188317
189318 _ => null // Property not mappable to Bing filters
190319 } ;
191320 }
192321
322+ /// <summary>
323+ /// Maps BingWebPage property names to Bing API advanced search operators for Contains operations.
324+ /// </summary>
325+ /// <param name="propertyName">The BingWebPage property name.</param>
326+ /// <returns>The corresponding Bing advanced search operator, or null if not mappable.</returns>
327+ private static string ? MapPropertyToContainsFilter ( string propertyName )
328+ {
329+ return propertyName . ToUpperInvariant ( ) switch
330+ {
331+ // Map properties to Bing's contains-style operators
332+ "NAME" => "intitle" , // intitle:"search term" - title contains
333+ "SNIPPET" => "inbody" , // inbody:"search term" - body contains
334+ "URL" => "url" , // url:"search term" - URL contains
335+ "DISPLAYURL" => "site" , // site:domain.com - site contains
336+
337+ _ => null // Property not mappable to Contains-style filters
338+ } ;
339+ }
340+
193341 /// <summary>
194342 /// Execute a Bing search query and return the results.
195343 /// </summary>
0 commit comments