Dynamics CRM/Dynamics 365 for Customer Engagement (CRM/D365CE) is an incredibly flexible application for the most part. Regardless of how your business operates, you can generally tailor the system to suit your requirements and extend it to your heart’s content; often to the point where it is completely unrecognisable from the base application. Notwithstanding this argument, you will come across aspects of the application that are (literally) hard-coded to behave a certain way and cannot be straightforwardly overridden via the application interface. The most recognisable example of this is the Lead Qualification process. You are heavily restricted in how this piece of functionality acts by default but, thankfully, there are ways in which it can be modified if you are comfortable working with C#, JScript and Ribbon development.

Before we can start to look at options for tailoring the Lead Qualification process, it is important to understand what occurs during the default action within the application. In developer-speak, this is generally referred to as the QualifyLead message and most typically executes when you click the button below on the Lead form:

When called by default, the following occurs:

  • The Status/Status Reason of the Lead is changed to Qualified, making the record inactive and read-only.
  • A new OpportunityContact and Account record is created and populated with (some) of the details entered on the Lead record. For example, the Contact record will have a First Name/Last Name value supplied on the preceding Lead record.
  • You are automatically redirected to the newly created Opportunity record.

This is all well and good if you are able to map your existing business processes to the application, but most organisations will typically differ from the applications B2B orientated focus. For example, if you are working within a B2C business process, creating an Account record may not make sense, given that this is typically used to represent a company/organisation. Or, conversely, you may want to jump straight from a Lead to a Quote record. Both of these scenarios would require bespoke development to accommodate currently within CRM/D365CE. This can be broadly categorised into two distinct pieces of work:

  1. Modify the QualifyLead message during its execution to force the desired record creation behaviour.
  2. Implement client-side logic to ensure that the user is redirected to the appropriate record after qualification.

The remaining sections of this post will demonstrate how you can go about achieving the above requirements in two different ways.

Our first step is to “intercept” the QualifyLead message at runtime and inject our own custom business logic instead

I have seen a few ways that this can be done. One way, demonstrated here by the always helpful Jason Lattimer, involves creating a custom JScript function and a button on the form to execute your desired logic. As part of this code, you can then specify your record creation preferences. A nice and effective solution, but one in its guise above will soon obsolete as a result of the SOAP endpoint deprecation. An alternative way is to instead deploy a simplistic C# plugin class that ensures your custom logic is obeyed across the application, and not just when you are working from within the Lead form (e.g. you could have a custom application that qualifies leads using the SDK). Heres how the code would look in practice:

public void Execute(IServiceProvider serviceProvider)
    {
        //Obtain the execution context from the service provider.

        IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

        if (context.MessageName != "QualifyLead")
            return;

        //Get a reference to the Organization service.

        IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
        IOrganizationService service = factory.CreateOrganizationService(context.UserId);

        //Extract the tracing service for use in debugging sandboxed plug-ins

        ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

        tracingService.Trace("Input parameters before:");
        foreach (var item in context.InputParameters)
        {
            tracingService.Trace("{0}: {1}", item.Key, item.Value);
        }

        //Modify the below input parameters to suit your requirements.
        //In this example, only a Contact record will be created
        
        context.InputParameters["CreateContact"] = true;
        context.InputParameters["CreateAccount"] = false;
        context.InputParameters["CreateOpportunity"] = false;

        tracingService.Trace("Input parameters after:");
        foreach (var item in context.InputParameters)
        {
            tracingService.Trace("{0}: {1}", item.Key, item.Value);
        }
    }

To work correctly, you will need to ensure this is deployed out on the Pre-Operation stage, as by the time the message reaches the Post-Operation stage, you will be too late to modify the QualifyLead message.

The next challenge is to handle the redirect to your record of choice after Lead qualification

Jason’s code above handles this effectively, with a redirect after the QualifyLead request has completed successfully to the newly created Account (which can be tweaked to redirect to the Contact instead). The downside of the plugin approach is that this functionality is not supported. So, if you choose to disable the creation of an Opportunity record and then press the Qualify Lead button…nothing will happen. The record will qualify successfully (which you can confirm by refreshing the form) but you will then have to manually navigate to the record(s) that have been created.

The only way around this with the plugin approach is to look at implementing a similar solution to the above – a Web API request to retrieve your newly created Contact/Account record and then perform the necessary redirect to your chosen entity form:

