Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/XrmMockup365/Requests/RetrieveMultipleRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ internal override OrganizationResponse Execute(OrganizationRequest orgRequest, E

FillAliasIfEmpty(queryExpr);

// Reject up-front any filter that references an attribute that does not exist on the
// targeted entity, mirroring Dataverse (which faults instead of silently ignoring the
// condition). Run after FillAliasIfEmpty so link-criteria conditions carry their alias.
ValidateFilterAttributes(queryExpr);

db.PrefillDBWithOnlineData(queryExpr);
// Create a snapshot for thread-safe enumeration during calculated field execution
var rows = db.GetDBEntityRows(queryExpr.EntityName).ToList();
Expand Down Expand Up @@ -366,6 +371,90 @@ private static ISet<string> GetRequestedAttributes(ColumnSet columnSet)
return new HashSet<string>(columnSet.Columns, StringComparer.OrdinalIgnoreCase);
}

// Validates that every attribute referenced in the query's filters exists on the entity it
// targets. Conditions carry an EntityName (the primary entity or a link alias) that decides
// which entity's metadata the attribute is looked up against; the lookup is case-sensitive,
// matching Dataverse.
private void ValidateFilterAttributes(QueryExpression query)
{
// alias/logical-name -> metadata for the primary entity and every (nested) link entity.
var scopes = new Dictionary<string, EntityMetadata>(StringComparer.Ordinal);
if (metadata.EntityMetadata.TryGetValue(query.EntityName, out var primaryMeta))
{
scopes[query.EntityName] = primaryMeta;
}
foreach (var link in query.LinkEntities)
{
CollectLinkScopes(link, scopes);
}

ValidateFilterAttributes(query.Criteria, query.EntityName, scopes);
foreach (var link in query.LinkEntities)
{
ValidateLinkCriteria(link, scopes);
}
}

private void CollectLinkScopes(LinkEntity link, Dictionary<string, EntityMetadata> scopes)
{
var scopeKey = GetLinkScopeKey(link);
if (!scopes.ContainsKey(scopeKey) &&
metadata.EntityMetadata.TryGetValue(link.LinkToEntityName, out var meta))
{
scopes[scopeKey] = meta;
}
foreach (var nested in link.LinkEntities)
{
CollectLinkScopes(nested, scopes);
}
}

private void ValidateLinkCriteria(LinkEntity link, Dictionary<string, EntityMetadata> scopes)
{
ValidateFilterAttributes(link.LinkCriteria, GetLinkScopeKey(link), scopes);
foreach (var nested in link.LinkEntities)
{
ValidateLinkCriteria(nested, scopes);
}
}

// FillAliasIfEmpty only assigns an alias when one is null, so a link whose EntityAlias is an
// explicit empty string keeps it. Fall back to the linked entity's logical name in that case
// so the link's criteria are still validated against the correct metadata.
private static string GetLinkScopeKey(LinkEntity link)
{
return string.IsNullOrEmpty(link.EntityAlias) ? link.LinkToEntityName : link.EntityAlias;
}

private void ValidateFilterAttributes(FilterExpression filter, string defaultScope, Dictionary<string, EntityMetadata> scopes)
{
if (filter == null) return;

foreach (var condition in filter.Conditions)
{
// A null/empty attribute name is a filter on the primary id, not a real column.
if (string.IsNullOrEmpty(condition.AttributeName)) continue;

var scopeKey = string.IsNullOrEmpty(condition.EntityName) ? defaultScope : condition.EntityName;
// If the scope can't be resolved to a known entity we can't validate it; leave it be
// rather than risk rejecting a legitimate query.
if (scopeKey == null || !scopes.TryGetValue(scopeKey, out var meta)) continue;

if (!Utility.IsValidAttribute(condition.AttributeName, meta))
{
var entityName = string.IsNullOrEmpty(meta.SchemaName) ? meta.LogicalName : meta.SchemaName;
throw new FaultException<OrganizationServiceFault>(
new OrganizationServiceFault(),
$"'{entityName}' entity doesn't contain attribute with Name = '{condition.AttributeName}' and NameMapping = 'Logical' (look up attribute by name is case-sensitive)");
}
}

foreach (var sub in filter.Filters)
{
ValidateFilterAttributes(sub, defaultScope, scopes);
}
}

private static void CollectFilterAttributes(FilterExpression filter, HashSet<string> set)
{
if (filter == null) return;
Expand Down
106 changes: 106 additions & 0 deletions tests/XrmMockup365Test/TestRetrieveMultiple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.ServiceModel;
using Xunit;

namespace DG.XrmMockupTest
Expand Down Expand Up @@ -1867,5 +1868,110 @@ public void TestRetrieveMultipleEntityReferenceNamePopulatedWithFetchXml()
Assert.Equal(parentAccount.Id, retrievedAccount.ParentAccountId.Id);
Assert.Equal("FetchXml Parent Account", retrievedAccount.ParentAccountId.Name);
}

