Dynamics 365 Workflow Activity – Set MultiSelect Optionset field by a Workflow

In Dynamics 365, you can’t change value of a MultiSelect Optionset filed in a Workflow or Business Rule as out of box. But by using below code you can create a Custom Workflow Activity to set any MultiSelect Optionset field in any entity. It’s fully generic.

  • Target Record URL (required) : URL of the record to be updated.
  • Attribute Name: (required) : logical name of the attribute to be updated.
  • Attribute Values: (required) : option set values to be set. They should be separated by comma. In case you want to clear the values, just use an empty string like “”.
  • Keep Existing Values (optional): indicate if the existing selected values in the target multi-select optionset attribute will be maintained. By default, it will remove the existing values and assign the new ones given by the argument “Attributes Values”.

SetMultiSelectOptionSet.cs

using D365_Core_Workflows.Helpers;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Sdk.Workflow;
using System;
using System.Activities;

namespace D365_Core_Workflows.WorkflowActivities
{
    public class SetMultiSelectOptionSet : CodeActivity
    {
        #region WFA Parameters

        [RequiredArgument]
        [Input("Target Record URL")]
        public InArgument<string> TargetRecordUrl { get; set; }

        [RequiredArgument]
        [Input("Attribute Name")]
        public InArgument<string> AttributeName { get; set; }

        [RequiredArgument]
        [Input("Attribute Values")]
        public InArgument<string> AttributeValues { get; set; }

        [Input("Keep Existing Values")]
        [Default("false")]
        public InArgument<Boolean> KeepExistingValues { get; set; }

        #endregion WFA Parameters

        #region Service Parameters
        private ITracingService tracingService;
        private IWorkflowContext context;
        private IOrganizationServiceFactory serviceFactory;
        private IOrganizationService service;
        #endregion Service Parameters

        protected override void Execute(CodeActivityContext executionContext)
        {
            #region Initializing Services
            try
            {
                tracingService = executionContext.GetExtension<ITracingService>();
                context = executionContext.GetExtension<IWorkflowContext>();
                serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
                service = serviceFactory.CreateOrganizationService(context.UserId);

                tracingService.Trace("Services are initialized.");
            }
            catch (Exception e)
            {

                throw new InvalidOperationException($"There was an error during initializing the services: {e.Message}");
            }
            #endregion Initializing Services

            EntityReference sourceEntityReference = GetTargeteEntityReference(executionContext);
            string attributeName = GetAttributeName(executionContext);
            OptionSetValueCollection newValues = GetNewAttributeValues(executionContext);
            OptionSetValueCollection existingValues = GetExistingAttributeValues(sourceEntityReference, attributeName, executionContext);

            UpdateRecord(sourceEntityReference, attributeName, newValues, existingValues);
        }

        private EntityReference GetTargeteEntityReference(CodeActivityContext executionContext)
        {
            string sourceRecordUrl = TargetRecordUrl.Get<string>(executionContext) ?? throw new ArgumentNullException("Source URL is empty");
            tracingService.Trace("Source Record URL:'{0}'", sourceRecordUrl);
            return new DynamicUrlParser(sourceRecordUrl).ToEntityReference(service);
        }

        private string GetAttributeName(CodeActivityContext executionContext)
        {
            string attributeName = AttributeName.Get<string>(executionContext) ?? throw new ArgumentNullException("Attribute Name is empty");
            tracingService.Trace("Attribute name:'{0}'", attributeName);
            return attributeName;
        }

        private OptionSetValueCollection GetNewAttributeValues(CodeActivityContext executionContext)

        {
            string attributeValues = AttributeValues.Get<string>(executionContext) ?? throw new ArgumentNullException("Attribute Values is empty");
            tracingService.Trace("Attribute Values:'{0}'", attributeValues);

            if (string.IsNullOrEmpty(attributeValues))
            {
                tracingService.Trace("No values found. Setting attribute to null");
                return new OptionSetValueCollection();
            }

            string[] values = attributeValues.Split(',');

            if (values == null || values.Length == 0)
            {
                tracingService.Trace("No values found in array. Setting attribute to null");
                return new OptionSetValueCollection();
            }

            OptionSetValueCollection optionSetValueCollection = new OptionSetValueCollection();

            foreach (string value in values)
            {
                if (int.TryParse(value, out int intValue))
                {
                    tracingService.Trace("Value '{0}' added correctly", value);
                    optionSetValueCollection.Add(new OptionSetValue(intValue));
                }
                else
                {
                    tracingService.Trace("Value '{0}' couldn't be parsed", value);
                }
            }

            return optionSetValueCollection;
        }