function redirectOnQualify() {

    setTimeout(function(){
        
        var leadID = Xrm.Page.data.entity.getId();

        leadID = leadID.replace("{", "");
        leadID = leadID.replace("}", "");

        var req = new XMLHttpRequest();
        req.open("GET", Xrm.Page.context.getClientUrl() + "/api/data/v8.0/leads(" + leadID + ")?$select=_parentaccountid_value,_parentcontactid_value", true);
        req.setRequestHeader("OData-MaxVersion", "4.0");
        req.setRequestHeader("OData-Version", "4.0");
        req.setRequestHeader("Accept", "application/json");
        req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
        req.setRequestHeader("Prefer", "odata.include-annotations=\"OData.Community.Display.V1.FormattedValue\"");
        req.onreadystatechange = function () {
            if (this.readyState === 4) {
                req.onreadystatechange = null;
                if (this.status === 200) {
                    var result = JSON.parse(this.response);
                    
                    //Uncomment based on which record you which to redirect to.
                    //Currently, this will redirect to the newly created Account record
                    var accountID = result["_parentaccountid_value"];
                    Xrm.Utility.openEntityForm('account', accountID);

                    //var contactID = result["_parentcontactid_value"];
                    //Xrm.Utility.openEntityForm('contact', contactID);

                }
                else {
                    alert(this.statusText);
                }
            }
        };
        req.send();
        
    }, 6000);     
}

The code is set to execute the Web API call 6 seconds after the function triggers. This is to ensure adequate time for the QualifyLead request to finish and make the fields we need available for accessing.

To deploy out, we use the eternally useful Ribbon Workbench to access the existing Qualify Lead button and add on a custom command that will fire alongside the default one:

As this post has hopefully demonstrated, overcoming challenges within CRM/D365CE can often result in different – but no less preferred – approaches to achieve your desired outcome. Let me know in the comments below if you have found any other ways of modifying the default Lead Qualification process within the application.

When working with applications day in, day out, you sometimes overlook something that is sitting there, staring at you in the face. It may be an important feature or an inconsequential piece of functionality, but you never really take the time to fully understand either way just what it is and whether it can offer any distinct benefits or assistance. I realised a great example of this when recently deploying some new Plug-ins into Dynamics CRM/Dynamics 365 for Enterprise (CRM/D365E). When you are setting up a new Step for your Plug-in, you are given the option of specifying an Unsecure Configuration and Secure Configuration via a multi-line text box to the right of the window:

1

I was curious about just what these are and why it is not something that you ever really come across when you are first learning about Plug-in development with the application. I took a closer look at what these text boxes do and, as part of this week’s blog post, I wanted to share my findings and provide a demonstration of how they work in practice.

The Theoretical Bit: Unsecure/Secure Configuration Overview

Typically, when we want to get some juicy information relating to a piece of CRM/D365E functionality, we would turn to our good friends TechNet or MSDN. In this instance, however, there is no dedicated page that covers this topic in-depth. We must instead navigate to the Write a Plug-in Constructor page to find dedicated information about how these work:

The Microsoft Dynamics 365 platform supports an optional plug-in constructor that accepts either one or two string parameters. If you write a constructor like this, you can pass any strings of information to the plug-in at run time.

These “one or two” parameters are the multi-line text boxes indicated above. Information is exposed as string objects within you C# code and you enable this feature within your code by specifying the following, SDK adapted constructor within your Plug-in class:

public MyPlugin(string unsecureString, string secureString)
    {
        if (String.IsNullOrWhiteSpace(unsecureString) ||
            String.IsNullOrWhiteSpace(secureString))
            {
                throw new InvalidPluginExecutionException("Unsecure and secure strings are required for this plugin to execute.");
            }

            _unsecureString = unsecureString;
            _secureString = secureString;
    }

As with anything, there are a number of important caveats to bear in mind with this feature. These can be gleaned via additional online sources:

In terms of use cases, the above articles highlight some potential scenarios that they are best utilised within. Perhaps the best example is for an ISV solution that requires integration with external web services to retrieve data that is then consumed by CRM/D365E. Credentials for these web services can be stored securely when the Plug-in is deployed via the use of Secure configuration parameters. Other than that, if you are developing a Plug-in for internal use, that is unlikely to be deployed/managed across multiple environments, then it is probably not worthwhile to look at utilising configuration parameters when you can just as easily specify these within your code.

