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!

Tuesday, 17 September 2013

Retrieve System Users by a Security Role (CRM 2011)

We have had the need to retrieve an entity collection of system users from a security role in a plugin. We developed a method that accept the role's name and a collection of attributes of System User entity.
In this method we use query expression to retrieve the system user specifying a link to entity "SystemUserRoles" and then a link to the "Role" entity. The linking attribute is the SystemUserId for the first link and the RoleId for the second one.
Finally a condition on the role's name to filter the system users.

public EntityCollection RetrieveUsersInfoByRole(string roleName,
                                                string[] userColumnSet)
{
    try
    {
        serviceProxy = ServiceConnector.
                       GetOrganizationService("<Organization.svc URL>");
 
        QueryExpression query = new QueryExpression("systemuser");
        query.ColumnSet = new ColumnSet(userColumnSet);
        query.Distinct = true;
        query.Criteria = new FilterExpression();
        //Add link to system user roles and role
        query.AddLink("systemuserroles", "systemuserid", "systemuserid").
            AddLink("role", "roleid", "roleid").LinkCriteria.
            AddCondition("name", ConditionOperator.Equal, roleName);
 
        EntityCollection users = serviceProxy.RetrieveMultiple(query);
 
        return users;
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

 
Hope it can be useful.
Happy CRM coding

Monday, 29 July 2013

Clone an entity record via JavaScript (CRM 2011)

We have had the need to clone a record with client-side Scripting, responding to a click on a button in the entity's Ribbon.
Conceptually the operation is easy and is based on the retrieving of all the record information using the Organization web service, then build an object identical to the first and use the web service to create a new record with this object.
Few things are important to take in mind:
1. In the building of the object the record ID must be "skipped" because, obviously, we can't create a record with an existing Guid
2. Date fields are returned in a format like: "Date("date_string") that it is not accepted in record creation from the Organization web service: depending of the need of every field, we decided to "skip it" or "reformat it". In the sample code we show both of these cases.
3. StateCode and StatusCode should be skipped (or managed) because, natively, Organization web service doesn't allow the creation of a record in a closed state.


function cloneRecord(serverUrl, originRecordType, originRecordGuid) {
    var cloneData = {};
    var activityId;
    var serverUrl = document.location.protocol + "//" + document.location.host
                    + "/"Xrm.Page.context.getOrgUniqueName();
    var oDataUri = serverUrl + "/xrmservices/2011/OrganizationData.svc/" 
                   originRecordType + "Set?$select=*&$filter=<EntityId> eq guid'"
                   + originRecordGuid + "'";
    jQuery.support.cors = true;
    jQuery.ajax({
        type: "GET",
        contentType: "application/json; charset=utf-8",
        datatype: "json",
        url: oDataUri,
        async: false, //Synchronous operation 
        beforeSend: function (XMLHttpRequest) {
            //Specifying this header ensures that the results
              will be returned as JSON.           
            XMLHttpRequest.setRequestHeader("Accept", "application/json");
        },
        success: function (data, textStatus, XmlHttpRequest) {
            if (data && data.d && data.d.results) {
                cloneData = data.d.results[0];
                //Here insert the code to skip/transform fields such as
                  Record Id, Date fields, etc..
                replacer = function (key, value) {
                    if (key == "ModifiedOn" || key == originRecordType + "Id" ||
                        key == "CreatedOn" || key == "StateCode" ||
                        key == "StatusCode") {
                       
                        return undefined;
                    } else if (key == "ActualStart" || key == "ActualEnd") {
                        if (value) {
                            var date =
                            eval('new ' +
                               value.replace("/", "").replace("/", ""));
                            return date.format('yyyy-MM-dd'); //Format the date
                        }
                    }
                    else return value;
                }
                //Create new Activity
                var oDataUri = serverUrl +
                               "/xrmservices/2011/OrganizationData.svc/" +
                               originRecordType + "Set";
                jQuery.support.cors = true;
                jQuery.ajax({
                    type: "POST",
                    contentType: "application/json; charset=utf-8",
                    datatype: "json",
                    url: oDataUri,
                    async: false, //Synchronous operation 
                    data: JSON.stringify(cloneData, replacer),
                    beforeSend: function (XMLHttpRequest) {
                        //Specifying this header ensures that the results will be
                          returned as JSON.           
                        XMLHttpRequest.setRequestHeader("Accept",
                                                          "application/json");
                    },
                    success: function (data, textStatus, XmlHttpRequest) {
                        if (data && data.d) {
                            activityId = data.d.ActivityId;
                        }
                    },
                    error: function (XmlHttpRequest, textStatus, errorThrown) {
                      alert("Error :  has occured during creation of
                              the activity "  + originRecordType.text + ": " +
                             XmlHttpRequest.responseText);
                    }
                });
            } else {
                //No data returned
            }
        },
        error: function (XmlHttpRequest, textStatus, errorThrown) {
            alert("Error :  has occured during retrieving of the activity "
                    + originRecordType.text + ": " +
                    XmlHttpRequest.responseText);
        }
    });
    return activityId;
}
Hope it can be useful
Happy CRM coding