4
votes

I am exploring the "QueryExpression" mechanism used to Retrieve data via the Dynamics CRM SDK, and I think I have hit a problem / limitiation with the SDK, but I would like to ascertain that for certain..

Given this desired SQL:

Select C.firstname, C.lastname 
FROM contact C 
INNER JOIN customeraddress A on C.contactid = A.parentid
WHERE 
((C.firstname = 'Max' AND C.lastname = 'Planck') OR (C.firstname = 'Albert' AND C.lastname = 'Einstein'))
OR 
A.Line1 = 'The secret moonbase'

I cannot appear to translate the filter criteria above (the where clause) into the equivalent SDK conditions / filterexpressions etc.

As you can see, I want to query:-

  1. contact, joined to customeraddress (thats simple, just add a link entity to the query expression),
  2. where the contact is either Albert Einstein, or Max Planck (Again, that is simple, add FilterExpressions to the QueryExpression)
  3. OR the customeraddress 'line1' equals 'the secret moonbase' (This is the problematic bit, as soon as I append filter criteria to the LinkEntity, Dynamics uses an "AND" conjunction with the criteria / filters on the main entity.

So the problem I have described in point 3 above, means I can't query dynamics for:

  1. (Albert Einstein Or Max Planck) or anyone who lives at the secret moonbase.

Is this is a current limtation of the SDK?

2
There is an easier way - use LINQ with autogenerated early-bound types msdn.microsoft.com/en-us/library/ff681573.aspx I know this is not exactly an answer to your question... hence it's a comment :)mwrichardson
Thanks @mwrichardson, I was able to do this query via the CrmOrganizationServiceContext, however my understanding is that the Linq provider all sits on top of the underlying OrgService SDK and does the calls for you.. This would suggest it should also be possible purely using the OrganisationService? Anyway, thanks for the comment, but yes my original question still stands.Darrell
Ok I just found the answer to this problem. I can't answer my own question yet as stack overflow imposes a time limit, but after that limit is up I will post the answer.Darrell

2 Answers

6
votes

Ok, I have discovered the answer to this, thanks in part to @mwrichardsone who prompted me to explore how the Dynamics Crm Linq query provider does it, I was then able to work backwards from there..

So here is the equivalent Linq query expression which works (I am using the CrmOrganisationServiceContext):-

var contactsQuery = from c in orgService.CreateQuery("contact")
                    join a in orgService.CreateQuery("customeraddress") on (Guid)c["contactid"] equals (Guid)a["parentid"]
                                where (((string)c["firstname"] == "Max" && (string)c["lastname"] == "Planck")
                                || ((string)c["firstname"] == "Albert" && (string)c["lastname"] == "Einstein"))
                                || (string)a["line1"] == "The secret moonbase"
                    select c;

I then found this article which explains how you can convert linq query to a Query Expression or Fetch Xml: http://pogo69.wordpress.com/2012/04/05/crm-linq-provider-converting-expressions-to-queryexpression-andor-fetchxml/

Once i applied that technique I was able to see what the equivalent QueryExpression looks like.. and basically, the bit that I was missing (key insight) is that when you add a ConditionExpression you can set it's "EntityName". This means you can add a ConditionExpression to a filter group thats on the parent / main entity, even though the condition is actually for an attribute thats present on a link entity (in this case customeraddrress line1). I was assuming you had to add the condition to the linkentity that had that particular attribute - which is also what @Henk van Boeijen did in his answer - and that did not give the correct results.

So the final working QueryExpression looks like this (notice the condition for address line 1 is not added to the address link entity, its added to the filter group on the main entity, and it has an "entity name" set to the alias of the link entity)

var orgService = serviceProvider.GetOrganisationService();
        using (orgService as IDisposable)
        {

            var query = new QueryExpression("contact");
            query.ColumnSet.AddColumn("firstname");
            query.ColumnSet.AddColumn("lastname");

            // so link in customer address.
            query.AddLink("customeraddress", "contactid", "parentid", JoinOperator.Inner);
            var addressLink = query.LinkEntities[0];
            addressLink.EntityAlias = "A";
            addressLink.IncludeAllColumns();

            // conditions for max planck
            var firstName1Condition = new ConditionExpression("firstname", ConditionOperator.Equal, "Max");
            var lastname1Condition = new ConditionExpression("lastname", ConditionOperator.Equal, "Planck");

            // Groups those conditions using an "AND" conjunction.
            var maxPlankFilter = new FilterExpression(LogicalOperator.And);
            maxPlankFilter.AddCondition(firstName1Condition);
            maxPlankFilter.AddCondition(lastname1Condition);

            // conditions for albert einstein
            var firstname2Condition = new ConditionExpression("firstname", ConditionOperator.Equal, "Albert");
            var lastname2Condition = new ConditionExpression("lastname", ConditionOperator.Equal, "Einstein");

            // Groups those conditions using an "AND" conjunction.
            var albertEinsteinFilter = new FilterExpression(LogicalOperator.And);
            albertEinsteinFilter.AddCondition(firstname2Condition);
            albertEinsteinFilter.AddCondition(lastname2Condition);

            // could optionally chain the 2 filters so we get Albert's contitions chained (using AND) to max's conditions 
            //  albertEinsteinFilter.AddFilter(maxPlankFilter);

            // conditions for address line 1 moonbase
            var addressLine1Filter = new FilterExpression(LogicalOperator.And); 
            var line1Condition = new ConditionExpression("A", "line1", ConditionOperator.Equal, "The secret moonbase");
            addressLine1Filter.AddCondition(line1Condition);


            // add filters to query 
            // ensures each filter that we add to our queries criteria is chained together using an OR.
            query.Criteria.FilterOperator = LogicalOperator.Or;
            query.Criteria.AddFilter(albertEinsteinFilter);
            query.Criteria.AddFilter(maxPlankFilter);
            query.Criteria.AddFilter(addressLine1Filter);

            var results = orgService.RetrieveMultiple(query);
            int resultCount = 0;
            foreach (var r in results.Entities)
            {
                resultCount++;
                Console.WriteLine(string.Format("{0} {1} {2}", (string)r["firstname"], (string)r["lastname"], (string)((AliasedValue)r["A.line1"]).Value));
            }
            Console.WriteLine("There were " + resultCount + " results..");


        }

Side Note: See @Henk van Boeijen's post below if you would like to see a shorter syntax for building a query expression. If productivity is truly your concern however, I would have to echo the comment from @Nicknow below and suggest that you seriously take a look at using the Linq query mechanism for performing CRM queries.

Also @Henk van Boeijen has pointed out that my answer is based on a feature that only appears in the 2013 SDK, and doesn't appear to be in prior versions. I haven't checked this personally, but that information is probably very useful for you to know especially if you are not using the latest versions of the SDK.

1
votes

It is actually pretty straightforward; use the LogicalOperator and the LinkEntity.

I would recommend adding the DISTINCT predicate.

private IEnumerable<Entity> QueryExpression(IOrganizationService service)
{
    var query = new QueryExpression("contact");
    query.Distinct = true;
    query.ColumnSet.AddColumns("firstname", "lastname");

    query.Criteria.FilterOperator = LogicalOperator.Or;
    var f1 = query.Criteria.AddFilter(LogicalOperator.And);
    f1.AddCondition("firstname", ConditionOperator.Equal, "Max");
    f1.AddCondition("lastname", ConditionOperator.Equal, "Planck");

    var f2 = query.Criteria.AddFilter(LogicalOperator.And);
    f2.AddCondition("firstname", ConditionOperator.Equal, "Albert");
    f2.AddCondition("lastname", ConditionOperator.Equal, "Einstein");

    var link = query.AddLink("customeraddress", "contactid", "parentid");
    link.EntityAlias = "ca";
    query.Criteria.AddCondition("ca", "line1", ConditionOperator.Equal, "The secret moonbase");

    var response = service.RetrieveMultiple(query);

    return response.Entities;
}

It is important to note that this query uses a new feature added in Dynamics CRM 2013. It does not work in Dynamics CRM 2011, because in that version it is not possible to specify an entityname (or its alias) in the ConditionExpression.