Practice Makes (for) Perfect (Understanding)!

The best way to see how something works is by getting hands-on and seeing how it works in action. Let’s assume you wish to deploy a plugin that executes whenever a record is opened/viewed by any user across the platform. The plugin should update the First Name (firstname) and Last Name (lastname) fields to match the value(s) in the Unsecure and Secure Configuration properties accordingly. The below plugin code will achieve these requirements:

using System;
using Microsoft.Xrm.Sdk;

namespace D365.BlogDemoAssets.Plugins
{
    public class PostContactRetrieve_PluginConfigurationTest : IPlugin
    {
        private readonly string _unsecureString;
        private readonly string _secureString;
        public PostContactRetrieve_PluginConfigurationTest(string unsecureString, string secureString)
        {
            if (String.IsNullOrWhiteSpace(unsecureString) ||
                String.IsNullOrWhiteSpace(secureString))
            {
                throw new InvalidPluginExecutionException("Unsecure and secure strings are required for this plugin to execute.");
            }

            _unsecureString = unsecureString;
            _secureString = secureString;
        }
        public void Execute(IServiceProvider serviceProvider)
        {
            // Obtain the execution context from the service provider.

            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            // Obtain the organization service reference.
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

            // The InputParameters collection contains all the data passed in the message request.
            if (context.InputParameters.Contains("Target") &&
                context.InputParameters["Target"] is EntityReference)
            {
                Entity contact = new Entity("contact", ((EntityReference)context.InputParameters["Target"]).Id);
                
                contact["firstname"] = _unsecureString;
                contact["lastname"] = _secureString;
                service.Update(contact);

            }
        }
    }
}

When deploying the plugin using the Plugin Registration Tool, we specify the step to execute on the Retrieve message and to execute in the Pre-Operation Stage (otherwise the form will need to be refreshed to see the updated values!). We also need to specify our desired values for the First Name and Last Name fields in the appropriate Configuration fields. The Register New Step window should look similar to the below if configured correctly:

When we navigate into the Jim Glynn (sample) Contact record within CRM/D365E, we can see that the Plug-in has triggered successfully and updated the fields to match against the values specified on Step above:

We can also confirm that the appropriate error is thrown when one of the configuration properties is missing a value, by modifying our Plug-in step and attempting to reload our sample Contact record:

Can you spot what’s missing? 🙂

By clicking Download Log File, we can view the error message specified as part of the InvalidPluginExecutionException call. Below is a modified excerpt of the ErrorDetails XML that is generated:

  <InnerFault>
    <ActivityId>ed4a2021-9c87-4f06-a493-6d804676bf96</ActivityId>
    <ErrorCode>-2147220891</ErrorCode>
    <ErrorDetails xmlns:d3p1="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
    <Message>Unsecure and secure strings are required for this plugin to execute.</Message>
    <ExceptionSource i:nil="true" />
    <InnerFault i:nil="true" />
    <OriginalException i:nil="true" />
    <TraceText i:nil="true" />
  </InnerFault>

Conclusions or Wot I Think

It is impossible to become what I would like to term a “pub quiz champion” in CRM/D365E; what I mean by this is that I would defy anyone to rattle off every little detail and fact about the entire platform. As with any pub-quiz, those that do may more than likely end up cheating by having their phone out. With this metaphor in mind, I think Plug-in configuration properties would be an excellent topic for a quiz of this nature. As mentioned previously, it is not something that I was ever made aware of when starting to learn about Plug-in development and is not a feature touted regularly within the online community. Perhaps this is because of its very specific and limited application – although it is handy to have at our disposal, I think its usage is really only targeted towards those who are developing solutions that are deployed across multiple environments AND require the need to store configuration properties for external URL’s/web services in a compact and secure manner. Therefore, if you are currently having to use a custom entity within the application to store this type of information, it would make sense to reduce the footprint of your solution within the application itself and make the appropriate changes to use Secure configuration parameters instead. Using a bit of ingenuity (such as XML configuration parameters), you can achieve the same requirements without the need to customise the application unnecessarily.

