Custom Workflow Action for Mapping Many-to-Many Relationships

The following post is a simple set of example code to map a set of Many to Many relationships from one record to another via the addition of a Custom Workflow Action in MSCRM. This is ostensibly aimed at ensuring a set of Many to Many associations are correctly mapped from Lead to Contact or Opportunity when the Lead is qualified.

Mapping Many-to-Many Relationships from Lead to Contact

For mapping a series of Many-to-Many associations from Lead to Contact upon qualification – we could use a Custom Workflow Action which takes in the GUID Id of the Lead record and the GUID Id of the Contact record to then read back the Lead record’s associations and recreate them for the Contact, the code below would implement this:

    [CrmWorkflowActivity("Map Area of Interest from Lead to Contact", "MSCRM.MappingManyToMany")]
    public partial class MapManyToMany_FromLead_ToContact : SequenceActivity
    {
        #region Execute / Main()

        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
            try
            {
                
                // Undertake logic of Custom Workflow Action
                if (this.LeadID != null)
                {
                    IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
                    IWorkflowContext context = contextService.Context;
                    PluginCRMMethods crmMethods = new PluginCRMMethods(context.CreateCrmService());

                    Guid[] mappings = crmMethods.GetManyToManyAssociationsFromRecord(this.LeadID.Value, "lead", "new_areaofinterest_lead", "new_areaofinterest");

                    this.MapManyToManyNo = new CrmNumber(mappings.Length);

                    if (this.ContactID != null)
                    {
                        crmMethods.CreateManyToManyAssociations(this.ContactID.Value, "contact", "new_areaofinterest_contact", mappings, "new_areaofinterest");
                    }
                    else
                    {
                        throw new Exception("Contact ID not specified for custom Workflow Action");
                    }

                    crmMethods = null;
                }
                else
                {
                    throw new Exception("Lead ID not specified for custom Workflow Action");
                }

                // Return Execution to MSCRM
                return base.Execute(executionContext);
            }
            catch (Exception ex)
            {
                string errMessage = "MapManyToMany_FromLead_ToContact (" + ex.Message + ")";
                // eventLogging.LogError(errMessage);
                throw new Exception(errMessage);
            }
        }

        #endregion

        #region Custom Action Inputs

        public static DependencyProperty LeadIDProperty = DependencyProperty.Register("LeadID", typeof(Lookup), typeof(MapManyToMany_FromLead_ToContact));

        [CrmInput("LeadID")]
        [CrmReferenceTarget("lead")]
        public Lookup LeadID
        {
            get
            {
                return (Lookup)base.GetValue(LeadIDProperty);
            }
            set
            {
                base.SetValue(LeadIDProperty, value);
            }
        }

        public static DependencyProperty ContactIDProperty = DependencyProperty.Register("ContactID", typeof(Lookup), typeof(MapManyToMany_FromLead_ToContact));

        [CrmInput("ContactID")]
        [CrmReferenceTarget("contact")]
        public Lookup ContactID
        {
            get
            {
                return (Lookup)base.GetValue(ContactIDProperty);
            }
            set
            {
                base.SetValue(ContactIDProperty, value);
            }
        }

        #endregion

        #region Custom Action Outputs

        public static DependencyProperty MapManyToManyNoProperty = DependencyProperty.Register("MapManyToManyNo", typeof(CrmNumber), typeof(MapManyToMany_FromLead_ToContact));

        [CrmOutput("MapManyToManyNo")]
        public CrmNumber MapManyToManyNo
        {
            get
            {
                return (CrmNumber)base.GetValue(MapManyToManyNoProperty);
            }
            set
            {
                base.SetValue(MapManyToManyNoProperty, value);
            }
        }

        #endregion

    }

This code can be compiled and registered via the Plugin Registration Tool and then invoked in a MSCRM Workrule Rule which fires on creation of a new Contact record which has an association to an Originating Lead:

Invoking the Custom Workflow Action via a new Workflow Rule triggering whenever a Contact is created where the Originating Lead field is populated

Invoking the Custom Workflow Action via a new Workflow Rule triggering whenever a Contact is created where the Originating Lead field is populated

This will act to recreate the list of associations for the Lead record whenever the Lead is qualified into a Contact.

Mapping Many-to-Many Relationships from Lead to Opportunity

