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..
Discover more from Nishant Rana's Weblog
Subscribe to get the latest posts sent to your email.

One thought on “Compare Plugin Steps and Images Across Environments in Dynamics 365”