<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ján</title>
    <description>The latest articles on DEV Community by Ján (@mucha).</description>
    <link>https://dev.to/mucha</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4119392%2F43837995-4a4d-4472-a296-52f522d1fde2.png</url>
      <title>DEV Community: Ján</title>
      <link>https://dev.to/mucha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mucha"/>
    <language>en</language>
    <item>
      <title>Why Dataverse Plug-in Steps Drift — and Why Stable IDs Fix It</title>
      <dc:creator>Ján</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:23:27 +0000</pubDate>
      <link>https://dev.to/mucha/why-dataverse-plug-in-steps-drift-and-why-stable-ids-fix-it-eah</link>
      <guid>https://dev.to/mucha/why-dataverse-plug-in-steps-drift-and-why-stable-ids-fix-it-eah</guid>
      <description>&lt;p&gt;If you have worked on a Dataverse project with more than one developer and more than one environment, you have probably seen this:&lt;/p&gt;

&lt;p&gt;A plug-in fires twice. Or three times.&lt;/p&gt;

&lt;p&gt;An autonumber skips values.&lt;/p&gt;

&lt;p&gt;A validation message appears twice in the same dialog.&lt;/p&gt;

&lt;p&gt;You open the Plugin Registration Tool, expand the assembly, and find two steps registered on the same message, the same table, the same stage — one of them created three weeks ago by someone who no longer remembers doing it.&lt;/p&gt;

&lt;p&gt;The usual reaction is to delete the extra step and move on.&lt;/p&gt;

&lt;p&gt;But the duplicate is not the problem.&lt;/p&gt;

&lt;p&gt;It is a symptom of where the registration lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where registration actually lives
&lt;/h2&gt;

&lt;p&gt;A plug-in step is a row in Dataverse.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SdkMessageProcessingStep&lt;/code&gt; stores information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;message&lt;/li&gt;
&lt;li&gt;table&lt;/li&gt;
&lt;li&gt;execution stage&lt;/li&gt;
&lt;li&gt;execution mode&lt;/li&gt;
&lt;li&gt;filtering attributes&lt;/li&gt;
&lt;li&gt;execution order&lt;/li&gt;
&lt;li&gt;unsecure configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Secure configuration is stored separately in &lt;code&gt;SdkMessageProcessingStepSecureConfig&lt;/code&gt;, and plug-in images are separate records as well.&lt;/p&gt;

&lt;p&gt;None of that is part of the compiled plug-in assembly.&lt;/p&gt;

&lt;p&gt;The assembly contains the code.&lt;/p&gt;

&lt;p&gt;Dataverse contains the metadata that decides &lt;strong&gt;when and how that code executes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This separation is where drift starts.&lt;/p&gt;

&lt;p&gt;Your C# lives in Git, gets code-reviewed, has a history, and is expected to be consistent across environments.&lt;/p&gt;

&lt;p&gt;Registration metadata, on the other hand, can become environment state — modified through tools or transported through solutions — and it can diverge from what developers believe should be registered.&lt;/p&gt;

&lt;p&gt;Without a single source of truth connecting the two, code and registration drift apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the duplicate actually appears
&lt;/h2&gt;

&lt;p&gt;The mechanics are worth being precise about, because they explain why the problem is so persistent.&lt;/p&gt;

&lt;p&gt;When you register a new step through the Plugin Registration Tool, Dataverse creates a new &lt;code&gt;SdkMessageProcessingStep&lt;/code&gt; record with its own ID.&lt;/p&gt;

&lt;p&gt;If another logically equivalent step is registered separately, it is still a different Dataverse record.&lt;/p&gt;

&lt;p&gt;Both are valid.&lt;/p&gt;

&lt;p&gt;Both can fire.&lt;/p&gt;

&lt;p&gt;Now add multiple environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;development&lt;/li&gt;
&lt;li&gt;test&lt;/li&gt;
&lt;li&gt;UAT&lt;/li&gt;
&lt;li&gt;production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And multiple ways of getting plug-ins there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;manual registration&lt;/li&gt;
&lt;li&gt;solution transport&lt;/li&gt;
&lt;li&gt;deployment scripts&lt;/li&gt;
&lt;li&gt;different developers doing different things over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A step may be transported as a solution component while another version of what developers consider the same logical step was created manually.&lt;/p&gt;

&lt;p&gt;If those records have different identities, Dataverse does not consider them duplicates.&lt;/p&gt;

&lt;p&gt;They are simply two different registered steps with similar configuration.&lt;/p&gt;

