From 954af4aa940e8842e765602e286c78798367abc4 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Mon, 2 Sep 2024 08:50:21 +0100 Subject: [PATCH 1/2] Added support for and improved handling of no attributes --- .../FetchXmlConversionTests.cs | 25 +++++++++++++++---- .../FetchXmlToWebAPIConverter.cs | 8 ++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/MarkMpn.FetchXmlToWebAPI.Tests/FetchXmlConversionTests.cs b/MarkMpn.FetchXmlToWebAPI.Tests/FetchXmlConversionTests.cs index 8024728..e5bd72f 100644 --- a/MarkMpn.FetchXmlToWebAPI.Tests/FetchXmlConversionTests.cs +++ b/MarkMpn.FetchXmlToWebAPI.Tests/FetchXmlConversionTests.cs @@ -586,7 +586,7 @@ public void FilterAll() var odata = ConvertFetchToOData(fetch); - Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/accounts?$filter=(contact_customer_accounts/all(x1:(x1/firstname eq 'Mark')))", odata); + Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/accounts?$select=accountid&$filter=(contact_customer_accounts/all(x1:(x1/firstname eq 'Mark')))", odata); } [TestMethod] @@ -607,7 +607,7 @@ public void FilterAny() var odata = ConvertFetchToOData(fetch); - Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/accounts?$filter=(contact_customer_accounts/any(x1:(x1/firstname eq 'Mark')))", odata); + Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/accounts?$select=accountid&$filter=(contact_customer_accounts/any(x1:(x1/firstname eq 'Mark')))", odata); } [TestMethod] @@ -628,7 +628,7 @@ public void FilterNotAny() var odata = ConvertFetchToOData(fetch); - Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/accounts?$filter=(not contact_customer_accounts/any(x1:(x1/firstname ne 'Mark')))", odata); + Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/accounts?$select=accountid&$filter=(not contact_customer_accounts/any(x1:(x1/firstname ne 'Mark')))", odata); } [TestMethod] @@ -649,7 +649,7 @@ public void FilterNotAll() var odata = ConvertFetchToOData(fetch); - Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/accounts?$filter=(not contact_customer_accounts/all(x1:(x1/firstname ne 'Mark')))", odata); + Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/accounts?$select=accountid&$filter=(not contact_customer_accounts/all(x1:(x1/firstname ne 'Mark')))", odata); } [TestMethod] @@ -674,7 +674,22 @@ public void FilterNotAllNestedNotAny() var odata = ConvertFetchToOData(fetch); - Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/accounts?$filter=(not contact_customer_accounts/all(x1:(x1/account_primarycontact/any(x2:(x2/name eq 'Data8')))))", odata); + Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/accounts?$select=accountid&$filter=(not contact_customer_accounts/all(x1:(x1/account_primarycontact/any(x2:(x2/name eq 'Data8')))))", odata); + } + + [TestMethod] + public void SelectAllAttributes() + { + var fetch = @" + + + + + "; + + var odata = ConvertFetchToOData(fetch); + + Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/contacts", odata); } private string ConvertFetchToOData(string fetch) diff --git a/MarkMpn.FetchXmlToWebAPI/FetchXmlToWebAPIConverter.cs b/MarkMpn.FetchXmlToWebAPI/FetchXmlToWebAPIConverter.cs index 54157f5..dfed19a 100644 --- a/MarkMpn.FetchXmlToWebAPI/FetchXmlToWebAPIConverter.cs +++ b/MarkMpn.FetchXmlToWebAPI/FetchXmlToWebAPIConverter.cs @@ -362,10 +362,18 @@ private IEnumerable ConvertJoins(string entityName, object[] it private IEnumerable ConvertSelect(string entityName, object[] items) { + // A missing $select is equivalent to selecting all attributes + if (items.OfType().Any()) + return Array.Empty(); + var attributeitems = items .OfType() .Where(i => i.name != null); + // If we don't want to select any attributes, just include the primary key + if (!attributeitems.Any()) + return new[] { _metadata.GetEntity(entityName).PrimaryIdAttribute }; + return GetAttributeNames(entityName, attributeitems); } From bccd6ab86b25e7bc6fa23ea3339b6e929dd1e0b4 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:28:33 +0000 Subject: [PATCH 2/2] Handle many-to-many links with null children --- .../FetchXmlConversionTests.cs | 71 +++++++++++++++++++ .../FetchXmlToWebAPIConverter.cs | 22 +++--- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/MarkMpn.FetchXmlToWebAPI.Tests/FetchXmlConversionTests.cs b/MarkMpn.FetchXmlToWebAPI.Tests/FetchXmlConversionTests.cs index e5bd72f..9069c10 100644 --- a/MarkMpn.FetchXmlToWebAPI.Tests/FetchXmlConversionTests.cs +++ b/MarkMpn.FetchXmlToWebAPI.Tests/FetchXmlConversionTests.cs @@ -692,6 +692,23 @@ public void SelectAllAttributes() Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/contacts", odata); } + [TestMethod] + public void InnerJoinManyToManyWithNoChildren() + { + var fetch = @" + + + + + + + "; + + var odata = ConvertFetchToOData(fetch); + + Assert.AreEqual("https://example.crm.dynamics.com/api/data/v9.0/contacts?$select=contactid&$filter=(lists/any(o1:(o1/listid ne null)))", odata); + } + private string ConvertFetchToOData(string fetch) { var context = new XrmFakedContext(); @@ -716,6 +733,19 @@ private string ConvertFetchToOData(string fetch) ReferencingAttribute = "primarycontactid" } }; + var nnRelationships = new[] + { + new ManyToManyRelationshipMetadata + { + SchemaName = "contact_list", + Entity1LogicalName = "contact", + Entity1IntersectAttribute = "entityid", + Entity1NavigationPropertyName = "lists", + Entity2LogicalName = "list", + Entity2IntersectAttribute = "listid", + Entity2NavigationPropertyName = "contacts" + } + }; var entities = new[] { @@ -748,6 +778,16 @@ private string ConvertFetchToOData(string fetch) { LogicalName = "incident", EntitySetName = "incidents" + }, + new EntityMetadata + { + LogicalName = "list", + EntitySetName = "lists" + }, + new EntityMetadata + { + LogicalName = "listmember", + EntitySetName = "listmembers" } }; @@ -848,6 +888,34 @@ private string ConvertFetchToOData(string fetch) { LogicalName = "iscustomizable" } + }, + ["listmember"] = new AttributeMetadata[] + { + new UniqueIdentifierAttributeMetadata + { + LogicalName = "listmemberid" + }, + new LookupAttributeMetadata + { + LogicalName = "entityid", + Targets = new[] { "contact" } + }, + new LookupAttributeMetadata + { + LogicalName = "listid", + Targets = new[] { "list" } + } + }, + ["list"] = new AttributeMetadata[] + { + new UniqueIdentifierAttributeMetadata + { + LogicalName = "listid" + }, + new StringAttributeMetadata + { + LogicalName = "name" + } } }; @@ -855,6 +923,9 @@ private string ConvertFetchToOData(string fetch) SetRelationships(entities, relationships); SetAttributes(entities, attributes); SetSealedProperty(entities.Single(e => e.LogicalName == "incident"), nameof(EntityMetadata.ObjectTypeCode), 112); + SetSealedProperty(entities.Single(e => e.LogicalName == "contact"), nameof(EntityMetadata.ManyToManyRelationships), nnRelationships); + SetSealedProperty(entities.Single(e => e.LogicalName == "listmember"), nameof(EntityMetadata.ManyToManyRelationships), nnRelationships); + SetSealedProperty(entities.Single(e => e.LogicalName == "list"), nameof(EntityMetadata.ManyToManyRelationships), nnRelationships); foreach (var entity in entities) context.SetEntityMetadata(entity); diff --git a/MarkMpn.FetchXmlToWebAPI/FetchXmlToWebAPIConverter.cs b/MarkMpn.FetchXmlToWebAPI/FetchXmlToWebAPIConverter.cs index dfed19a..dbf75a3 100644 --- a/MarkMpn.FetchXmlToWebAPI/FetchXmlToWebAPIConverter.cs +++ b/MarkMpn.FetchXmlToWebAPI/FetchXmlToWebAPIConverter.cs @@ -339,24 +339,28 @@ private List ConvertInnerJoinFilters(string entityName, object[] it private IEnumerable ConvertJoins(string entityName, object[] items, object[] rootEntityItems) { - foreach (var linkEntity in items.OfType().Where(l => l.Items != null && l.Items.Any())) + foreach (var linkEntity in items.OfType()) { var currentLinkEntity = linkEntity; var expand = new LinkEntityOData(); expand.PropertyName = LinkItemToNavigationProperty(entityName, currentLinkEntity, out var child, out var manyToManyNextLink); currentLinkEntity = manyToManyNextLink ?? currentLinkEntity; - expand.Select.AddRange(ConvertSelect(currentLinkEntity.name, currentLinkEntity.Items)); - if (linkEntity.linktype == "outer" || child) + if (currentLinkEntity.Items != null) { - // Don't need to add filters at this point for single-valued properties in inner joins, they'll be added separately later - expand.Filter.AddRange(ConvertFilters(currentLinkEntity.name, currentLinkEntity.Items, rootEntityItems)); - } + expand.Select.AddRange(ConvertSelect(currentLinkEntity.name, currentLinkEntity.Items)); - // Recurse into child joins - expand.Expand.AddRange(ConvertJoins(currentLinkEntity.name, currentLinkEntity.Items, rootEntityItems)); + if (linkEntity.linktype == "outer" || child) + { + // Don't need to add filters at this point for single-valued properties in inner joins, they'll be added separately later + expand.Filter.AddRange(ConvertFilters(currentLinkEntity.name, currentLinkEntity.Items, rootEntityItems)); + } - yield return expand; + // Recurse into child joins + expand.Expand.AddRange(ConvertJoins(currentLinkEntity.name, currentLinkEntity.Items, rootEntityItems)); + + yield return expand; + } } }