@@ -60,20 +60,25 @@ internal override OrganizationResponse Execute(OrganizationRequest orgRequest, E
6060
6161 if ( queryExpr . LinkEntities . Count > 0 )
6262 {
63- //foreach (var linkEntity in queryExpr.LinkEntities)
64- Parallel . ForEach ( queryExpr . LinkEntities , linkEntity =>
63+ // Every top-level link must be satisfied simultaneously (AND / cross join),
64+ // mirroring Dataverse inner-join semantics. Computing each link independently
65+ // and unioning the results would instead OR the links together, letting a
66+ // parent through if it matched any single link. Compute each link's aliased
67+ // results, then cross-join them so a parent is only returned when all links
68+ // match, with each result row carrying every link's aliased attributes.
69+ var perLinkResults = queryExpr . LinkEntities
70+ . Select ( linkEntity => GetAliasedValuesFromLinkentity ( linkEntity , entity , toAdd , db ) )
71+ . ToList ( ) ;
72+ var matchingValues = CombineLinkResults ( toAdd , perLinkResults )
73+ . Where ( e => EntityMatcher . MatchesCriteria ( e , queryExpr . Criteria ) ) ;
74+ foreach ( var m in matchingValues )
6575 {
66- var alliasedValues = GetAliasedValuesFromLinkentity ( linkEntity , entity , toAdd , db ) ;
67- var matchingValues = alliasedValues . Where ( e => EntityMatcher . MatchesCriteria ( e , queryExpr . Criteria ) ) ;
68- Parallel . ForEach ( matchingValues , m =>
76+ if ( security . HasPermission ( m , AccessRights . ReadAccess , userRef ) )
6977 {
70- if ( security . HasPermission ( m , AccessRights . ReadAccess , userRef ) )
71- {
72- Utility . SetFormattedValues ( db , m , entityMetadata ) ;
73- collection . Add ( new KeyValuePair < DbRow , Entity > ( row , m ) ) ;
74- }
75- } ) ;
76- } ) ;
78+ Utility . SetFormattedValues ( db , m , entityMetadata ) ;
79+ collection . Add ( new KeyValuePair < DbRow , Entity > ( row , m ) ) ;
80+ }
81+ }
7782 }
7883 else if ( EntityMatcher . MatchesCriteria ( toAdd , queryExpr . Criteria ) )
7984 {
@@ -182,16 +187,18 @@ private List<Entity> GetAliasedValuesFromLinkentity(LinkEntity linkEntity, Entit
182187
183188 if ( linkEntity . LinkEntities . Count > 0 )
184189 {
185- var subEntities = new List < Entity > ( ) ;
190+ // Multiple nested links under this link must all be satisfied (AND /
191+ // cross join), just like multiple top-level links. Unioning them would
192+ // OR the nested links together.
193+ var perNestedResults = new List < List < Entity > > ( ) ;
186194 foreach ( var nestedLinkEntity in linkEntity . LinkEntities )
187195 {
188196 nestedLinkEntity . LinkFromEntityName = linkEntity . LinkToEntityName ;
189- var alliasedLinkValues = GetAliasedValuesFromLinkentity (
190- nestedLinkEntity , linkedEntity , aliasedEntity , db ) ;
191- subEntities . AddRange ( alliasedLinkValues
192- . Where ( e => EntityMatcher . MatchesCriteria ( e , linkEntity . LinkCriteria ) ) ) ;
197+ perNestedResults . Add ( GetAliasedValuesFromLinkentity (
198+ nestedLinkEntity , linkedEntity , aliasedEntity , db ) ) ;
193199 }
194- collection . AddRange ( subEntities ) ;
200+ collection . AddRange ( CombineLinkResults ( aliasedEntity , perNestedResults )
201+ . Where ( e => EntityMatcher . MatchesCriteria ( e , linkEntity . LinkCriteria ) ) ) ;
195202 }
196203 else if ( EntityMatcher . MatchesCriteria ( aliasedEntity , linkEntity . LinkCriteria ) )
197204 {
@@ -228,6 +235,39 @@ private Entity GetEntityWithAliasAttributes(string alias, Entity toAdd, EntityMe
228235 return parentClone ;
229236 }
230237
238+ // Cross-joins the per-link aliased result lists for sibling links into a single set of
239+ // combined rows. Each combined row is a clone of the base entity carrying the aliased
240+ // attributes of one pick from every link. If any link produced no rows (an inner link
241+ // with no match), the product is empty and the parent is dropped; LeftOuter/NotAny links
242+ // already contribute the parent itself, so outer joins keep the parent.
243+ private List < Entity > CombineLinkResults ( Entity baseEntity , List < List < Entity > > perLinkResults )
244+ {
245+ IEnumerable < Entity > combined = new [ ] { baseEntity } ;
246+ foreach ( var linkResults in perLinkResults )
247+ {
248+ combined = combined
249+ . SelectMany ( acc => linkResults . Select ( linked => MergeAliasAttributes ( acc , linked ) ) )
250+ . ToList ( ) ;
251+ }
252+ return combined . ToList ( ) ;
253+ }
254+
255+ // Returns a clone of baseEntity (preserving any aliased attributes it already carries)
256+ // augmented with the aliased attributes from withAliases.
257+ private Entity MergeAliasAttributes ( Entity baseEntity , Entity withAliases )
258+ {
259+ var merged = core . GetStronglyTypedEntity ( baseEntity ,
260+ metadata . EntityMetadata . GetMetadata ( baseEntity . LogicalName ) , null ) ;
261+ foreach ( var attr in withAliases . Attributes . Where ( a => a . Key . Contains ( '.' ) ) )
262+ {
263+ if ( ! merged . Attributes . ContainsKey ( attr . Key ) )
264+ {
265+ merged . Attributes . Add ( attr . Key , attr . Value ) ;
266+ }
267+ }
268+ return merged ;
269+ }
270+
231271 private void KeepAttributesAndAliasAttributes ( Entity entity , ColumnSet toKeep )
232272 {
233273 var clone = entity . CloneEntity ( metadata . EntityMetadata . GetMetadata ( entity . LogicalName ) , toKeep ) ;
0 commit comments