Tuesday, June 25, 2019

How to create a recurring workflow in dynamic 365 CRM?


Business Problem Statement

There was a need to mark all Open Clinic Appointment in a completed state at the end of every day. This is by government law that once a prescription or an advice is generated by doctor for a specific customer, no further modification are allowed.

Technical Options

We can achieve this by having a wait condition in workflow for all open clinic appointment and after certain interval, we can auto close them. But imagine how many workflow instances might be running from the date when clinic appointment got created into the system.

Then I came to know about a blog post https://alexanderdevelopment.net/post/2013/05/18/scheduling-recurring-dynamics-crm-workflows-with-fetchxml having a smart mechanism to schedule a recurring workflow, which is also very easy to configure.

Solution

Here, I will showcase the configuration steps for deactivating the Clinic Appointment daily. 
This involves following components

  • A child workflow to Close a Clinic Appointment in completed state.
  • A Custom entity (Recurring Process) which contains workflow scheduling details and a fetchXml.
  • A Custom Workflow (.Net Code) which is uses this configuration data to fetch actual clinic appointments and execute the child workflow to mark appointment close. 
  • A workflow which will run daily and get trigger and registered on custom entity (Recurring Process)

1) Create a Child Workflow to close or complete the clinic appointment.




2) Next is to configure a custom entity (Recurring Process) with scheduling details and FetchXml for applicable records. (Get installed from solution provided or you can create such custom entity.



3) A workflow (Recurring Workflow Runner) to run on a recurring schedule.


Click on Set Properties for RunScheduledWorkflows

Click on Set Properties – Update Recurring Process for Frequency equals Hourly.


Last, the custom workflow (RunScheduledWorkflow) get installed from the Alex Solution. The code of the workflow is also shared to public.


using System;
using System.Activities;
using System.ServiceModel;
//using System.Globalization;
//using System.Runtime.Serialization;
using System.IO;
using System.Text;
//using System.Net;
using System.Xml;
using Microsoft.Xrm.Sdk;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Workflow;
using Microsoft.Xrm.Sdk.Query;
using System.Linq;

namespace Company.Workflows
{
public sealed class RunScheduledWorkflows : CodeActivity
{
    [RequiredArgument]
    [Input("FetchXML query")]
    public InArgument FetchXMLQuery { get; set; }

    [RequiredArgument]
    [Input("Workflow")]
    [ReferenceTarget("workflow")]
    public InArgument Workflow { get; set; }

    [RequiredArgument]
    [Input("Page size")]
    public InArgument PageSize { get; set; }

    //name of your custom workflow activity for tracing/error logging
    private string _activityName = "RunRecurringProcess";

    ///

    /// Executes the workflow activity.
    ///

    /// executionContext">The execution context.
    protected override void Execute(CodeActivityContext executionContext)
    {
        // Create the tracing service
        ITracingService tracingService = executionContext.GetExtension();

        if (tracingService == null)
        {
            throw new InvalidPluginExecutionException("Failed to retrieve tracing service.");
        }

        tracingService.Trace("Entered " + _activityName + ".Execute(), Activity Instance Id: {0}, Workflow Instance Id: {1}",
            executionContext.ActivityInstanceId,
            executionContext.WorkflowInstanceId);

        // Create the context
        IWorkflowContext context = executionContext.GetExtension();

        if (context == null)
        {
            throw new InvalidPluginExecutionException("Failed to retrieve workflow context.");
        }

        tracingService.Trace(_activityName + ".Execute(), Correlation Id: {0}, Initiating User: {1}",
            context.CorrelationId,
            context.InitiatingUserId);

        IOrganizationServiceFactory serviceFactory = executionContext.GetExtension();
        IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

        try
        {
            //create empty entitycollection for later
            EntityCollection recordsToProcess = new EntityCollection();

            // set page size
            int fetchCount = PageSize.Get(executionContext);

            // Initialize the page number.
            int pageNumber = 1;

            // Specify the current paging cookie. For retrieving the first page,
            // pagingCookie should be null.
            string pagingCookie = null;

            //execute query and page through results
            while (true)
            {
                // Build fetchXml string with the placeholders.
                string fetchXml = CreateXml(FetchXMLQuery.Get(executionContext), pagingCookie, pageNumber, fetchCount);

                EntityCollection retrieved = service.RetrieveMultiple(new FetchExpression(fetchXml));

                //add retrieved records to our main entitycollection
                recordsToProcess.Entities.AddRange(retrieved.Entities);

                if (retrieved.MoreRecords)
                {
                    // Increment the page number to retrieve the next page.
                    pageNumber++;

                    // Set the paging cookie to the paging cookie returned from current results.                           
                    pagingCookie = retrieved.PagingCookie;
                }
                else
                {
                    // If no more records in the result nodes, exit the loop.
                    break;
                }
            }

            //loop through results
            recordsToProcess.Entities.ToList().ForEach(a =>
            {
                ExecuteWorkflowRequest request = new ExecuteWorkflowRequest
                {
                    EntityId = a.Id,
                    WorkflowId = (Workflow.Get(executionContext)).Id
                };

                service.Execute(request);  //run the workflow
            });


        }
        catch (FaultException e)
        {
            tracingService.Trace("Exception: {0}", e.ToString());

            // Handle the exception.
            throw;
        }
        catch (Exception e)
        {
            tracingService.Trace("Exception: {0}", e.ToString());
            throw;
        }

        tracingService.Trace("Exiting " + _activityName + ".Execute(), Correlation Id: {0}", context.CorrelationId);
    }

    ///

    /// used to enable paging in the fetchxml queries - https://msdn.microsoft.com/en-us/library/gg328046.aspx
    ///

    /// xml">
    /// cookie">
    /// page">
    /// count">
    ///
    public string CreateXml(string xml, string cookie, int page, int count)
    {
        StringReader stringReader = new StringReader(xml);
        XmlTextReader reader = new XmlTextReader(stringReader);

        // Load document
        XmlDocument doc = new XmlDocument();
        doc.Load(reader);

        return CreateXml(doc, cookie, page, count);
    }

    ///

    /// used to enable paging in the fetchxml queries - https://msdn.microsoft.com/en-us/library/gg328046.aspx
    ///

    /// doc">
    /// cookie">
    /// page">
    /// count">
    ///
    public string CreateXml(XmlDocument doc, string cookie, int page, int count)
    {
        XmlAttributeCollection attrs = doc.DocumentElement.Attributes;

        if (cookie != null)
        {
            XmlAttribute pagingAttr = doc.CreateAttribute("paging-cookie");
            pagingAttr.Value = cookie;
            attrs.Append(pagingAttr);
        }

        XmlAttribute pageAttr = doc.CreateAttribute("page");
        pageAttr.Value = System.Convert.ToString(page);
        attrs.Append(pageAttr);

        XmlAttribute countAttr = doc.CreateAttribute("count");
        countAttr.Value = System.Convert.ToString(count);
        attrs.Append(countAttr);

        StringBuilder sb = new StringBuilder(1024);
        StringWriter stringWriter = new StringWriter(sb);

        XmlTextWriter writer = new XmlTextWriter(stringWriter);
        doc.WriteTo(writer);
        writer.Close();

        return sb.ToString();
    }
}

}

Thanks Alex, for sharing such a good workflow logic.

Regards,
Vipin Jaiswal
vipinjaiswal12@gmail.com

No comments: