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.
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;
publicsealedclassRetrieveConnectionsByRolesAndProcessEmail : CodeActivity
{
//Email message
[RequiredArgument]
[Input("Email")]
[ReferenceTarget("email")]
publicInArgument<EntityReference> EmailMessage
{
get;
set;
}
[Input("Role To (Name)")]
publicInArgument<string> RoleToName
{
get;
set;
}
[Input("Role From (Name)")]
publicInArgument<string> RoleFromName
{
get;
set;
}
///<summary>
/// Executes the workflow activity.
///</summary>
///<param name="executionContext">The execution context.</param>
protectedoverridevoid Execute(CodeActivityContext executionContext)
{
// Create the tracing service
ITracingService tracingService = executionContext.GetExtension<ITracingService>();
if (tracingService == null)
{
thrownewInvalidPluginExecutionException("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)
{
thrownewInvalidPluginExecutionException("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 = newList<ActivityParty>();
//string emailAddresses = string.Empty;
// This query retrieves all connections this record is part of.
QueryExpression query = newQueryExpression
{
EntityName = Connection.EntityLogicalName,
ColumnSet = newColumnSet("connectionid", "record2id", "record1roleid", "record2roleid"), //Connection ID, Connected To, As this role (From), As this role (To)
Criteria = newFilterExpression
{
FilterOperator = LogicalOperator.And,
Conditions =
{
newConditionExpression
{
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 = newQueryExpression
{
EntityName = ConnectionRole.EntityLogicalName,
ColumnSet = newColumnSet(false),
Criteria = newFilterExpression
{
FilterOperator = LogicalOperator.And,
Conditions =
{
newConditionExpression
{
AttributeName = "name",
Operator = ConditionOperator.Equal,
Values = { this.RoleToName.Get(executionContext).ToString() }
}
}
}
};
var rolesTo = service.RetrieveMultiple(queryRoleTo);
if (rolesTo.Entities.Count() == 0)
{
thrownewInvalidPluginExecutionException("No connection role with the name provided: please check the \"Role From\" and correct it");
}
LinkEntity linkEntityRoleTo = newLinkEntity();
linkEntityRoleTo.JoinOperator = JoinOperator.Natural;
linkEntityRoleTo.LinkFromEntityName = Connection.EntityLogicalName;
linkEntityRoleTo.LinkFromAttributeName = "record2roleid";
linkEntityRoleTo.LinkToEntityName = ConnectionRole.EntityLogicalName;
linkEntityRoleTo.LinkToAttributeName = "connectionroleid";
ConditionExpression conditionRoleTo = newConditionExpression(
"name",
ConditionOperator.Equal,
newobject[] { this.RoleToName.Get(executionContext).ToString() }
);
linkEntityRoleTo.LinkCriteria.Conditions.AddRange(newConditionExpression[] { conditionRoleTo });
query.LinkEntities.AddRange(newLinkEntity[] { 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 = newQueryExpression
{
EntityName = ConnectionRole.EntityLogicalName,
ColumnSet = newColumnSet(false),
Criteria = newFilterExpression
{
FilterOperator = LogicalOperator.And,
Conditions =
{
newConditionExpression
{
AttributeName = "name",
Operator = ConditionOperator.Equal,
Values = { this.RoleFromName.Get(executionContext).ToString() }
}
}
}
};
var rolesFrom = service.RetrieveMultiple(queryRoleFrom);
if (rolesFrom.Entities.Count() == 0)
{
thrownewInvalidPluginExecutionException("No connection role with the name provided: please check the \"Role To\" and correct it");
}
LinkEntity linkEntityRoleFrom = newLinkEntity();
linkEntityRoleFrom.JoinOperator = JoinOperator.Natural;
linkEntityRoleFrom.LinkFromEntityName = Connection.EntityLogicalName;
linkEntityRoleFrom.LinkFromAttributeName = "record1roleid";
linkEntityRoleFrom.LinkToEntityName = ConnectionRole.EntityLogicalName;
linkEntityRoleFrom.LinkToAttributeName = "connectionroleid";
ConditionExpression conditionRoleFrom = newConditionExpression(
"name",
ConditionOperator.Equal,
newobject[] { this.RoleFromName.Get(executionContext).ToString() }
);
linkEntityRoleFrom.LinkCriteria.Conditions.AddRange(newConditionExpression[] { conditionRoleFrom });
query.LinkEntities.AddRange(newLinkEntity[] { 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)
{
caseSystemUser.EntityLogicalName:
columnSet = newColumnSet(newstring[] { emailFieldName = "internalemailaddress" });
break;
caseAccount.EntityLogicalName:
columnSet = newColumnSet(newstring[] { emailFieldName = "emailaddress1" });
break;
caseContact.EntityLogicalName:
columnSet = newColumnSet(newstring[] { emailFieldName = "emailaddress1" });
break;
caseQueue.EntityLogicalName:
columnSet = newColumnSet(newstring[] { emailFieldName = "emailaddress" });
break;
caseLead.EntityLogicalName:
columnSet = newColumnSet(newstring[] { emailFieldName = "emailaddress1" });
break;
caseEquipment.EntityLogicalName:
columnSet = newColumnSet(newstring[] { 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(newActivityParty() { PartyId = newEntityReference(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, newColumnSet(false));
//Set "To" (Recipients)
email.To = emailAddresses;
//Update email
service.Update(email);
//Send email
SendEmailRequest sendEmailreq = newSendEmailRequest
{
EmailId = email.Id,
TrackingToken = "",
IssueSend = true
};
SendEmailResponse sendEmailresp = (SendEmailResponse)service.Execute(sendEmailreq);
}
catch (FaultException<OrganizationServiceFault> e)
{
tracingService.Trace("Exception: {0}", e.ToString());
thrownewInvalidPluginExecutionException("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!