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;
+ }
}
}