It is often the case, as part of any application or database system, that certain record types will be well-suited towards duplication. Whilst this is generally a big no-no for individual customer records or invoice details, for example, there are other situations where the ability to duplicate and slightly modify an existing record becomes incredibly desirable. This is then expanded further to the point where end-users are given the ability to perform such duplication themselves.
A good example of this can be found within Dynamics CRM/Dynamics 365 for Enterprise (CRM/D365E). Email Templates are, in essence, a record type that is duplicated whenever a user selects the Template and creates a new Email record from within the application. Whilst there will always be details that need to be modified once the duplication is performed, having the ability to essentially “copy + paste” an existing record can generate the following benefits for a business:
- Streamlining and adherence to business processes
- Efficiency savings
- Brand consistency
CRM/D365E does a pretty good job of making available a number of record types designed solely for this purpose, but a recent real-life example demonstrated a potential gap. A business I was working with was implementing the full sales process within the application - Lead to Opportunity to Quote etc. At the Quote stage, the businesses existing process would generally involve having a number of predefined “templates” for Quotes. This was due to the fact that the business would very regularly quote for the same kind of work, often with little or no variation. Out of the box with CRM/D365E, the sales team would have to create a new Quote record and add on every Quote Product line item each time a Quote was required - leading to little or no efficiency benefit of using the application.
To get around the issue, I was tasked with creating a means of setting up a number of “template” Quote records and then have the ability to quickly copy these template records, along with all of their associated Quote Product records, with some minor details changed in the process (for example, the Name value of the Quote). A workflow is immediately the best candidate for addressing the second requirement of this task but would require some additional development work to bring to fruition. I decided then to look, rather nervously, at creating a custom workflow assembly.
Why nervously? To be frank, although I have had plenty experience to date with writing plugins for CRM/D365E, I had not previously developed a custom workflow assembly. So I was a little concerned that the learning curve involved would be steep and take much longer than first anticipated. Fortunately, my fears were unfounded, and I was able to grasp the differences between a plugin and a custom workflow assembly very quickly:
- Instead of inheriting from the IPlugin interface, your class instead needs to be set to the CodeActivity interface. As with plugins and, depending on Visual Studio version, you can then use CTRL + . to implement your Execute method.
- Context (i.e. the information regarding the who, what and why of the execution; the User, the Entity and the action) is derived from the IWorkflowContext as opposed to the IPluginExecutionContext
- Input/Output Parameters are specified within your Execute method and can be given a label, a target entity and then information regarding the data type that will be passed in/out. For example, to specify an Input Parameter for a Quote EntityReference, with the label Quote Record to Copy, you can use the following snippet:
The rest is as you would expect when writing a C# plugin. It is good to know that the jump across from plugins to custom workflow assemblies is not too large, so I would encourage anyone to try writing one if they haven’t done so already.
Back to the task at hand…
I implemented the appropriate logic within the custom workflow assembly to first create the Quote, using a Retrieve request to populate the quote variable with the Entity details and fields to copy over:
The important thing to remember with this is that you must set the ID of the record to blank and then remove it from the newQuote - otherwise, your code will attempt to create the new record with the existing GUID of the copied record, resulting in an error.
Next, I performed a RetrieveMultiple request based off a QueryExpression to return all Quote Product records related to the existing records. Once I had my results in my qp EntityCollection, I then implemented my logic as follows:
After deploying to CRM and setting up the corresponding Workflow that referenced the assembly, I began testing. I noticed that the Workflow would occasionally fail on certain Quote records, with the following error message:
The plug-in execution failed because the operation has timed-out at the Sandbox Client.System.TimeoutException
The word Sandbox immediately made me think back to some of the key differences between CRM/D365E Online and On-Premise version, precisely the following detail pertaining to custom code deployed to Online versions of the application - it must always be deployed in Sandbox mode which, by default, only allows your code to process for 2 minutes maximum. If it exceeds this, the plugin/workflow will immediately fail and throw back the error message above. Upon closer investigation, the error was only being thrown for Quote records that had a lot of Quote Products assigned to them. I made the assumption that the reason why the workflow was taking longer than 2 minutes is because my code was performing a Create request into CRM for every Quote Product record and, as part of this, only proceeding to the next record once a success/failure response was returned from the application.
The challenge was therefore to find an alternative means of creating the Quote Product records without leading the Workflow to fail. After doing some research, I came across a useful MSDN article and code example that utilised the ExecuteMultipleRequest message:
You can use the ExecuteMultipleRequest message to support higher throughput bulk message passing scenarios in Microsoft Dynamics 365 (online & on-premises), particularly in the case of Microsoft Dynamics 365 (online) where Internet latency can be the largest limiting factor. ExecuteMultipleRequest accepts an input collection of message Requests, executes each of the message requests in the order they appear in the input collection, and optionally returns a collection of Responses containing each message’s response or the error that occurred.
Throwing caution to the wind, I repurposed my code as follows, in this instance choosing not to return a response for each request:
Thankfully, after re-testing, we no longer encountered the same errors on our particularly large Quote test records.
As a learning experience, the above has been very useful in showcasing how straightforward custom workflow assemblies are when coming from a primarily plugin development background. In addition, the above has also presented an alternative method for creating batch records within CRM/D365E, in a way that will not cause severe performance detriment. I was surprised, however, that there is no out of the box means of quickly copying existing records, thereby requiring an approach using code to resolve. Quotes are an excellent example of an Entity that could benefit from Template-isation in the near future, in order to expedite common order scenarios and help prevent carpel tunnel syndrome from CRM users the world over :)