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