From 4a7c752d75faafd4a1bbf410129a01f939581d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Gether=20S=C3=B8rensen=20=28Delegate=29?= Date: Mon, 15 Jun 2026 11:43:41 +0200 Subject: [PATCH] Fix: negative links did not handle null correctly --- src/XrmMockup365/Internal/EntityMatcher.cs | 7 ++ .../XrmMockup365Test/TestRetrieveMultiple.cs | 65 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/XrmMockup365/Internal/EntityMatcher.cs b/src/XrmMockup365/Internal/EntityMatcher.cs index f6964fb3..ab47c578 100644 --- a/src/XrmMockup365/Internal/EntityMatcher.cs +++ b/src/XrmMockup365/Internal/EntityMatcher.cs @@ -80,6 +80,8 @@ private static bool Matches(object attr, ConditionOperator op, IEnumerable value is unknown). + if (attr == null) return false; return !Matches(attr, ConditionOperator.Equal, values); case ConditionOperator.GreaterThan: @@ -89,6 +91,7 @@ private static bool Matches(object attr, ConditionOperator op, IEnumerable contact1) has Address1_PostalCode = "MK111DW"; lead2 (-> contact2) has none (null). + // A NotEqual link-criteria must exclude the contact whose linked lead has a null value, + // matching real Dataverse (NULL <> value is unknown -> row excluded). + var query = new QueryExpression("contact") + { + ColumnSet = new ColumnSet("lastname") + }; + query.Criteria = new FilterExpression(LogicalOperator.And); + query.Criteria.AddCondition(new ConditionExpression("lastname", ConditionOperator.Like, "contact%")); + + var linkEntity = new LinkEntity() + { + LinkToEntityName = "lead", + LinkToAttributeName = "parentcontactid", + LinkFromEntityName = "contact", + LinkFromAttributeName = "contactid", + Columns = new ColumnSet("address1_postalcode"), + EntityAlias = "lead" + }; + linkEntity.LinkCriteria = new FilterExpression(LogicalOperator.And); + linkEntity.LinkCriteria.AddCondition(new ConditionExpression("lead", "address1_postalcode", ConditionOperator.NotEqual, "ZZZ")); + query.LinkEntities.Add(linkEntity); + + var res = orgAdminService.RetrieveMultiple(query).Entities; + Assert.Single(res); + Assert.Equal("contact1", res[0].GetAttributeValue("lastname")); + } + + [Fact] + public void TestQueryExpressionNotEqualExcludesNull() + { + // Only lead1 has a postal code; leads 2-4 are null. NotEqual must exclude the null rows. + var query = new QueryExpression("lead") { ColumnSet = new ColumnSet(true) }; + query.Criteria.AddCondition(new ConditionExpression("address1_postalcode", ConditionOperator.NotEqual, "ZZZ")); + + var res = orgAdminService.RetrieveMultiple(query).Entities.Cast().ToList(); + Assert.Single(res); + Assert.Equal(lead1.Id, res[0].Id); + } + + [Fact] + public void TestQueryExpressionNotInExcludesNull() + { + var query = new QueryExpression("lead") { ColumnSet = new ColumnSet(true) }; + query.Criteria.AddCondition(new ConditionExpression("address1_postalcode", ConditionOperator.NotIn, new[] { "AAA", "BBB" })); + + var res = orgAdminService.RetrieveMultiple(query).Entities.Cast().ToList(); + Assert.Single(res); + Assert.Equal(lead1.Id, res[0].Id); + } + + [Fact] + public void TestQueryExpressionDoesNotBeginWithExcludesNull() + { + var query = new QueryExpression("lead") { ColumnSet = new ColumnSet(true) }; + query.Criteria.AddCondition(new ConditionExpression("address1_postalcode", ConditionOperator.DoesNotBeginWith, "ZZ")); + + var res = orgAdminService.RetrieveMultiple(query).Entities.Cast().ToList(); + Assert.Single(res); + Assert.Equal(lead1.Id, res[0].Id); + } + [Fact] public void TestQueryExpressionLinkEntityNoSetEntityName() {