        private OptionSetValueCollection GetExistingAttributeValues(EntityReference targetEntityReference, string attributeName, CodeActivityContext executionContext)
        {
            tracingService.Trace("Retrieving existing values");

            Boolean attributeValues = KeepExistingValues.Get<Boolean>(executionContext);

            if (attributeValues == false)
                return null;

            Entity record = service.Retrieve(targetEntityReference.LogicalName, targetEntityReference.Id, new ColumnSet(new string[] { attributeName }));

            tracingService.Trace("Existing values have been retrieved correctly");

            if (record.Contains(attributeName))
                return record[attributeName] as OptionSetValueCollection;
            else
                return null;
        }

        private void UpdateRecord(EntityReference targetEntityReference, string attributeName, OptionSetValueCollection newValues, OptionSetValueCollection existingValues)
        {
            if (targetEntityReference == null || attributeName == null || newValues == null)
                throw new ArgumentNullException(string.Format("Unexpected null parameters when trying to update record. Record reference '{0}' - attibute name '{1}' - values '{2}'", targetEntityReference, attributeName, newValues));


            Entity targetEntity = new Entity(targetEntityReference.LogicalName, targetEntityReference.Id);
            targetEntity[attributeName] = MergeOptionSetCollections(newValues, existingValues);

            service.Update(targetEntity);

            tracingService.Trace("Multi-select option set attribute '{0}' has been updated correctly for the record type '{1}' with id '{2}'", attributeName, targetEntityReference.LogicalName, targetEntityReference.Id);
        }

        private OptionSetValueCollection MergeOptionSetCollections(OptionSetValueCollection newValues, OptionSetValueCollection existingValues)
        {
            tracingService.Trace("Merging new and exiting multi-select optionset values");

            if (existingValues == null && newValues == null)
                return new OptionSetValueCollection();

            if (existingValues == null)
                return newValues;

            if (newValues == null)
                return existingValues;

            foreach (OptionSetValue newValue in newValues)
            {
                if (!existingValues.Contains(newValue))
                    existingValues.Add(newValue);
            }

            tracingService.Trace("New and existing multi-select optionset values have been merged correctly. Total options: {0} ", existingValues.Count);
            return existingValues;
        }
    }
}

Helpers.cs

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata.Query;
using Microsoft.Xrm.Sdk.Query;
using System;

namespace D365_Core_Workflows.Helpers
{
    public class DynamicUrlParser
    {
        public string Url { get; private set; }
        public int EntityTypeCode { get; private set; }
        public Guid Id { get; private set; }

        public DynamicUrlParser(string url)
        {
            try
            {
                Url = url;
                var uri = new Uri(url);
                int found = 0;

                string[] parameters = uri.Query.TrimStart('?').Split('&');
                foreach (string param in parameters)
                {
                    var nameValue = param.Split('=');
                    switch (nameValue[0])
                    {
                        case "etc":
                            EntityTypeCode = int.Parse(nameValue[1]);
                            found++;
                            break;
                        case "id":
                            Id = new Guid(nameValue[1]);
                            found++;
                            break;
                    }
                    if (found > 1) break;
                }
            }
            catch (Exception ex)
            {
                throw new Exception(String.Format("Url '{0}' is incorrectly formated for a Dynamics CRM Dynamics Url", url), ex);
            }
        }

        public string GetEntityLogicalName(IOrganizationService service)
        {
            var entityFilter = new MetadataFilterExpression(LogicalOperator.And);
            entityFilter.Conditions.Add(new MetadataConditionExpression("ObjectTypeCode ", MetadataConditionOperator.Equals, this.EntityTypeCode));
            var propertyExpression = new MetadataPropertiesExpression { AllProperties = false };
            propertyExpression.PropertyNames.Add("LogicalName");
            var entityQueryExpression = new EntityQueryExpression()
            {
                Criteria = entityFilter,
                Properties = propertyExpression
            };

            var retrieveMetadataChangesRequest = new RetrieveMetadataChangesRequest()
            {
                Query = entityQueryExpression
            };

            var response = (RetrieveMetadataChangesResponse)service.Execute(retrieveMetadataChangesRequest);

            if (response.EntityMetadata.Count == 1)
            {
                return response.EntityMetadata[0].LogicalName;
            }
            return null;
        }

        public EntityReference ToEntityReference(IOrganizationService service)
        {
            return new EntityReference(this.GetEntityLogicalName(service), this.Id);
        }
    }
}

One thought on “Dynamics 365 Workflow Activity – Set MultiSelect Optionset field by a Workflow

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 )

Connecting to %s