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
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