Wednesday, 30 April 2014

Retrieve connections from a record, filter them on the name of the connection role and send email (CRM 2011)

We have had the need of retrieve all the records connected to a given record with a specific connection role, and send an email to the corresponding email addresses. This "notification" must occur in response of a "field change" event on the form. We developed one custom workflow activity to implement this requisite.
One complexity to consider is on the generalization of the records type: we can have contacts, as well as accounts, system users and so on.
An important consideration concerns the name of the connection role: there is no unique constraint on it in Dynamics CRM 2011, therefore there could be two different connection roles with the same name. If this happens, and there are two connections to a record with "different" connection roles (but same name), the "magic" behind CRM retrieves both of them.
Another important point is the need to skip (in some way) the retrieved (and filtered) records that have not a value for the email. This is because the asynchronous nature of the workflow. During the retrieving records, if a column (in the column set) has no value, it results in the "absence" of the corresponding attribute in the retrieved record. When the workflow tries to read it, the system puts it in "Waiting status" in the wait for a value (with a finite number of tentatives). This is not what we want, because logically, if a record has no email, simply we want to avoid to notify via email.
One important point to focus in the code below is the parameter (InArgument<EntityReference>) "EmailMessage".
This implementative choice is because we need all the features provided natively (OOB) from email message (Rich Text Box in the body, Activity Party for the "From" and "To" field, and so on), that we would lose if creation of the email was inside our CWA.
We implemented this "pattern" to pass the email created as parameter, and afterwards the CWA processes it adding the recipients (To) retrieved with our custom logic.

using System;
using System.Activities;
using System.ServiceModel;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Workflow;
using System.Linq;
using Microsoft.Crm.Sdk.Messages;
using System.Collections.Generic;
using Microsoft.Xrm.Sdk.Query;

public sealed class RetrieveConnectionsByRolesAndProcessEmail : CodeActivity
{
    //Email message
    [RequiredArgument]
    [Input("Email")]
    [ReferenceTarget("email")]
    public InArgument<EntityReference> EmailMessage
    {
        get;
        set;
    }

    [Input("Role To (Name)")]
    public InArgument<string> RoleToName
    {
        get;
        set;
    }

    [Input("Role From (Name)")]
    public InArgument<string> RoleFromName
    {
        get;
        set;
    }