This code can easily be reformatted to then work for a new Opportunity resulting from a qualified Lead in a very similar fashion:

    [CrmWorkflowActivity("Map Area of Interest from Lead to Opportunity", "MSCRM.MappingManyToMany")]
    public partial class MapManyToMany_FromLead_ToOpportunity : SequenceActivity
    {
        #region Execute / Main()

        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
            try
            {

                // Undertake logic of Custom Workflow Action
                if (this.LeadID != null)
                {
                    IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
                    IWorkflowContext context = contextService.Context;
                    PluginCRMMethods crmMethods = new PluginCRMMethods(context.CreateCrmService());

                    Guid[] mappings = crmMethods.GetManyToManyAssociationsFromRecord(this.LeadID.Value, "lead", "new_areaofinterest_lead", "new_areaofinterest");

                    this.MapManyToManyNo = new CrmNumber(mappings.Length);

                    if (this.OpportunityID != null)
                    {
                        crmMethods.CreateManyToManyAssociations(this.OpportunityID.Value, "opportunity", "new_areaofinterest_opportunity", mappings, "new_areaofinterest");
                    }
                    else
                    {
                        throw new Exception("Opportunity ID not specified for custom Workflow Action");
                    }

                    crmMethods = null;
                }
                else
                {
                    throw new Exception("Lead ID not specified for custom Workflow Action");
                }

                // Return Execution to MSCRM
                return base.Execute(executionContext);                
            }
            catch (Exception ex)
            {
                string errMessage = "MapManyToMany_FromLead_ToOpportunity (" + ex.Message + ")";

                throw new Exception(errMessage);
            }
        }

        #endregion

        #region Custom Action Inputs

        public static DependencyProperty LeadIDProperty = DependencyProperty.Register("LeadID", typeof(Lookup), typeof(MapManyToMany_FromLead_ToOpportunity));

        [CrmInput("LeadID")]
        [CrmReferenceTarget("lead")]
        public Lookup LeadID
        {
            get
            {
                return (Lookup)base.GetValue(LeadIDProperty);
            }
            set
            {
                base.SetValue(LeadIDProperty, value);
            }
        }

        public static DependencyProperty OpportunityIDProperty = DependencyProperty.Register("OpportunityID", typeof(Lookup), typeof(MapManyToMany_FromLead_ToOpportunity));

        [CrmInput("OpportunityID")]
        [CrmReferenceTarget("opportunity")]
        public Lookup OpportunityID
        {
            get
            {
                return (Lookup)base.GetValue(OpportunityIDProperty);
            }
            set
            {
                base.SetValue(OpportunityIDProperty, value);
            }
        }

        #endregion

        #region Custom Action Outputs

        public static DependencyProperty MapManyToManyNoProperty = DependencyProperty.Register("MapManyToManyNo", typeof(CrmNumber), typeof(MapManyToMany_FromLead_ToOpportunity));

        [CrmOutput("MapManyToManyNo")]
        public CrmNumber MapManyToManyNo
        {
            get
            {
                return (CrmNumber)base.GetValue(MapManyToManyNoProperty);
            }
            set
            {
                base.SetValue(MapManyToManyNoProperty, value);
            }
        }

        #endregion

    }

Plugin CRM Methods

