@@ -186,7 +186,10 @@ private static TextSearchFilter ConvertLinqExpressionToBraveFilter<TRecord>(Expr
186186 }
187187 else
188188 {
189- throw new NotSupportedException ( $ "Property '{ equalityClause . FieldName } ' cannot be mapped to Brave API filters. Supported properties: { string . Join ( ", " , s_queryParameters ) } ") ;
189+ throw new NotSupportedException (
190+ $ "Property '{ equalityClause . FieldName } ' cannot be mapped to Brave API filters. " +
191+ $ "Supported properties: { string . Join ( ", " , s_queryParameters ) } . " +
192+ "Example: page => page.Country == \" US\" && page.SafeSearch == \" moderate\" " ) ;
190193 }
191194 }
192195 }
@@ -202,18 +205,21 @@ private static TextSearchFilter ConvertLinqExpressionToBraveFilter<TRecord>(Expr
202205 private static string ? MapPropertyToBraveFilter ( string propertyName ) =>
203206 propertyName . ToUpperInvariant ( ) switch
204207 {
205- "COUNTRY" => "country" ,
206- "SEARCHLANG" => "search_lang" ,
207- "UILANG" => "ui_lang" ,
208- "SAFESEARCH" => "safesearch" ,
209- "TEXTDECORATIONS" => "text_decorations" ,
210- "SPELLCHECK" => "spellcheck" ,
211- "RESULTFILTER" => "result_filter" ,
212- "UNITS" => "units" ,
213- "EXTRASNIPPETS" => "extra_snippets" ,
208+ "COUNTRY" => BraveParamCountry ,
209+ "SEARCHLANG" => BraveParamSearchLang ,
210+ "UILANG" => BraveParamUiLang ,
211+ "SAFESEARCH" => BraveParamSafeSearch ,
212+ "TEXTDECORATIONS" => BraveParamTextDecorations ,
213+ "SPELLCHECK" => BraveParamSpellCheck ,
214+ "RESULTFILTER" => BraveParamResultFilter ,
215+ "UNITS" => BraveParamUnits ,
216+ "EXTRASNIPPETS" => BraveParamExtraSnippets ,
214217 _ => null // Property not mappable to Brave filters
215218 } ;
216219
220+ // TODO: Consider extracting LINQ expression analysis logic to a shared utility class
221+ // to reduce duplication across text search connectors (Brave, Tavily, etc.).
222+ // See code review for details.
217223 /// <summary>
218224 /// Analyzes a LINQ expression and extracts filter clauses.
219225 /// </summary>
@@ -230,17 +236,35 @@ private static void AnalyzeExpression(Expression expression, List<FilterClause>
230236 AnalyzeExpression ( binaryExpr . Left , filterClauses ) ;
231237 AnalyzeExpression ( binaryExpr . Right , filterClauses ) ;
232238 }
239+ else if ( binaryExpr . NodeType == ExpressionType . OrElse )
240+ {
241+ // Handle OR expressions by recursively analyzing both sides
242+ // Note: OR results in multiple filter values for the same property
243+ AnalyzeExpression ( binaryExpr . Left , filterClauses ) ;
244+ AnalyzeExpression ( binaryExpr . Right , filterClauses ) ;
245+ }
233246 else if ( binaryExpr . NodeType == ExpressionType . Equal )
234247 {
235248 // Handle equality expressions
236249 ExtractEqualityClause ( binaryExpr , filterClauses ) ;
237250 }
251+ else if ( binaryExpr . NodeType == ExpressionType . NotEqual )
252+ {
253+ // Handle inequality expressions (property != value)
254+ // This is supported as a negation pattern
255+ ExtractInequalityClause ( binaryExpr , filterClauses ) ;
256+ }
238257 else
239258 {
240- throw new NotSupportedException ( $ "Binary expression type '{ binaryExpr . NodeType } ' is not supported. Only AndAlso and Equal are supported .") ;
259+ throw new NotSupportedException ( $ "Binary expression type '{ binaryExpr . NodeType } ' is not supported. Supported operators: AndAlso (&&), OrElse (||), Equal (==), NotEqual (!=) .") ;
241260 }
242261 break ;
243262
263+ case UnaryExpression unaryExpr when unaryExpr . NodeType == ExpressionType . Not :
264+ // Handle NOT expressions (negation)
265+ AnalyzeNotExpression ( unaryExpr , filterClauses ) ;
266+ break ;
267+
244268 case MethodCallExpression methodCall :
245269 // Handle method calls like Contains, StartsWith, etc.
246270 ExtractMethodCallClause ( methodCall , filterClauses ) ;
@@ -283,34 +307,127 @@ private static void ExtractEqualityClause(BinaryExpression binaryExpr, List<Filt
283307 }
284308 }
285309
310+ /// <summary>
311+ /// Extracts an inequality filter clause from a binary not-equal expression.
312+ /// </summary>
313+ /// <param name="binaryExpr">The binary not-equal expression.</param>
314+ /// <param name="filterClauses">The list to add the extracted clause to.</param>
315+ private static void ExtractInequalityClause ( BinaryExpression binaryExpr , List < FilterClause > filterClauses )
316+ {
317+ // Note: Inequality is tracked but handled differently depending on the property
318+ // For now, we log a warning that inequality filtering may not work as expected
319+ string ? propertyName = null ;
320+ object ? value = null ;
321+
322+ if ( binaryExpr . Left is MemberExpression leftMember )
323+ {
324+ propertyName = leftMember . Member . Name ;
325+ value = ExtractValue ( binaryExpr . Right ) ;
326+ }
327+ else if ( binaryExpr . Right is MemberExpression rightMember )
328+ {
329+ propertyName = rightMember . Member . Name ;
330+ value = ExtractValue ( binaryExpr . Left ) ;
331+ }
332+
333+ if ( propertyName != null && value != null )
334+ {
335+ // Add a marker for inequality - this will need special handling in conversion
336+ // For now, we don't add it to filter clauses as Brave API doesn't support direct negation
337+ throw new NotSupportedException ( $ "Inequality operator (!=) is not directly supported for property '{ propertyName } '. Use NOT operator instead: !(page.{ propertyName } == value).") ;
338+ }
339+
340+ throw new NotSupportedException ( "Unable to extract property name and value from inequality expression." ) ;
341+ }
342+
343+ /// <summary>
344+ /// Analyzes a NOT (negation) expression.
345+ /// </summary>
346+ /// <param name="unaryExpr">The unary NOT expression.</param>
347+ /// <param name="filterClauses">The list to add extracted filter clauses to.</param>
348+ private static void AnalyzeNotExpression ( UnaryExpression unaryExpr , List < FilterClause > filterClauses )
349+ {
350+ // NOT expressions are complex for web search APIs
351+ // We support simple cases like !(page.SafeSearch == "off")
352+ if ( unaryExpr . Operand is BinaryExpression binaryExpr && binaryExpr . NodeType == ExpressionType . Equal )
353+ {
354+ // This is !(property == value), which we can handle for some properties
355+ throw new NotSupportedException ( "NOT operator (!) with equality is not directly supported. Most web search APIs don't support negative filtering." ) ;
356+ }
357+
358+ throw new NotSupportedException ( "NOT operator (!) is only supported with simple equality expressions." ) ;
359+ }
360+
286361 /// <summary>
287362 /// Extracts a filter clause from a method call expression (e.g., Contains, StartsWith).
288363 /// </summary>
289364 /// <param name="methodCall">The method call expression.</param>
290365 /// <param name="filterClauses">The list to add the extracted clause to.</param>
291366 private static void ExtractMethodCallClause ( MethodCallExpression methodCall , List < FilterClause > filterClauses )
292367 {
293- if ( methodCall . Method . Name == "Contains" && methodCall . Object is MemberExpression member )
368+ if ( methodCall . Method . Name == "Contains" )
294369 {
295- var propertyName = member . Member . Name ;
296- var value = ExtractValue ( methodCall . Arguments [ 0 ] ) ;
370+ // Check if this is property.Contains(value) or array.Contains(property)
371+ if ( methodCall . Object is MemberExpression member )
372+ {
373+ // This is property.Contains(value) - e.g., page.ResultFilter.Contains("web")
374+ var propertyName = member . Member . Name ;
375+ var value = ExtractValue ( methodCall . Arguments [ 0 ] ) ;
297376
298- if ( value != null )
377+ if ( value != null )
378+ {
379+ // For Contains, we'll map it to equality for certain properties
380+ if ( propertyName . Equals ( "ResultFilter" , StringComparison . OrdinalIgnoreCase ) )
381+ {
382+ filterClauses . Add ( new EqualToFilterClause ( propertyName , value ) ) ;
383+ }
384+ else
385+ {
386+ throw new NotSupportedException ( $ "Contains method is only supported for ResultFilter property, not '{ propertyName } '.") ;
387+ }
388+ }
389+ }
390+ else if ( methodCall . Object == null && methodCall . Arguments . Count == 2 )
299391 {
300- // For Contains, we'll map it to equality for certain properties
301- if ( propertyName . Equals ( "ResultFilter" , StringComparison . OrdinalIgnoreCase ) )
392+ // This is array.Contains(property) - e.g., new[] { "US", "GB" }.Contains(page.Country)
393+ // This is an extension method call where the first argument is the array
394+ var arrayExpr = methodCall . Arguments [ 0 ] ;
395+ var propertyExpr = methodCall . Arguments [ 1 ] ;
396+
397+ if ( propertyExpr is MemberExpression propertyMember )
302398 {
303- filterClauses . Add ( new EqualToFilterClause ( propertyName , value ) ) ;
399+ var propertyName = propertyMember . Member . Name ;
400+ var arrayValue = ExtractValue ( arrayExpr ) ;
401+
402+ if ( arrayValue is System . Collections . IEnumerable enumerable )
403+ {
404+ // Convert to OR expressions - each value becomes an equality clause
405+ foreach ( var value in enumerable )
406+ {
407+ if ( value != null )
408+ {
409+ filterClauses . Add ( new EqualToFilterClause ( propertyName , value ) ) ;
410+ }
411+ }
412+ }
413+ else
414+ {
415+ throw new NotSupportedException ( $ "Contains argument must be an array or collection, got: { arrayValue ? . GetType ( ) . Name } ") ;
416+ }
304417 }
305418 else
306419 {
307- throw new NotSupportedException ( $ "Contains method is only supported for ResultFilter property, not ' { propertyName } ' .") ;
420+ throw new NotSupportedException ( "Contains with inline collection requires a property reference as the second argument ." ) ;
308421 }
309422 }
423+ else
424+ {
425+ throw new NotSupportedException ( "Unsupported Contains expression format." ) ;
426+ }
310427 }
311428 else
312429 {
313- throw new NotSupportedException ( $ "Method '{ methodCall . Method . Name } ' is not supported in Brave search filters.") ;
430+ throw new NotSupportedException ( $ "Method '{ methodCall . Method . Name } ' is not supported in Brave search filters. Only 'Contains' is supported. ") ;
314431 }
315432 }
316433
@@ -349,8 +466,19 @@ private static void ExtractMethodCallClause(MethodCallExpression methodCall, Lis
349466 private static readonly ITextSearchStringMapper s_defaultStringMapper = new DefaultTextSearchStringMapper ( ) ;
350467 private static readonly ITextSearchResultMapper s_defaultResultMapper = new DefaultTextSearchResultMapper ( ) ;
351468
469+ // Constants for Brave API parameter names
470+ private const string BraveParamCountry = "country" ;
471+ private const string BraveParamSearchLang = "search_lang" ;
472+ private const string BraveParamUiLang = "ui_lang" ;
473+ private const string BraveParamSafeSearch = "safesearch" ;
474+ private const string BraveParamTextDecorations = "text_decorations" ;
475+ private const string BraveParamSpellCheck = "spellcheck" ;
476+ private const string BraveParamResultFilter = "result_filter" ;
477+ private const string BraveParamUnits = "units" ;
478+ private const string BraveParamExtraSnippets = "extra_snippets" ;
479+
352480 // See https://api-dashboard.search.brave.com/app/documentation/web-search/query#WebSearchAPIQueryParameters
353- private static readonly string [ ] s_queryParameters = [ "country" , "search_lang" , "ui_lang" , "safesearch" , "text_decorations" , "spellcheck" , "result_filter" , "units" , "extra_snippets" ] ;
481+ private static readonly string [ ] s_queryParameters = [ BraveParamCountry , BraveParamSearchLang , BraveParamUiLang , BraveParamSafeSearch , BraveParamTextDecorations , BraveParamSpellCheck , BraveParamResultFilter , BraveParamUnits , BraveParamExtraSnippets ] ;
354482
355483 private static readonly string [ ] s_safeSearch = [ "off" , "moderate" , "strict" ] ;
356484
0 commit comments