Generally, when you are looking at adopting Dynamics CRM/Dynamics 365 for Enterprise (D365E) within your business, you can be reasonably satisfied that the majority of what is already configured within the system can be very quickly adapted to suit your business needs. Whether it’s the Lead to Opportunity sales process or the entire Case management module, the functionality at your disposal is suitable for many organisations across the globe. The great thing as well is that, should you wish to fine-tune things further, you have a broad range of options at your disposal that can help you achieve your objectives – sometimes in very specific and highly unique ways. I have previously looked at a good example of this on the blog – namely, how to override the systems built-in pricing engine in favour of your own – and, assuming you have a good understanding of C# and how to deploy plugins to the application, you can spin an important aspect of the systems functionality on its head to match how your business operates.

Having this ability is, undoubtedly, a real boon, but can present some odd behaviours. For example, you may start to notice that suddenly the Extended Amount field is no longer being populated with data after implementing your custom pricing engine. The example pictures below demonstrate a before and after example of adding a Product line item to the Quote entity, using the exact same sample Product:

Before…

…and after.

The odd thing about this is that, as soon as you click into the record, you will suddenly see a value appear in this field. Very strange!

It is difficult to pinpoint exactly what is causing the problem, but I can do a “stab in the dark”. CRM/D365E uses the CalculatePrice message to determine the points when either a) the default price engine or b) a custom one is triggered to perform all necessary calculations. Although there is no official documentation to back this up, I suspect that this message is only triggered when you Update or Retrieve an existing Product line item record (regardless of whether it is an Opportunity Product, Quote Product etc.). This is proven by the fact that, as soon as we click into our Product record, the Extended Amount field is suddenly populated – the platform has triggered the Retrieve message as a result of you opening the record and then, as a next step, forces the CalculatePrice message to also fire. The important thing to clarify with this point is that you must have a custom pricing implemented successfully within the application for this to work. Otherwise, don’t be too surprised if the Extended Amount value remains at 0.

Whilst the workaround for this is somewhat tolerable if you are working with a small subset of records and do not rely on the Extended Amount as part of any existing reporting within the application, this could really start to cause problems for your end users in the long term and give an impression that the application does not “work” as it should do. Fortunately, there is a solution that we can look at implementing that will hopefully lead to some happy fingers from not needing to click into records anymore 🙂 Be sure to have the CRM/D365E SDK handy before you begin the below!

  1. Open up the Plugin Registration Tool from within the SDK, and log into your CRM/D365E instance.
  2. Scroll down to your Assembly and Plugin that contains your custom pricing engine. If already configured correctly, it should have a step configured for the CalculatePrice message on any entity, as a Synchronous, Post-Operation step.
  3. Right click your plugin and click on Register New Step to open the window that lets you specify the required settings for your step. Populate the form as follows:
    • Message: Create
    • Primary Entity: Select one of the Product line item entities that your custom pricing engine uses. The list of accepted entities are invoicedetailopportunityproduct, salesorderdetail or quotedetail.
    • Event Pipeline Stage of Execution: Post-Operation
    • Execution Mode: Synchronous

All other settings can be left as default. Your window should look similar to the below if configured correctly for the quotedetail entity:

  1. Click on Register New Step to add the step to the application.
  2. Repeat steps 3-4 for any additional Product line item entities that are using with your custom pricing engine

Now, when you go back into CRM/D36E, the Extended Amount values will start to be populated automatically as soon as you add a new Product onto the Product line item subgrid.

Conclusions or Wot I Think

Whilst the ability to override an important piece of CRM’s/D365E’s functionality is welcome, you do need to bear in mind the additional overhead and responsibility this leaves your organisation in ensuring that your custom pricing engine is correct and that you have adequately tested the solution to properly identify actions which are out of the ordinary, such as the one discussed in this post. What is slightly frustrating about this quirk, in particular, is the lack of clear documentation regarding the CalculatePrice message from Microsoft. Granted, the message is only exposed for minimal interaction from an SDK point of view and is, for all intents and purposes, an internal application message that we shouldn’t really mess with or care about. Having said this, even just a brief summary of when the message is triggered on the platform would have made it instantly more understandable why any custom pricing calculation engine will fail to provide you with an instant amount within your Extended Amount field. In the end, however, I am pleased that there is a straightforward workaround that can be put into place to ensure that things work as expected; hopefully to the extent that it becomes virtually impossible to determine easily whether your organisation is using the default or a custom pricing engine in the first place.