    /// <summary>
    /// Executes the workflow activity.
    /// </summary>
    /// <param name="executionContext">The execution context.</param>
    protected override void Execute(CodeActivityContext executionContext)
    {
        // Create the tracing service
        ITracingService tracingService = executionContext.GetExtension<ITracingService>();

        if (tracingService == null)
        {
            throw new InvalidPluginExecutionException("Failed to retrieve tracing service.");
        }

        tracingService.Trace("Entered RetrieveConnectionsByRolesAndProcessEmail.Execute(), Activity Instance Id: {0}, Workflow Instance Id: {1}",
            executionContext.ActivityInstanceId,
            executionContext.WorkflowInstanceId);

        // Create the context
        IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();

        if (context == null)
        {
            throw new InvalidPluginExecutionException("Failed to retrieve workflow context.");
        }

        tracingService.Trace("RetrieveConnectionsByRolesAndProcessEmail.Execute(), Correlation Id: {0}, Initiating User: {1}",
            context.CorrelationId,
            context.InitiatingUserId);

        IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
        IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

        //Retrieve Pre Image entity
        Entity preImageEntity = context.PreEntityImages.Values.FirstOrDefault();

        try
        {
            List<ActivityParty> emailAddresses = new List<ActivityParty>();
            //string emailAddresses = string.Empty;

            // This query retrieves all connections this record is part of.
            QueryExpression query = newQueryExpression
            {
                EntityName = Connection.EntityLogicalName,
                ColumnSet = new ColumnSet("connectionid", "record2id", "record1roleid", "record2roleid"), //Connection ID, Connected To, As this role (From), As this role (To)
                Criteria = new FilterExpression
                {
                    FilterOperator = LogicalOperator.And,
                    Conditions =
                {
                    new ConditionExpression
                    {
                        AttributeName = "record1id",
                        Operator = ConditionOperator.Equal,
                        Values = { preImageEntity.Id }
                    }
                }
                }
            };

            //Filter on Role To if provided
            if (!string.IsNullOrEmpty(this.RoleToName.Get(executionContext).ToString()))
            {
                //Check if the role´s name is correct
                QueryExpression queryRoleTo = new QueryExpression
                {
                    EntityName = ConnectionRole.EntityLogicalName,
                    ColumnSet = new ColumnSet(false),
                    Criteria = new FilterExpression
                    {
                        FilterOperator = LogicalOperator.And,
                        Conditions =
                    {
                        new ConditionExpression
                        {
                            AttributeName = "name",
                            Operator = ConditionOperator.Equal,
                            Values = { this.RoleToName.Get(executionContext).ToString() }
                        }
                    }
                    }
                };

                var rolesTo = service.RetrieveMultiple(queryRoleTo);
                if (rolesTo.Entities.Count() == 0)
                {
                    throw newInvalidPluginExecutionException("No connection role with the name provided: please check the \"Role From\" and correct it");
                }

                LinkEntity linkEntityRoleTo = new LinkEntity();
                linkEntityRoleTo.JoinOperator = JoinOperator.Natural;
                linkEntityRoleTo.LinkFromEntityName = Connection.EntityLogicalName;
                linkEntityRoleTo.LinkFromAttributeName = "record2roleid";
                linkEntityRoleTo.LinkToEntityName = ConnectionRole.EntityLogicalName;
                linkEntityRoleTo.LinkToAttributeName = "connectionroleid";


                ConditionExpression conditionRoleTo = new ConditionExpression(
                    "name",
                    ConditionOperator.Equal,
                    new object[] { this.RoleToName.Get(executionContext).ToString() }
                );

                linkEntityRoleTo.LinkCriteria.Conditions.AddRange(new ConditionExpression[] { conditionRoleTo });

                query.LinkEntities.AddRange(new LinkEntity[] { linkEntityRoleTo });
            }

            //Filter on Role From if provided
            if (!string.IsNullOrEmpty(this.RoleFromName.Get(executionContext).ToString()))
            {
                //Check if the role´s name is correct
                QueryExpression queryRoleFrom = new QueryExpression
                {
                    EntityName = ConnectionRole.EntityLogicalName,
                    ColumnSet = new ColumnSet(false),
                    Criteria = new FilterExpression
                    {
                        FilterOperator = LogicalOperator.And,
                        Conditions =
                    {
                        new ConditionExpression
                        {
                            AttributeName = "name",
                            Operator = ConditionOperator.Equal,
                            Values = { this.RoleFromName.Get(executionContext).ToString() }
                        }
                    }
                    }
                };

                var rolesFrom = service.RetrieveMultiple(queryRoleFrom);
                if (rolesFrom.Entities.Count() == 0)
                {
                    throw newInvalidPluginExecutionException("No connection role with the name provided: please check the \"Role To\" and correct it");
                }

                LinkEntity linkEntityRoleFrom = new LinkEntity();
                linkEntityRoleFrom.JoinOperator = JoinOperator.Natural;
                linkEntityRoleFrom.LinkFromEntityName = Connection.EntityLogicalName;
                linkEntityRoleFrom.LinkFromAttributeName = "record1roleid";
                linkEntityRoleFrom.LinkToEntityName = ConnectionRole.EntityLogicalName;
                linkEntityRoleFrom.LinkToAttributeName = "connectionroleid";


                ConditionExpression conditionRoleFrom = new ConditionExpression(
                    "name",
                    ConditionOperator.Equal,
                    new object[] { this.RoleFromName.Get(executionContext).ToString() }
                );

                linkEntityRoleFrom.LinkCriteria.Conditions.AddRange(new ConditionExpression[] { conditionRoleFrom });

                query.LinkEntities.AddRange(new LinkEntity[] { linkEntityRoleFrom });
            }

            EntityCollection results = service.RetrieveMultiple(query);

            string entityName, emailFieldName = string.Empty;
            ColumnSet columnSet = newColumnSet();

            if (results.Entities.Count() == 0)
                return;

            foreach (Entity connection in results.Entities.AsEnumerable())
            {
                entityName = connection.Attributes.Contains("record2id") ? ((EntityReference)connection.Attributes["record2id"]).LogicalName : string.Empty;

                //The list below contains all and only the entity allowed in the "Email To" field
                //Every other entity record type is ignored and not managedLoop
                switch (entityName)
                {
                    case SystemUser.EntityLogicalName:
                        columnSet = new ColumnSet(new string[] { emailFieldName = "internalemailaddress" });
                        break;
                    case Account.EntityLogicalName:
                        columnSet = new ColumnSet(new string[] { emailFieldName = "emailaddress1" });
                        break;
                    case Contact.EntityLogicalName:
                        columnSet = new ColumnSet(new string[] { emailFieldName = "emailaddress1" });
                        break;
                    case Queue.EntityLogicalName:
                        columnSet = new ColumnSet(new string[] { emailFieldName = "emailaddress" });
                        break;
                    caseLead.EntityLogicalName:
                        columnSet = new ColumnSet(new string[] { emailFieldName = "emailaddress1" });
                        break;
                    case Equipment.EntityLogicalName:
                        columnSet = new ColumnSet(new string[] { emailFieldName = "emailaddress" });
                        break;
                    default:
                        emailFieldName = string.Empty;
                        break;
                }

                Entity record;

                //If entity record not valid --> skip
                if (!string.IsNullOrEmpty(emailFieldName))
                {
                    //Retrieving only one field (email address) imply less complexity and "maximize" query-performances
                    record = service.Retrieve(entityName, ((EntityReference)connection.Attributes["record2id"]).Id, columnSet);
                    
                    if (record.Attributes.Contains(emailFieldName)) //If email address is not set --> skip
                    {
                        emailAddresses.Add(new ActivityParty() { PartyId = new EntityReference(entityName, record.Id), AddressUsed = record.Attributes[emailFieldName].ToString() });
                    }
                }
            }

            //Retrieving email (Email parameter is required)
            Email email = (Email)service.Retrieve(Email.EntityLogicalName, EmailMessage.Get(executionContext).Id, new ColumnSet(false));

            //Set "To" (Recipients)
            email.To = emailAddresses;

            //Update email
            service.Update(email);

            //Send email
            SendEmailRequest sendEmailreq = new SendEmailRequest
            {
                EmailId = email.Id,
                TrackingToken = "",
                IssueSend = true
            };

            SendEmailResponse sendEmailresp = (SendEmailResponse)service.Execute(sendEmailreq);
        }
        catch (FaultException<OrganizationServiceFault> e)
        {
            tracingService.Trace("Exception: {0}", e.ToString());

            throw new InvalidPluginExecutionException("Error message: " + e.Message + " - Error stacktrace: " + e.StackTrace);
        }

        tracingService.Trace("Exiting RetrieveConnectionsByRolesAndProcessEmail.Execute(), Correlation Id: {0}", context.CorrelationId);
    }
}



Hope it can be useful!
Happy CRM coding!