Skip to content

Commit cb30a62

Browse files
authored
Merge pull request #336 from delegateas/linkcriteria-1d7
Fix issue with multiple top level links in retrievemultiple
2 parents e92c992 + c739f81 commit cb30a62

2 files changed

Lines changed: 135 additions & 18 deletions

File tree

src/XrmMockup365/Requests/RetrieveMultipleRequestHandler.cs

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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);

tests/XrmMockup365Test/TestRetrieveMultiple.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,83 @@ public void TestQueryExpressionLinkEntity()
871871
Assert.Equal(2, res.Count());
872872
}
873873

874+
[Fact]
875+
public void TestQueryExpressionMultipleLinkEntitiesAreAndJoined()
876+
{
877+
// Two sibling links, each with its own LinkCriteria, must be combined with AND
878+
// (cross join) semantics like real Dataverse. Previously each link was evaluated
879+
// independently and the results unioned, so a parent matching only one link still
880+
// came back.
881+
var accA = new Account { Name = "ReproA", Address1_City = "ReproCityA" };
882+
var accB = new Account { Name = "ReproB", Address1_City = "ReproCityB" };
883+
accA.Id = orgAdminService.Create(accA);
884+
accB.Id = orgAdminService.Create(accB);
885+
886+
var conX = new Contact { LastName = "ReproX" };
887+
var conY = new Contact { LastName = "ReproY" };
888+
conX.Id = orgAdminService.Create(conX);
889+
conY.Id = orgAdminService.Create(conY);
890+
891+
// pAX matches both link criteria; pAY matches only the account link; pBX only the contact link.
892+
var pAX = new ctx_parent { ctx_Name = "pAX", ctx_AccountId = accA.ToEntityReference(), ctx_ContactId = conX.ToEntityReference() };
893+
var pAY = new ctx_parent { ctx_Name = "pAY", ctx_AccountId = accA.ToEntityReference(), ctx_ContactId = conY.ToEntityReference() };
894+
var pBX = new ctx_parent { ctx_Name = "pBX", ctx_AccountId = accB.ToEntityReference(), ctx_ContactId = conX.ToEntityReference() };
895+
pAX.Id = orgAdminService.Create(pAX);
896+
pAY.Id = orgAdminService.Create(pAY);
897+
pBX.Id = orgAdminService.Create(pBX);
898+
899+
var query = new QueryExpression("ctx_parent") { ColumnSet = new ColumnSet("ctx_name") };
900+
901+
var accountLink = query.AddLink("account", "ctx_accountid", "accountid");
902+
accountLink.EntityAlias = "acc";
903+
accountLink.Columns = new ColumnSet("address1_city");
904+
accountLink.LinkCriteria.AddCondition("address1_city", ConditionOperator.Equal, "ReproCityA");
905+
906+
var contactLink = query.AddLink("contact", "ctx_contactid", "contactid");
907+
contactLink.EntityAlias = "con";
908+
contactLink.Columns = new ColumnSet("lastname");
909+
contactLink.LinkCriteria.AddCondition("lastname", ConditionOperator.Equal, "ReproX");
910+
911+
var res = orgAdminService.RetrieveMultiple(query).Entities;
912+
913+
Assert.Single(res);
914+
var match = res[0];
915+
Assert.Equal(pAX.Id, match.Id);
916+
Assert.Equal("pAX", match.GetAttributeValue<string>("ctx_name"));
917+
// The cross-join must carry the aliased columns from BOTH links onto the single result row.
918+
Assert.Equal("ReproCityA", match.GetAttributeValue<AliasedValue>("acc.address1_city")?.Value);
919+
Assert.Equal("ReproX", match.GetAttributeValue<AliasedValue>("con.lastname")?.Value);
920+
}
921+
922+
[Fact]
923+
public void TestQueryExpressionSiblingLinkDoesNotBreakOtherLinkCriteria()
924+
{
925+
// Regression for the reported bug: adding a second sibling link must not relax the
926+
// first link's criteria. Here no parent satisfies both criteria simultaneously, so
927+
// the result must be empty (it previously returned the OR of the two links).
928+
var accA = new Account { Name = "ReproC", Address1_City = "ReproCityC" };
929+
accA.Id = orgAdminService.Create(accA);
930+
931+
var conY = new Contact { LastName = "ReproZ" };
932+
conY.Id = orgAdminService.Create(conY);
933+
934+
// Matches the account link only; its contact does not match the contact link.
935+
var p = new ctx_parent { ctx_Name = "pOnlyAccount", ctx_AccountId = accA.ToEntityReference(), ctx_ContactId = conY.ToEntityReference() };
936+
p.Id = orgAdminService.Create(p);
937+
938+
var query = new QueryExpression("ctx_parent") { ColumnSet = new ColumnSet("ctx_name") };
939+
940+
var accountLink = query.AddLink("account", "ctx_accountid", "accountid");
941+
accountLink.LinkCriteria.AddCondition("address1_city", ConditionOperator.Equal, "ReproCityC");
942+
943+
var contactLink = query.AddLink("contact", "ctx_contactid", "contactid");
944+
contactLink.LinkCriteria.AddCondition("lastname", ConditionOperator.Equal, "NoSuchLastName");
945+
946+
var res = orgAdminService.RetrieveMultiple(query).Entities;
947+
948+
Assert.Empty(res);
949+
}
950+
874951

875952
[Fact]
876953
public void TestQueryExpressionLinkEntityNoSetEntityNameAndAlias()

0 commit comments

Comments
 (0)