The code for the two Custom Workflow Actions above uses a class called PluginCRMMethods to create a simple object for referencing the MSCRM Web Service through the Plugin context – this is a simple method to separate out the Data Access (i.e. MSCRM Web Service Calls) from the Business Logic that I often use for smaller Plugin Projects, and completes the sample code here.

    class PluginCRMMethods
    {
        ICrmService _service = null;

        public PluginCRMMethods(ICrmService service)
        {
            _service = service;
        }

        /// <summary>
        /// Retrieves the list of MSCRM Records that another MSCRM Record is related to
        /// via a Many-to-Many relationship.
        /// </summary>
        /// <param name="recordId">The MSCRM Record GUID Id that is related to a number of other Records</param>
        /// <param name="recordTypeName">The MSCRM Type of Record involved</param>
        /// <param name="relationshipName">The Database Name of the Many-to-Many relationship involved</param>
        /// <param name="manyToManyTypeName">The MSCRM Type of the Records that are associated through the Many-to-Many relationship</param>
        /// <returns></returns>
        public Guid[] GetManyToManyAssociationsFromRecord(System.Guid recordId, string recordTypeName, string relationshipName, string manyToManyTypeName)
        {
            try
            {
                Guid[] mappings;

                // Create a query expression                
                QueryExpression query = new QueryExpression();
                query.EntityName = manyToManyTypeName;
                query.ColumnSet = new AllColumns();

                LinkEntity le = new LinkEntity();
                le.LinkFromEntityName = manyToManyTypeName;
                le.LinkFromAttributeName = manyToManyTypeName + "id";
                le.LinkToEntityName = relationshipName;
                le.LinkToAttributeName = manyToManyTypeName + "id";

                LinkEntity le2 = new LinkEntity();
                le2.LinkFromEntityName = relationshipName;
                le2.LinkFromAttributeName = recordTypeName + "id";
                le2.LinkToEntityName = recordTypeName;
                le2.LinkToAttributeName = recordTypeName + "id";

                ConditionExpression ce = new ConditionExpression();
                ce.AttributeName = recordTypeName + "id";
                ce.Operator = ConditionOperator.Equal;
                ce.Values = new object[] { recordId };

                le2.LinkCriteria = new FilterExpression();
                le2.LinkCriteria.AddCondition(ce);

                le.LinkEntities.Add(le2);
                query.LinkEntities.Add(le);

                Microsoft.Crm.SdkTypeProxy.RetrieveMultipleRequest request = new RetrieveMultipleRequest();
                request.Query = query;
                request.ReturnDynamicEntities = true;

                Microsoft.Crm.SdkTypeProxy.RetrieveMultipleResponse response =
                    (Microsoft.Crm.SdkTypeProxy.RetrieveMultipleResponse)_service.Execute(request);

                if (response.BusinessEntityCollection != null)
                {
                    if (response.BusinessEntityCollection.BusinessEntities != null)
                    {
                        mappings = new Guid[response.BusinessEntityCollection.BusinessEntities.Count];

                        if (response.BusinessEntityCollection.BusinessEntities.Count > 0)
                        {
                            for (int n = 0; n != response.BusinessEntityCollection.BusinessEntities.Count; n++)
                            {
                                Microsoft.Crm.Sdk.DynamicEntity de = (Microsoft.Crm.Sdk.DynamicEntity)response.BusinessEntityCollection.BusinessEntities[n];
                                Microsoft.Crm.Sdk.Key mappingKey = (Microsoft.Crm.Sdk.Key)de.Properties[manyToManyTypeName + "id"];

                                Guid mappingId = mappingKey.Value;

                                mappings[n] = mappingId;
                            }
                        }

                        return mappings;
                    }
                }

                throw new Exception("No Business Entities or Collection returned from CRM Web Service");
            }
            catch (System.Web.Services.Protocols.SoapException wex)
            {
                throw new Exception("GetManyToManyAssociationsFromRecord (SOAP Exceiption : " + wex.Detail.InnerText + ")");
            }
            catch (Exception ex)
            {
                throw new Exception("GetManyToManyAssociationsFromRecord (General Exceiption : " + ex.Message + ")");
            }
            finally
            {
                // 
            }
        }

        /// <summary>
        /// Creates a series of Many-to-Many associations between a MSCRM Record and a number of other MSCRM Records
        /// </summary>
        /// <param name="recordId">The MSCRM Record GUID Id of the record to create the Many-to-Many associations against</param>
        /// <param name="recordTypeName">The MSCRM Type of Record involved</param>
        /// <param name="manyToManyRelationshipName">The Database Name of the Many-to-Many relationship involved</param>
        /// <param name="manyToManyRecords">The GUID Ids of the other MSCRM Records to relate to the [recordId] MSCRM Record through the Many-to-Many relationship</param>        
        /// <param name="manyToManyTypeName">The MSCRM Type of the Records that are associated through the Many-to-Many relationship</param>
        /// <returns></returns>
        public bool CreateManyToManyAssociations(System.Guid recordID, string recordTypeName, string manyToManyRelationshipName, System.Guid[] manyToManyRecords, string manyToManyTypeName)
        {
            try
            {
                for (int n = 0; n != manyToManyRecords.Length; n++)
                {
                    Moniker record = new Moniker();
                    record.Name = recordTypeName;
                    record.Id = recordID;

                    Moniker manyToManyRecord = new Moniker();
                    manyToManyRecord.Name = manyToManyTypeName;
                    manyToManyRecord.Id = manyToManyRecords[n];

                    AssociateEntitiesRequest request = new AssociateEntitiesRequest();
                    request.Moniker1 = record;
                    request.Moniker2 = manyToManyRecord;
                    request.RelationshipName = manyToManyRelationshipName;
                    AssociateEntitiesResponse response = (AssociateEntitiesResponse)_service.Execute(request);
                }

                return true;
            }
            catch (System.Web.Services.Protocols.SoapException wex)
            {
                throw new Exception("CreateManyToManyMappings (SOAP Exceiption : " + wex.Detail.InnerText + ")");
            }
            catch (Exception ex)
            {
                throw new Exception("CreateManyToManyMappings (General Exceiption : " + ex.Message + ")");
            }
            finally
            {
                // 
            }
        }
    }