&lt;p&gt;That distinction is important.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem is identity
&lt;/h2&gt;

&lt;p&gt;The common explanation is that deployment tools should update existing steps instead of creating new ones.&lt;/p&gt;

&lt;p&gt;That is only part of the problem.&lt;/p&gt;

&lt;p&gt;Dataverse can update an existing component when it knows &lt;strong&gt;which component it is updating&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But two registrations with different IDs remain two different registrations, even if they share the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;message&lt;/li&gt;
&lt;li&gt;table&lt;/li&gt;
&lt;li&gt;stage&lt;/li&gt;
&lt;li&gt;plug-in type&lt;/li&gt;
&lt;li&gt;execution mode&lt;/li&gt;
&lt;li&gt;filtering attributes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the platform's perspective, they are still two separate records.&lt;/p&gt;

&lt;p&gt;Without a stable identity for the intended step, deployment tooling has no reliable way to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is the step that should already exist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that is why duplicate registrations keep coming back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that partially help
&lt;/h2&gt;

&lt;p&gt;There are several approaches that reduce the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution-based transport
&lt;/h3&gt;

&lt;p&gt;Add plug-in steps as solution components and move them with the solution.&lt;/p&gt;

&lt;p&gt;This works and is the platform-native approach.&lt;/p&gt;

&lt;p&gt;But it does not prevent duplicates that already exist in development, and the registration itself is still not particularly convenient to review next to the C# implementation.&lt;/p&gt;

&lt;p&gt;The code says what the plug-in does.&lt;/p&gt;

&lt;p&gt;The environment still says when it runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration-driven deployment
&lt;/h3&gt;

&lt;p&gt;You can describe registrations in deployment configuration and synchronize environments from there.&lt;/p&gt;

&lt;p&gt;This improves repeatability and auditability, but the registration still lives somewhere separate from the code it governs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attribute-based or declarative deployment tools
&lt;/h3&gt;

&lt;p&gt;Tools such as &lt;a href="https://github.com/scottdurow/SparkleXrm" rel="noopener noreferrer"&gt;spkl&lt;/a&gt; and &lt;a href="https://github.com/cgoconseils/XrmFramework" rel="noopener noreferrer"&gt;XrmFramework&lt;/a&gt; have used a declarative approach for years.&lt;/p&gt;

&lt;p&gt;The registration is described in source control and deployment tooling synchronizes Dataverse accordingly.&lt;/p&gt;

&lt;p&gt;This is the right shape of solution.&lt;/p&gt;

&lt;p&gt;And the principle behind it matters more than the specific tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is a stable identity declared in code
&lt;/h2&gt;

&lt;p&gt;If a logical plug-in step has a stable identity that lives in source control, deployment can stop being:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Register another step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and become:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make the environment match this declaration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Repeated deployments can then find the registration created previously and update it instead of creating another copy.&lt;/p&gt;

&lt;p&gt;That single change has consequences that go far beyond duplicate plug-in steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registration becomes reviewable
&lt;/h2&gt;

&lt;p&gt;Imagine changing a plug-in from:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PreOperation&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PostOperation&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;or adding another filtering attribute.&lt;/p&gt;

&lt;p&gt;That can completely change the behavior of the solution.&lt;/p&gt;

&lt;p&gt;Yet in many Dataverse projects, that change happens through the Plugin Registration Tool and never appears in a pull request.&lt;/p&gt;

&lt;p&gt;When registration metadata lives with the code, the trigger becomes part of the review.&lt;/p&gt;

&lt;p&gt;The reviewer no longer sees only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// business logic&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They also see &lt;strong&gt;when that code is supposed to execute&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is a much more complete review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environments become comparable
&lt;/h2&gt;

&lt;p&gt;A very common Dataverse question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is production registered the same way as test?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without deterministic deployment, answering that often means opening two environments and comparing registrations manually.&lt;/p&gt;

&lt;p&gt;With declarative registration, the intended state already exists in the repository.&lt;/p&gt;

&lt;p&gt;Deployment tooling can compare the environment against that state and report the difference.&lt;/p&gt;

&lt;p&gt;You are no longer comparing environment A with environment B.&lt;/p&gt;

&lt;p&gt;You are comparing both environments with the same source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollback becomes reproducible
&lt;/h2&gt;

&lt;p&gt;When registration is source-controlled, rollback stops being:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does anyone remember what this step looked like before?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;revert the declaration&lt;/li&gt;
&lt;li&gt;deploy again&lt;/li&gt;
&lt;li&gt;restore the previous registration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Code and execution metadata move together.&lt;/p&gt;

&lt;p&gt;That is what deterministic deployment should mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://github.com/Pillaro-Standard/Dataverse-Plugin-Framework" rel="noopener noreferrer"&gt;Pillaro Dataverse Plugin Framework&lt;/a&gt;, the framework I maintain, plug-in registration metadata is declared directly in the plug-in class.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPluginRegistration&lt;/span&gt; &lt;span class="n"&gt;registration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;registration&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnUpdate&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Contact&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"5056ef4c-0e08-f111-8407-000d3ab261ac"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PreOperation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Synchronous&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The explicit identifier represents the identity of the logical Dataverse step.&lt;/p&gt;

&lt;p&gt;The same approach is used for plug-in images.&lt;/p&gt;

&lt;p&gt;Deployment reads these declarations and builds the desired registration state.&lt;/p&gt;

&lt;p&gt;It can then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;update existing steps&lt;/li&gt;
&lt;li&gt;create missing steps&lt;/li&gt;
&lt;li&gt;synchronize filtering attributes&lt;/li&gt;
&lt;li&gt;synchronize images&lt;/li&gt;
&lt;li&gt;synchronize execution order&lt;/li&gt;
&lt;li&gt;synchronize registration metadata&lt;/li&gt;
&lt;li&gt;remove registrations that are no longer declared&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same source of truth is used for local development and automated deployment.&lt;/p&gt;

&lt;p&gt;A developer can run the deployment locally while implementing the plug-in.&lt;/p&gt;

&lt;p&gt;The same deployment process can run from Azure DevOps.&lt;/p&gt;

&lt;p&gt;There is no need to manually recreate registrations in each environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plugin Registration Tool still has a place
&lt;/h2&gt;

&lt;p&gt;None of this means the Plugin Registration Tool is a bad tool.&lt;/p&gt;

&lt;p&gt;It is extremely useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspecting registrations&lt;/li&gt;
&lt;li&gt;troubleshooting&lt;/li&gt;
&lt;li&gt;understanding an unfamiliar environment&lt;/li&gt;
&lt;li&gt;checking execution configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there is an important distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is a good place to inspect what is registered.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is a poor place to decide what should be registered.&lt;/p&gt;

&lt;p&gt;That decision belongs in source control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part worth taking away
&lt;/h2&gt;

&lt;p&gt;Duplicate plug-in steps are usually treated as an annoying deployment issue.&lt;/p&gt;

&lt;p&gt;But the duplicate itself is only the visible symptom.&lt;/p&gt;

&lt;p&gt;The deeper problem is that the intended identity and configuration of the step are not part of the same source-controlled definition as the code.&lt;/p&gt;

&lt;p&gt;Once registration becomes declarative and each logical step has a stable identity, deployment becomes deterministic.&lt;/p&gt;

&lt;p&gt;And once deployment becomes deterministic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developers stop recreating registrations manually&lt;/li&gt;
&lt;li&gt;environments stop drifting&lt;/li&gt;
&lt;li&gt;registration changes become reviewable&lt;/li&gt;
&lt;li&gt;repeated deployments become predictable&lt;/li&gt;
&lt;li&gt;rollback becomes reproducible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team is deleting duplicate plug-in steps by hand every few weeks, deleting them is not the fix.&lt;/p&gt;

&lt;p&gt;Move the declaration into the repository.&lt;/p&gt;

&lt;p&gt;Which tool you use matters much less than making that move.&lt;/p&gt;




&lt;p&gt;The &lt;a href="https://github.com/Pillaro-Standard/Dataverse-Plugin-Framework" rel="noopener noreferrer"&gt;Pillaro Dataverse Plugin Framework&lt;/a&gt; is open source under Apache-2.0 and free for commercial use, including client projects.&lt;/p&gt;

&lt;p&gt;If you prefer another implementation of the same idea, also take a look at &lt;a href="https://github.com/scottdurow/SparkleXrm" rel="noopener noreferrer"&gt;spkl&lt;/a&gt; and &lt;a href="https://github.com/cgoconseils/XrmFramework" rel="noopener noreferrer"&gt;XrmFramework&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The important part is not the framework.&lt;/p&gt;

&lt;p&gt;The important part is making plug-in registration deterministic.&lt;/p&gt;

</description>
      <category>powerplatform</category>
      <category>dotnet</category>
      <category>devops</category>
      <category>dataverse</category>
    </item>
  </channel>
</rss>
