@@ -323,13 +323,62 @@ private bool HasNestedPropertyInRuleGroup(RuleGroup? ruleGroup, string navigatio
323323 {
324324 var expressions = new List < Expression < Func < Contact , bool > > > ( ) ;
325325
326- // Process individual rules
327- foreach ( var rule in ruleGroup . Rules )
326+ // When using AND connector, group rules by top-level collection navigation so that
327+ // multiple conditions on the same collection (e.g., emailLogs.status AND emailLogs.createdAt)
328+ // are evaluated against the same record via a single .Any() call.
329+ if ( ruleGroup . Connector == RuleConnector . And )
328330 {
329- var ruleExpression = BuildRuleExpression ( rule ) ;
330- if ( ruleExpression != null )
331+ var collectionRuleGroups = new Dictionary < string , List < SegmentRule > > ( StringComparer . OrdinalIgnoreCase ) ;
332+ var otherRules = new List < SegmentRule > ( ) ;
333+
334+ foreach ( var rule in ruleGroup . Rules )
335+ {
336+ var segments = rule . FieldId . Split ( '.' ) ;
337+ if ( segments . Length >= 2 && ContactCollectionNavigations . ContainsKey ( segments [ 0 ] ) )
338+ {
339+ if ( ! collectionRuleGroups . TryGetValue ( segments [ 0 ] , out var group ) )
340+ {
341+ group = new List < SegmentRule > ( ) ;
342+ collectionRuleGroups [ segments [ 0 ] ] = group ;
343+ }
344+
345+ group . Add ( rule ) ;
346+ }
347+ else
348+ {
349+ otherRules . Add ( rule ) ;
350+ }
351+ }
352+
353+ foreach ( var ( collectionKey , rules ) in collectionRuleGroups )
354+ {
355+ var navInfo = ContactCollectionNavigations [ collectionKey ] ;
356+ var expr = BuildCombinedCollectionExpression ( navInfo , rules ) ;
357+ if ( expr != null )
358+ {
359+ expressions . Add ( expr ) ;
360+ }
361+ }
362+
363+ foreach ( var rule in otherRules )
364+ {
365+ var ruleExpression = BuildRuleExpression ( rule ) ;
366+ if ( ruleExpression != null )
367+ {
368+ expressions . Add ( ruleExpression ) ;
369+ }
370+ }
371+ }
372+ else
373+ {
374+ // For OR connector, process rules individually (separate .Any() calls are semantically equivalent)
375+ foreach ( var rule in ruleGroup . Rules )
331376 {
332- expressions . Add ( ruleExpression ) ;
377+ var ruleExpression = BuildRuleExpression ( rule ) ;
378+ if ( ruleExpression != null )
379+ {
380+ expressions . Add ( ruleExpression ) ;
381+ }
333382 }
334383 }
335384
@@ -594,13 +643,56 @@ private Expression BuildArrayIsNotEmptyExpression(Expression property)
594643 }
595644
596645 /// <summary>
597- /// Recursively builds nested Any() expressions for collection navigation paths.
598- /// E.g., for "orders.orderItems.productName" with Contains("Automation"):
599- /// c.Orders.Any(o => o.OrderItems.Any(oi => oi.ProductName.ToLower().Contains("automation"))) .
646+ /// Builds a single . Any() expression that combines inner predicates from multiple rules
647+ /// targeting the same collection navigation with AND, ensuring all conditions are evaluated
648+ /// against the same collection element .
600649 /// </summary>
601- private Expression ? BuildCollectionAnyExpression (
602- Expression parentExpression ,
603- string collectionPropertyName ,
650+ private Expression < Func < Contact , bool > > ? BuildCombinedCollectionExpression (
651+ ( string PropertyName , Type ElementType ) navInfo ,
652+ List < SegmentRule > rules )
653+ {
654+ var contactParam = Expression . Parameter ( typeof ( Contact ) , "c" ) ;
655+ var innerParam = Expression . Parameter ( navInfo . ElementType , navInfo . ElementType . Name [ 0 ..1 ] . ToLower ( ) ) ;
656+ Expression ? combinedPredicate = null ;
657+
658+ foreach ( var rule in rules )
659+ {
660+ var segments = rule . FieldId . Split ( '.' ) ;
661+ var remainingPath = segments . Skip ( 1 ) . ToArray ( ) ;
662+
663+ var predicate = BuildCollectionInnerPredicate ( innerParam , navInfo . ElementType , remainingPath , rule ) ;
664+ if ( predicate == null )
665+ {
666+ continue ;
667+ }
668+
669+ combinedPredicate = combinedPredicate == null
670+ ? predicate
671+ : Expression . AndAlso ( combinedPredicate , predicate ) ;
672+ }
673+
674+ if ( combinedPredicate == null )
675+ {
676+ return null ;
677+ }
678+
679+ var innerLambda = Expression . Lambda ( combinedPredicate , innerParam ) ;
680+ var collectionProperty = Expression . Property ( contactParam , navInfo . PropertyName ) ;
681+
682+ var anyMethod = typeof ( Enumerable ) . GetMethods ( )
683+ . First ( m => m . Name == "Any" && m . GetParameters ( ) . Length == 2 )
684+ . MakeGenericMethod ( navInfo . ElementType ) ;
685+
686+ var anyCall = Expression . Call ( anyMethod , collectionProperty , innerLambda ) ;
687+ return Expression . Lambda < Func < Contact , bool > > ( anyCall , contactParam ) ;
688+ }
689+
690+ /// <summary>
691+ /// Builds the inner predicate for a single rule within a collection element context.
692+ /// This handles both leaf properties and sub-collection navigations.
693+ /// </summary>
694+ private Expression ? BuildCollectionInnerPredicate (
695+ ParameterExpression elementParam ,
604696 Type elementType ,
605697 string [ ] remainingPath ,
606698 SegmentRule rule )
@@ -610,12 +702,8 @@ private Expression BuildArrayIsNotEmptyExpression(Expression property)
610702 return null ;
611703 }
612704
613- var innerParam = Expression . Parameter ( elementType , elementType . Name [ 0 ..1 ] . ToLower ( ) ) ;
614- Expression ? innerPredicate ;
615-
616705 if ( remainingPath . Length == 1 )
617706 {
618- // Leaf property — apply the operator
619707 var leafPropertyName = GetMappedPropertyName ( elementType , remainingPath [ 0 ] ) ;
620708 if ( leafPropertyName == null )
621709 {
@@ -624,33 +712,50 @@ private Expression BuildArrayIsNotEmptyExpression(Expression property)
624712
625713 try
626714 {
627- var leafProperty = Expression . Property ( innerParam , leafPropertyName ) ;
628- innerPredicate = ApplyOperator ( leafProperty , rule ) ;
715+ var leafProperty = Expression . Property ( elementParam , leafPropertyName ) ;
716+ return ApplyOperator ( leafProperty , rule ) ;
629717 }
630718 catch ( ArgumentException )
631719 {
632720 return null ;
633721 }
634722 }
635- else
723+
724+ // Sub-collection navigation (e.g., orderItems on Order)
725+ if ( SubCollectionNavigations . TryGetValue ( elementType . Name , out var subNavs ) &&
726+ subNavs . TryGetValue ( remainingPath [ 0 ] , out var subNavInfo ) )
636727 {
637- // Check for sub-collection navigation (e.g., orderItems on Order)
638- if ( SubCollectionNavigations . TryGetValue ( elementType . Name , out var subNavs ) &&
639- subNavs . TryGetValue ( remainingPath [ 0 ] , out var subNavInfo ) )
640- {
641- innerPredicate = BuildCollectionAnyExpression (
642- innerParam ,
643- subNavInfo . PropertyName ,
644- subNavInfo . ElementType ,
645- remainingPath . Skip ( 1 ) . ToArray ( ) ,
646- rule ) ;
647- }
648- else
649- {
650- return null ;
651- }
728+ return BuildCollectionAnyExpression (
729+ elementParam ,
730+ subNavInfo . PropertyName ,
731+ subNavInfo . ElementType ,
732+ remainingPath . Skip ( 1 ) . ToArray ( ) ,
733+ rule ) ;
652734 }
653735
736+ return null ;
737+ }
738+
739+ /// <summary>
740+ /// Recursively builds nested Any() expressions for collection navigation paths.
741+ /// E.g., for "orders.orderItems.productName" with Contains("Automation"):
742+ /// c.Orders.Any(o => o.OrderItems.Any(oi => oi.ProductName.ToLower().Contains("automation"))).
743+ /// </summary>
744+ private Expression ? BuildCollectionAnyExpression (
745+ Expression parentExpression ,
746+ string collectionPropertyName ,
747+ Type elementType ,
748+ string [ ] remainingPath ,
749+ SegmentRule rule )
750+ {
751+ if ( remainingPath . Length == 0 )
752+ {
753+ return null ;
754+ }
755+
756+ var innerParam = Expression . Parameter ( elementType , elementType . Name [ 0 ..1 ] . ToLower ( ) ) ;
757+ var innerPredicate = BuildCollectionInnerPredicate ( innerParam , elementType , remainingPath , rule ) ;
758+
654759 if ( innerPredicate == null )
655760 {
656761 return null ;
0 commit comments