The ability to modify the values within Option Set fields on a Dynamics CRM/Dynamics 365 for Enterprise (D365E) form is, depending on your business scenario, one of the best reasons to learn how to write basic JScript form functions. Microsoft makes available 3 methods via the Xrm.Page.ui control (which can be handily shortcutted to Xrm.Page.getControl):

  • addOption – Lets you add a new Option Set value, with the ability to optionally specify its position within the index.
  • clearOptions – Removes all options from an Option Set field.
  • removeOption – Allows you to remove a specific Option Set field value.

With these options at your disposal, you can begin to leverage functionality to accommodate the following business logic:

  • Only allow an Option Set to be populated with values (either all or just specific ones) once information has been entered into other fields.
  • Dynamically change the options available for selection, based on information entered within the primary entity form, or even a related one.

Now the key takeaway with all of this is that, ultimately, your code cannot do anything that would make the “base” Option Set collection invalid. For example – let’s assume you have a field called Fruit with the Option Set values of Apple, Pear and Apricot. If you then tried to introduce the value Banana on the form level, your code will more than likely error when saving the record.

From a CRM/D365E point of view, there are two additional field types which are also technically classed as Option Sets, thereby allowing you to utilise the above methods with them – the Status and Status Reason fields. If you look closely at the Status Reason field within the Customizations area of the application, you can see why: just like option sets, you specify a label and underlying value for it, that is dictated by your solution publisher:

The only difference worth noting is that, unlike normal Option Set fields, you have no choice when it comes to which underlying value is used. If your organisation is prone to changing Status Reason values often for an entity, you may begin to notice large gaps in Option Set values over time; annoying, but I can understand why it’s in place.

All of the above code examples should be sufficient for common scenarios, such as when you want to remove single option set values or are interacting with the Status Reason field on the main part of the form. What I wanted to do as part of today’s blog post is highlight two non-standard scenarios for the above and illustrate a solution for each – which, when brought together, demonstrate an example I was recently involved in developing a solution for.

The Problem

The business in question was using an entity across multiple parts of their business. On the entity, there was then a flag field to indicate which side of the business the entity record belonged to. This would then assist when running reports and, when coupled with Business Unit segregation for the records, ensured that colleagues in the business only saw records that they needed to as part of their day-to-day usage of the application.

Because the entity was shared, fields within the entities – such as Status Reason – were also shared and, by implication, contained values that were only relevant to a specific part of the business. We were therefore tasked with finding a solution to ensure that the Status Reason value list was modified for each person to reflect the record type they were working with. Colleagues primarily worked within the Web Application and the Status Reason field was on each form, within the Header area.

Solution #1: Efficiently Removing Multiple Option Set Values

When we take a look at the code examples, we are given pretty much everything we need to start implementing a solution. So if we assume that our Status Reason field contains 15 values, 10 of which we want to remove, we may be tricked into writing the following code and doing the good ol’ copy & paste:

Xrm.Page.getControl('statuscode').removeOption(100000000);
Xrm.Page.getControl('statuscode').removeOption(100000001);
Xrm.Page.getControl('statuscode').removeOption(100000002);
Xrm.Page.getControl('statuscode').removeOption(100000003);
Xrm.Page.getControl('statuscode').removeOption(100000004);
Xrm.Page.getControl('statuscode').removeOption(100000005);
Xrm.Page.getControl('statuscode').removeOption(100000006);
Xrm.Page.getControl('statuscode').removeOption(100000007);
Xrm.Page.getControl('statuscode').removeOption(100000008);
Xrm.Page.getControl('statuscode').removeOption(100000009);

Now, this code will work fine and can be pretty clearly deciphered. The problem lies in the number of lines it is expressed in (thereby increasing load times/processing time when CRM is processing your function). We can get round this by thinking back to Programming 101 and making the correct assumption that JScript has the ability to loop through similar commands – in this case, via the for Loop. By introducing an array into the mix as well, we can then express our code much more simply:

var statusCodes = [100000000, 100000001, 100000002, 100000003, 100000004,
                   100000005, 100000006, 100000007, 100000008, 100000009];
                   
    for (index = 0; index < statusCodes.length; ++index) {

        Xrm.Page.getControl('statuscode').removeOption(statusCodes[index]);

    }

Solution #2: Working with Header Controls A.K.A. Why I Hate ‘Object reference not set to an instance of an object’ Errors

