Posted on 1/30/2018 11:16:00 PM by Levin, Aric

Business Solutions  Development  Dynamics 365  Dynamics CRM 

In a few of my recent implementation I had the requirement of making modifications to many child records. Some of these processes required the use of an external SSIS package that executed a console application and ran on a schedule, but more recently I had a requirement to simply deactivate multiple child records. This of course can also be done using cascading rules, but it's a little more complicated once you involve custom filtering.

In the post I will go through the steps of creating the custom workflow activity to handle this type of request. The first thing is that we create a new class library project (.NET Framework 4.5.2), and add references to the following assemblies:
Microsoft.Crm.Sdk.Proxy, Microsoft.Xrm.Sdk, Microsoft.Xrm.Sdk.Workflow, Newtonsoft.Json and System.Activities.

We will rename our class SetStateChildRecords, and set it to inherit from the CodeActivity abstract class. The namespace declarations should include the following:

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

We will need the following Input Parameters added to the class in order to implement the solution.

#region Input/Output Parameters

/// Url of the Parent Record that we want to disable the child records for
[Input("Record Url")]
[RequiredArgument]
public InArgument<string> RecordUrl { get; set; }

/// The name of the 1:N relationship between the parent and child record
[Input("Relationship Name")]
[RequiredArgument]
public InArgument<string> RelationshipName { get; set; }

/// The name of the child entity lookup field pointing to the parent entity
[Input("Child Entity Related Field")]
[RequiredArgument]
public InArgument<string> ChildEntityRelatedFieldName { get; set; }

/// The integer value of the State Code field for deactivation (usually will be 1)
[Input("State Code")]
[RequiredArgument]
public InArgument<int> StateCode { get; set; }

/// The integer value of the Status Code field for deactivation (usually will be 2)
[Input("Status Code")]
[RequiredArgument]
public InArgument<int> StatusCode { get; set; }

#endregion

The ChildEntityRelatedFieldName is not required as this can be retrieved from the relationship, but was added here for ease of use. The StateCode and StatusCode field values allow using the same code for both Deactivation and Reactivation of child records.
An addition Input Argument called Condition can be added to add a Condition Expression to this logic, so that it will filter only a subset of the disabled records.

The next step will be to create the Execute method of the Workflow. The method will retrieve the relationship information, the child records and call an update method to update the Status of the Child Entity records.

protected override void Execute(CodeActivityContext executionContext)
{
    try
    {
        //Create the context
        IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();
        IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();
        IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

        EntityReference primaryEntity = ConvertToEntityReference(service, RecordUrl.Get<string>(executionContext));
        string relationshipName = RelationshipName.Get<string>(executionContext);

        Relationship relationship = new Relationship(relationshipName);
        RetrieveEntityRequest request = new RetrieveEntityRequest()
        {
            LogicalName = primaryEntity.LogicalName,
            EntityFilters = EntityFilters.Relationships,
            RetrieveAsIfPublished = true
        };
        RetrieveEntityResponse response = (RetrieveEntityResponse)service.Execute(request);

        string childEntityName = null;
        OneToManyRelationshipMetadata oneToNRelationship = response.EntityMetadata.OneToManyRelationships.FirstOrDefault(r => r.SchemaName == relationshipName);
        if (oneToNRelationship != null)
        {
            childEntityName = oneToNRelationship.ReferencingEntity;
        }

        if (childEntityName != null)
        {
            string parentAttributeName = ChildEntityRelatedFieldName.Get<string>(executionContext);

            EntityCollection rc = RetrieveChildEntityRecords(service, childEntityName, parentAttributeName, primaryEntity);
            if (rc.Entities.Count > 0)
            {
                int stateCode = StateCode.Get<int>(executionContext);
                int statusCode = StatusCode.Get<int>(executionContext);

                foreach (Entity entity in rc.Entities)
                {
                    UpdateEntityStatus(service, childEntityName, entity.Id, stateCode, statusCode);
                }
            }
        }
    }
    catch (FaultException<OrganizationServiceFault> ex)
    {
        throw new Exception("WorkflowUtilities.SetStateChildRecords: " + ex.Message);
    }
}

We will show ConvertToEntityReference function for the Record Url at the end. This method retrieve the name of the child entity of the relationship using the RetrieveEntityRequest and OneToManyRelationshipMetadata messages. Once all the information is contained we call the RetrieveChildRecords passing the entity name and the condition. The RetrieveChildEntityRecords is a simple query expression that has a condition where the lookup is equal to the parent record unique identifier and looks like this:

private EntityCollection RetrieveChildEntityRecords(IOrganizationService service, string entityName, string attributeName, EntityReference primaryEntity)
{
    QueryExpression query = new QueryExpression(entityName)
    {
        ColumnSet = new ColumnSet(entityName + "id"),
        Criteria = new FilterExpression(LogicalOperator.And)
        {
            Conditions =
            {
                new ConditionExpression(attributeName, ConditionOperator.Equal, primaryEntity.Id)
            }
        }
    };

    try
    {
        EntityCollection results = service.RetrieveMultiple(query);
        return results;
    }
    catch (FaultException<OrganizationServiceFault> ex)
    {
        throw new Exception("WorkflowUtilities.SetStateChildRecords.RetrieveChildEntityRecords: " + ex.Message);
    }
}

 Finally we look through the list of records, and call the UpdateEntityStatus method passing the StateCode and StatusCode values in order to call the SetStateRequest message:

private void UpdateEntityStatus(IOrganizationService service, string entityName, Guid entityId, int stateCode, int statusCode)
{
    EntityReference moniker = new EntityReference(entityName, entityId);

    SetStateRequest request = new SetStateRequest()
    {
        EntityMoniker = moniker,
        State = new OptionSetValue(stateCode),
        Status = new OptionSetValue(statusCode)
    };

    try
    {
        SetStateResponse response = (SetStateResponse)service.Execute(request);
    }
    catch (FaultException<OrganizationServiceFault> ex)
    {
        throw new Exception("WorkflowUtilities.SetStateChildRecords.RetrieveChildEntityRecords: " + ex.Message);
    }
}

 Finally, we have the ConvertToEntityReference function. The function retrieves the entity type code and record id from the Record Url, and call the RetrieveMetadataChangesRequest to get the Entity Reference for the record Url. The code is shown below.

public EntityReference ConvertToEntityReference(IOrganizationService service, string recordReference)
{
    try
    {
        var jsonEntityReference = JsonConvert.DeserializeObject<JsonEntityReference>(recordReference);
        return new EntityReference(jsonEntityReference.LogicalName, jsonEntityReference.Id);
    }
    catch (Exception e)
    {
        throw new Exception("Error converting string to EntityReference", e);
    }
}

The above method Deserializes the Json object and returns an Entity Reference for the Record Url. You can also perform the same function by Parsing the record url and calling the RetrieveMetadataChangesRequest passing and EntityQueryExpression with the object type code and if from the actual Url.

The JsonEntityReference above is simply a class with two get/set properties (containing Json Attributes):

public class JsonEntityReference
{
    [JsonProperty(PropertyName = "entityType")]
    public string LogicalName { get; set; }

    [JsonProperty(PropertyName = "id")]
    public Guid Id { get; set; }
}

This is basically it. You can perform a lot of different workflow activities that manipulate relationships and multiple parent/child records, and even pass condition expression or complete fetchXml queries. The screenshot below shows the Custom Workflow window.

Set State Child Records Custom Workflow Activity
I would like to thank Andrew Butenko for some guidance in this area, and for proving the community with Ultimate Workflow Toolkit.


Navigate to original post to enter comments