9 Responses to Custom Workflow Action for Mapping Many-to-Many Relationships

  1. Marcos says:

    Hi, may you help me in order to do error control (manage Exceptions) inside of workflow, ie: if I have an step (that update some properties) inside of workflow, and need to detect some SQL errors (like timeout or generic SQL errors). Do you known how can I do that (for example, if occurs timeout error I would like to retry) …

    I appreciate your help.
    I’m using MSCRM 4

    Regard
    Marcos

    • Hi Marcos,

      For a Custom Workflow Action I have tended to add .NET code to record errors or other execution info in the Windows Event Log.

      Is a great article here describing the Windows Event Log and how to use it via .NET code: http://www.blackwasp.co.uk/EventLog.aspx

      This is a workman like solution which works for On-Premise deployments, for a fuller solution, you can look into creating a custom Error entity and writing new records to this entity when errors occur – have done this a few times for more technical projects. This works quite well but poses the risk that no errors will be logged if the custom code cannot communicate to CRM for some reason.

      • Marcos says:

        Thanks for your reply but your recomendation doesn’t workk for me. I need somthing like this (inside of MSCRM workflow):

        something …
        step_statement 1

        if condition error_status_step_statement1 == TRUE
        step_statement 2
        else
        step_statement 3

        something …

        Thanks

  2. PA says:

    Thanks for a great blog! 🙂 I really need this workflow assemply. Could you please explain how to use the PluginCRMMethods?

    • This PluginCRMMethods Class is one method for handling Read/Write operations to the CRM Webservice via the Plugin Context – is a simple way of separating (or encapsulating in development terms) out the Plugin/Workflow Assembly business logic from the Web Service Data Access methods. Essentially is a very simple pattern for doing CRM Development – certainly not the best pattern or anything, but have used the same pattern on a variety of CRM projects over the years that is has formed a style of ‘standard-practise’ in my way of doing things.

      Is in fact a bit of throw-back to CRM 3 when accessing the Web Service was a little more tricky (particularly in Plugins, or Callouts as they were back then) – whereas for CRM 4 or 2011 is more of a simple method for separting all the methods into a single place, which can be handy for smaller projects. (doesn’t work on larger CRM Development projects for all sorts of reasons)

      In a practical sense you create the PluginCRMMethods object via passing in the Plugin Context object that the CRM Platform provides, and then invoke the object to call different Read/Write methods, similar to the following:

      BUSINESS LOGIC
      PluginCRMMethods crmMethods = new PluginCRMMethods(context.CreateCrmService());
      int someCrmValue = crmMethods.MyMethod(5);

      CRM METHODS (DATA ACCESS)
      class PluginCRMMethods
      {
      ICrmService _service = null;

      public PluginCRMMethods(ICrmService service)
      {
      _service = service;
      }

      public int myMethod(int myParameter)
      {
      // invoke the CRM Webservice to do something with our parameter, usually to return a value or other information
      int someReturnValue = _service.Retrieve(x,x,x)
      return someReturnValue;
      }
      }

      Hope that helps

  3. Felix says:

    Thank you for the ready-to-use PluginCRMMethods, saved me for sure some time!!

  4. Justin says:

    This is exactly what I was looking for except i’m on CRM 2011. Will this code work the same?

    • Yes – the format of how CRM handles many to many relationships has not significantly changed from v4 to 2011. The syntax of the code will need to be reformatted to meet the CRM 2011 SDK for a Workflow Custom Action, but otherwise much of the code will remain the same. Give me a prod if you need any help reformatting the code, as having gone from 1.2 to 3 to 4 to 2011 you get used to keeping up to date with the SDK changes.

  5. This code will come in handy, but I think it needs some updates to work with Dynamics 2013. Or at least, it’s not plugging into Visual Studio as-is. Any hints on how to make this update easier (and also 2015-friendly)?

    Thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s