I perhaps should have prefaced the above when I said ‘Now this code will work fine’. 🙂 Going back to the task at hand, when I attempted to deploy a version of the above code into the target environment, I immediately got the dreaded error message referenced above. This is generally the hallmark error to steer you towards checking your code for any typos, as the error message is basically telling you it can’t find the thing you are asking it to – in this case, the field statuscode.

After triple-checking the exact spelling of statuscode and making sure the field was on the form, I did some further diving into Developer Tools with the form loaded to figure out just what the hell was going wrong. When performing a search for the statuscode field on the DOM Explorer, I noticed that I got a few hits for statuscode – but that it was prefaced with the value header_. I followed this up with some further research online, which confirmed that, if fields in the Header need to be referenced, then they should be prefaced with header_. So our for Loop example would need to look as follows instead:

    for (index = 0; index < statusCodes.length; ++index) {

        Xrm.Page.getControl('header_statuscode').removeOption(statusCodes[index]);

    }

Bringing it All Together

After encapsulating all of the above into a single function, removeStatusCodes(), we can then call the following snippet as part of an OnLoad function to hide our Status Reason values if the flag field is set to the correct value:

if (Xrm.Page.getAttribute('new_myflagfield').getValue() == "100000000") {
                                                           
        removeStatusCodes();
    }

This will then work as intended and ensure that end-users only see the Status Reason values that directly concern them; no doubt assisting greatly in making the system as streamlined as possible to use and to avoid any potential for data-entry problems further down the line.

For those who are well versed in rolling out solution updates within Dynamics CRM/365 for Enterprise (CRM/D365E), the process will always have a certain familiarity to it, with a few surprises rolled in now and again. Often, the update will proceed as anticipated; sometimes, you may encounter bizarre issues. I can remember a particularly strange incident I had last year where a solution import would get to about 90-95% completion…and then the green progress bar would suddenly start rolling back to nothing. The import progress window would then hang with no further guidance or error message. To try and determine the root cause, we had to interrogate the importjob entity within the system, which ended up showing the import job progress stuck at 0.15846281% : / In the end, we had to escalate the issue to Microsoft for further investigation, but rest assured that if you have not yet witnessed your own curious solution import experience, it’s definitely in the post 🙂

Thankfully, if you are new to the whole “rolling out solution update” thing, you can be assured that the process is relatively straightforward, and mostly without issue. If you have been handed a set of solution import instructions for the first time, though, you may be wondering why something similar to the following step is included:

Go into the Data Management -> Duplicate Detection Rules page and click Publish on all Duplicate Detection Rules that have a Status Reason of Unpublished

Unfortunately, after importing a solution update, CRM/D365E will automatically unpublish all of your Duplicate Detection Rules automatically. You are therefore required to explicitly publish them again, lest you start to encounter a sudden increase in duplicate records and database storage within your system. The reason why this happens is both understandable and frustrating in equal measure. As outlined in the following MSDN article on the subject:

A duplicate rule condition specifies the name of a base attribute and the name of a matching attribute. For example, specify an account as a base entity and a contact as a matching entity to compare last names and addresses

As part of the above, explicit matchcodes are created for every record that the Duplicate Detection Rule is targeting, based on the current metadata of your CRM/D365E entities and attributes. Because your solution update can potentially alter significant aspects of this metadata, the system automatically unpublishes all Duplicate Detection Rules as a precaution.

The above is perhaps trivial in nature, as the actual process of re-publishing all Duplicate Detection Rules is somewhat negligible in effort terms. Where difficulties can arise is if someone innocently overlooks this part of the process or if your system has many different Duplicate Detection Rules, in a mixture of Unpublished/Published state. You would have to specifically make a note of which rules were Published before beginning your solution import so that you can ensure that the correct rules are published after the fact. I would have thought that after so many versions of the product, that something would be added to address this – for example, perhaps a checkbox at the start of the Solution Import Wizard that lets you specify whether all currently published rules should be reactivated after the import completes successfully.

If you find that the above is an annoyance that you can do without no longer, like with many things on the platform, there is a solution that can be deployed in code. The SDK exposes the PublishDuplicateRuleRequest class, which does exactly what it says on the tin – meaning that you can write a plugin that applies this functionality accordingly. The tricky bit comes in determining which Message (i.e. action) on the platform that you wish to run this against. CRM/D365E does not expose a SolutionUpdate or SolutionImport message that we can piggy-back onto, so we have to look at the PublishAll message instead – the action that is triggered when you press Publish All Customizations in the system. This is because this is generally the action you will always need to take when importing an (unmanaged) solution. As a result, we can write a plugin class that is triggered on the Post-Operation event of this entity to automatically publish all Unpublished Duplicate Detection Rules in the system!

