Skip to content

Compare Plugin Steps and Images Across Environments in Dynamics 365

Recently, while testing in UAT, we ran into a plugin-related issue that wasn’t reproducible in Dev. After investigating, we discovered the root cause: one of the plugin step images was missing an attribute in UAT. This wasn’t immediately obvious and not something we’d catch in a standard deployment check. Manually inspecting each plugin step image across environments would be tedious and error-prone. So, we wrote a quick comparison utility using the Dataverse SDK (C#) to help automate this process. Within the tool, we need to specify the schema name of the table. The tool finds all related the sdkmessageprocessingstepimage records, joins them with associated plugin steps (sdkmessageprocessingstep), and then compares the step, image, and the included attributes.

The result –

We can see it listing a missing plugin registration step in UAT and a mismatch in one of the attributes in a step’s image.

The sample code –

 static void Main(string[] args)
        {
            string devConnectionString = "AuthType=OAuth;Url=https://abcdev.crm6.dynamics.com/;Username=abd@xyz.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;LoginPrompt=Auto;RedirectUri=app://58145b91-0c36-4500-8554-080854f2ac97";
            string uatConnectionString = "AuthType=OAuth;Url=https://abcuat.crm6.dynamics.com/;Username=abd@xyz.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;LoginPrompt=Auto;RedirectUri=app://58145b91-0c36-4500-8554-080854f2ac97";
            var devService = new CrmServiceClient(devConnectionString);
            var uatService = new CrmServiceClient(uatConnectionString);
            string entityLogicalName = "custom_contract";
            var devDetails = GetPluginStepImages(devService, GetEntityTypeCode(devService, entityLogicalName));
            var uatDetails = GetPluginStepImages(uatService, GetEntityTypeCode(uatService, entityLogicalName));
            CompareStepsAndImages(devDetails, uatDetails);
            Console.ReadLine();
        }

        static int GetEntityTypeCode(IOrganizationService service, string entityLogicalName)
        {
            var query = new QueryExpression("entity")
            {
                ColumnSet = new ColumnSet("objecttypecode", "logicalname"),
                Criteria = new FilterExpression(LogicalOperator.And)
            };
            query.Criteria.AddCondition("logicalname", ConditionOperator.Equal, entityLogicalName);
            var response = service.RetrieveMultiple(query);
            var entity = response.Entities.FirstOrDefault();
            return (int)entity.Attributes["objecttypecode"];
        }


        static EntityCollection GetPluginStepImages(IOrganizationService service, int objectTypeCode)
        {
            var query = new QueryExpression("sdkmessageprocessingstepimage")
            {
                ColumnSet = new ColumnSet("name", "imagetype", "messagepropertyname", "entityalias", "attributes", "sdkmessageprocessingstepid")
            };

            // Link to sdkmessageprocessingstep
            var stepLink = query.AddLink("sdkmessageprocessingstep", "sdkmessageprocessingstepid", "sdkmessageprocessingstepid");
            stepLink.Columns = new ColumnSet("name", "sdkmessagefilterid");
            stepLink.EntityAlias = "step";

            // Link to sdkmessagefilter
            var filterLink = stepLink.AddLink("sdkmessagefilter", "sdkmessagefilterid", "sdkmessagefilterid");
            filterLink.LinkCriteria.AddCondition("primaryobjecttypecode", ConditionOperator.Equal, objectTypeCode);

            return service.RetrieveMultiple(query);
        }

        static void CompareStepsAndImages(EntityCollection devStepsWithImages, EntityCollection uatStepsWithImages)
        {
            Console.WriteLine("Comparing Plugin Step / Images between Dev and UAT...");

            // Create dictionaries for faster lookup
            var devDict = devStepsWithImages.Entities.GroupBy(e =>
            {
                var stepId = e.GetAttributeValue<EntityReference>("sdkmessageprocessingstepid")?.Id ?? Guid.Empty;
                return stepId;
            }).ToDictionary(g => g.Key, g => g.ToList());

            var uatDict = uatStepsWithImages.Entities.GroupBy(e =>
            {
                var stepId = e.GetAttributeValue<EntityReference>("sdkmessageprocessingstepid")?.Id ?? Guid.Empty;
                return stepId;
            }).ToDictionary(g => g.Key, g => g.ToList());

            foreach (var devStep in devDict)
            {
                var stepId = devStep.Key;
                var devImages = devStep.Value;
                var devStepName = devImages.FirstOrDefault()?.GetAttributeValue<AliasedValue>("step.name")?.Value?.ToString() ?? "(unknown)";

                if (!uatDict.TryGetValue(stepId, out var uatImages))
                {
                    Console.WriteLine($"[MISSING STEP in UAT] Step: {devStepName}, StepId: {stepId}");
                    continue;
                }

                foreach (var devImage in devImages)
                {
                    var devImageName = devImage.GetAttributeValue<string>("name");
                    var devAttrs = devImage.GetAttributeValue<string>("attributes") ?? "";
                    var devType = devImage.GetAttributeValue<OptionSetValue>("imagetype")?.Value;

                    var match = uatImages.FirstOrDefault(u =>
                        u.GetAttributeValue<string>("name") == devImageName);

                    if (match == null)
                    {
                        Console.WriteLine($"[MISSING IMAGE in UAT] Image: {devImageName}, Step: {devStepName}, StepId: {stepId}");
                        continue;
                    }

                    var uatAttrs = match.GetAttributeValue<string>("attributes") ?? "";
                    var uatType = match.GetAttributeValue<OptionSetValue>("imagetype")?.Value;

                    if (devAttrs != uatAttrs || devType != uatType)
                    {
                        Console.WriteLine($"[MISMATCH] Image: {devImageName}, Step: {devStepName}, StepId: {stepId}");
                        Console.WriteLine($"  Dev Attributes: {devAttrs}");
                        Console.WriteLine($"  UAT Attributes: {uatAttrs}");
                        Console.WriteLine($"  Dev ImageType: {ImageTypeToString(devType)}");
                        Console.WriteLine($"  UAT ImageType: {ImageTypeToString(uatType)}");
                    }
                }
            }

            Console.WriteLine("Comparison complete.");
        }

        static string ImageTypeToString(int? type)
        {
            switch (type)
            {
                case 0:
                    return "PreImage";
                case 1:
                    return "PostImage";
                case 2:
                    return "Both";
                default:
                    return "Unknown";
            }
        }

Hope it helps..

Advertisements


Discover more from Nishant Rana’s Weblog

Subscribe to get the latest posts sent to your email.

Unknown's avatar

Author: Nishant Rana

I love working in and sharing everything about Microsoft.NET technology !

Nishant Rana originally posted this article on 2 June 2025 at 8:30 PM.

Leave a Reply