[Fact]
public void TestRetrieveMultipleFilterOnInvalidAttributeFails()
{
var query = new QueryExpression(Account.EntityLogicalName)
{
ColumnSet = new ColumnSet("name")
};
query.Criteria.AddCondition("nonexistentattribute", ConditionOperator.Equal, "whatever");

var ex = Assert.Throws<FaultException<OrganizationServiceFault>>(
() => orgAdminService.RetrieveMultiple(query));
Assert.Equal(
"'Account' entity doesn't contain attribute with Name = 'nonexistentattribute' and NameMapping = 'Logical' (look up attribute by name is case-sensitive)",
ex.Message);
Comment thread
mkholt marked this conversation as resolved.
}

[Fact]
public void TestRetrieveMultipleFilterAttributeLookupIsCaseSensitive()
{
// "Name" (capitalised) is not the logical name; the lookup must be case-sensitive and fault.
var query = new QueryExpression(Account.EntityLogicalName)
{
ColumnSet = new ColumnSet("name")
};
query.Criteria.AddCondition("Name", ConditionOperator.Equal, "account1");

var ex = Assert.Throws<FaultException<OrganizationServiceFault>>(
() => orgAdminService.RetrieveMultiple(query));
Assert.Equal(
"'Account' entity doesn't contain attribute with Name = 'Name' and NameMapping = 'Logical' (look up attribute by name is case-sensitive)",
ex.Message);
Comment thread
mkholt marked this conversation as resolved.
}

[Fact]
public void TestRetrieveMultipleFilterOnValidAttributeSucceeds()
{
var account = new Account { Name = "ValidAttributeFilterAccount" };
account.Id = orgAdminService.Create(account);

var query = new QueryExpression(Account.EntityLogicalName)
{
ColumnSet = new ColumnSet("name")
};
query.Criteria.AddCondition("name", ConditionOperator.Equal, "ValidAttributeFilterAccount");

var result = orgAdminService.RetrieveMultiple(query);
Assert.Single(result.Entities);
}

[Fact]
public void TestRetrieveMultipleFetchXmlFilterOnInvalidAttributeFails()
{
var fetchXml = @"<fetch>
<entity name='account'>
<attribute name='name' />
<filter>
<condition attribute='nonexistentattribute' operator='eq' value='x' />
</filter>
</entity>
</fetch>";

var ex = Assert.Throws<FaultException<OrganizationServiceFault>>(
() => orgAdminService.RetrieveMultiple(new FetchExpression(fetchXml)));
Assert.Equal(
"'Account' entity doesn't contain attribute with Name = 'nonexistentattribute' and NameMapping = 'Logical' (look up attribute by name is case-sensitive)",
ex.Message);
Comment thread
mkholt marked this conversation as resolved.
}

[Fact]
public void TestRetrieveMultipleLinkEntityFilterOnInvalidAttributeFails()
{
var query = new QueryExpression(Account.EntityLogicalName)
{
ColumnSet = new ColumnSet("name")
};
var link = query.AddLink(Contact.EntityLogicalName, "primarycontactid", "contactid");
link.LinkCriteria.AddCondition("nonexistentattribute", ConditionOperator.Equal, "x");

var ex = Assert.Throws<FaultException<OrganizationServiceFault>>(
() => orgAdminService.RetrieveMultiple(query));
Assert.Equal(
"'Contact' entity doesn't contain attribute with Name = 'nonexistentattribute' and NameMapping = 'Logical' (look up attribute by name is case-sensitive)",
ex.Message);
Comment thread
mkholt marked this conversation as resolved.
}

[Fact]
public void TestRetrieveMultipleLinkEntityWithEmptyAliasFilterOnInvalidAttributeFails()
{
// An explicit empty alias is not replaced by alias-filling; the link criteria must still
// be validated against the linked entity's metadata.
var query = new QueryExpression(Account.EntityLogicalName)
{
ColumnSet = new ColumnSet("name")
};
var link = query.AddLink(Contact.EntityLogicalName, "primarycontactid", "contactid");
link.EntityAlias = "";
link.LinkCriteria.AddCondition("nonexistentattribute", ConditionOperator.Equal, "x");

var ex = Assert.Throws<FaultException<OrganizationServiceFault>>(
() => orgAdminService.RetrieveMultiple(query));
Assert.Equal(
"'Contact' entity doesn't contain attribute with Name = 'nonexistentattribute' and NameMapping = 'Logical' (look up attribute by name is case-sensitive)",
ex.Message);
}
}
}
Loading