The snippet below is adapted from the sample code provided by Microsoft, but has been tweaked as follows:

  • A QueryExpression is used as opposed to QueryByAttribute, since we need to query on two separate attributes and their values – statecode and statuscode. You also cannot return an easily accessible count on all results returned with QueryByAttribute. We will see why is useful in a few moments.
  • The code explicitly checks for if there are any Unpublished rules first before attempting to proceed further – no point in running code unnecessarily!
  • Instead of activating each rule one-by-one using an Execute request, all of the requests are collected together as part of an ExecuteMultipleRequest, given that we now know the performance benefits that this can have.
  • Tracing has been implemented in liberal amounts, to provide remote debugging from within CRM/D365E.

Here’s the code – just copy into an empty class file on your plugin project, modify the namespace to reflect the name of your project and you will be good to go!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Crm.Sdk.Messages;

namespace MyPlugin.Plugins
{
    public class PostPublishAll_PublishDuplicateDetectionRules : IPlugin 
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            //Obtain the execution context from the service provider.

            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            //Get a reference to the Organization service.

            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = factory.CreateOrganizationService(context.UserId);

            //Extract the tracing service for use in debugging sandboxed plug-ins

            ITracingService tracing = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

            tracing.Trace("Tracing implemented successfully!");

            if (context.MessageName == "PublishAll")

            {
                PublishRules(service, tracing);
            }
        }

        private void PublishRules(IOrganizationService service, ITracingService tracing)

        {
            EntityCollection rules = GetDuplicateDetectionRules(service);

            tracing.Trace("Obtained " + rules.TotalRecordCount.ToString() + " duplicate detection rules.");

            if (rules.TotalRecordCount >= 1)

            {
                // Create an ExecuteMultipleRequest object.
                ExecuteMultipleRequest request = new ExecuteMultipleRequest()
                {
                    // Assign settings that define execution behavior: don't continue on error, don't return responses. 
                    Settings = new ExecuteMultipleSettings()
                    {
                        ContinueOnError = false,
                        ReturnResponses = false
                    },
                    // Create an empty organization request collection.
                    Requests = new OrganizationRequestCollection()
                };

                //Create a collection of PublishDuplicateRuleRequests, and execute them in one batch

                foreach(Entity entity in rules.Entities)

                {

                    PublishDuplicateRuleRequest publishReq = new PublishDuplicateRuleRequest { DuplicateRuleId = entity.Id };
                    request.Requests.Add(publishReq);
                    
                }

                service.Execute(request);

            }

            else

            {
                tracing.Trace("Plugin execution cancelled, as there are no duplicate detection rules to publish.");
                return;
            }
        }

        private EntityCollection GetDuplicateDetectionRules(IOrganizationService service)

        {
            QueryExpression qe = new QueryExpression("duplicaterule");

            qe.ColumnSet = new ColumnSet("duplicateruleid");

            ConditionExpression condition1 = new ConditionExpression();
            condition1.AttributeName = "statecode";
            condition1.Operator = ConditionOperator.Equal;
            condition1.Values.Add(0);

            ConditionExpression condition2 = new ConditionExpression();
            condition2.AttributeName = "statuscode";
            condition2.Operator = ConditionOperator.Equal;
            condition2.Values.Add(0);

            FilterExpression filter = new FilterExpression();
            filter.FilterOperator = LogicalOperator.And;
            filter.Conditions.Add(condition1);
            filter.Conditions.Add(condition2);

            qe.Criteria.AddFilter(filter);

            //Have to add this, otherwise the record count won't be returned correctly

            qe.PageInfo.ReturnTotalRecordCount = true;

            return service.RetrieveMultiple(qe);

        } 
    }
}

The only caveat with the above is that it is arguably only useful for if you are regularly importing Unmanaged, as opposed to Managed solutions, as the Publish All Customizations option is not displayed on the import wizard for unmanaged solutions. Nevertheless, by rolling out the above into your environment, you no longer need to scrabble around for the mental note you have to make when performing a solution update 🙂