<?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 (@honza).</description>
    <link>https://dev.to/honza</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/honza</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/honza"/>
    <language>en</language>
    <item>
      <title>Moving from spkl to Pillaro: A Practical Migration Guide for Dataverse Plugins</title>
      <dc:creator>Ján</dc:creator>
      <pubDate>Wed, 23 Sep 2026 17:38:47 +0000</pubDate>
      <link>https://dev.to/honza/moving-from-spkl-to-pillaro-a-practical-migration-guide-for-dataverse-plugins-2aa6</link>
      <guid>https://dev.to/honza/moving-from-spkl-to-pillaro-a-practical-migration-guide-for-dataverse-plugins-2aa6</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 1/4 of the Modern Dataverse Plugin Development series.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A practical look at what Pillaro can replace in an existing spkl-based Dataverse plugin project, what changes during migration, and what Pillaro deliberately does not try to replace.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you have been building Dataverse plugins for a while, there is a good chance you have come across &lt;a href="https://github.com/scottdurow/SparkleXrm/wiki/spkl" rel="noopener noreferrer"&gt;spkl&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For years, spkl solved a very real problem: &lt;strong&gt;stop registering and deploying Dataverse plugins manually&lt;/strong&gt;. Instead of clicking through the Plugin Registration Tool for every environment, developers could describe registration in code and configuration and deploy it repeatedly.&lt;/p&gt;

&lt;p&gt;A lot of existing Dataverse solutions still use that model today.&lt;/p&gt;

&lt;p&gt;The problem is that spkl is no longer actively evolving with the Dataverse development ecosystem. The main SparkleXrm repository has seen no meaningful spkl development for a long time, while unresolved issues around newer Microsoft tooling and authentication remain open. For example, an incompatibility with a newer &lt;code&gt;Microsoft.CrmSdk.CoreTools&lt;/code&gt; version was reported in 2022 and remains open today.&lt;/p&gt;

&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; mean every existing spkl project suddenly needs to be rewritten.&lt;/p&gt;

&lt;p&gt;If your project is stable, its dependencies are pinned, deployment works, and you are not actively changing the solution, migration may give you very little value.&lt;/p&gt;

&lt;p&gt;But if you are maintaining an actively developed Dataverse codebase, upgrading dependencies, introducing automated testing, or trying to make deployment more predictable, you may eventually need a path forward.&lt;/p&gt;

&lt;p&gt;That is the problem Pillaro is trying to solve.&lt;/p&gt;




&lt;p&gt;Pillaro is listed in Microsoft Learn's &lt;strong&gt;Community Tools for Microsoft Dataverse&lt;/strong&gt; catalog and is also published in &lt;strong&gt;Microsoft Marketplace&lt;/strong&gt;. The Marketplace offer installs the Dataverse-side runtime layer, while the C# framework itself is consumed through NuGet.&lt;/p&gt;

&lt;p&gt;These listings provide Microsoft ecosystem discovery and distribution paths, but Pillaro remains a community-maintained framework. Microsoft explicitly states that community tools are supported by their publishers rather than by Microsoft.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/community-tools#pillaro-dataverse-plugin-framework" rel="noopener noreferrer"&gt;Microsoft Learn listing&lt;/a&gt; · &lt;a href="https://marketplace.microsoft.com/en-us/product/pillaro.pillaro-dataverse-plugin-framework" rel="noopener noreferrer"&gt;Microsoft Marketplace&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Pillaro is also listed in Microsoft Learn's &lt;strong&gt;Community Tools for Microsoft Dataverse&lt;/strong&gt; catalog. It remains a community-maintained framework rather than a Microsoft-supported product, but its inclusion gives Dataverse developers another official documentation path to discover the project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/community-tools#pillaro-dataverse-plugin-framework" rel="noopener noreferrer"&gt;Community Tools for Microsoft Dataverse — Microsoft Learn&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  This is not "spkl vs Pillaro"
&lt;/h2&gt;

&lt;p&gt;Pillaro is not a fork of spkl and it is not trying to recreate every spkl command.&lt;/p&gt;

&lt;p&gt;spkl is primarily a &lt;strong&gt;deployment task runner&lt;/strong&gt; around the standard Dataverse development model.&lt;/p&gt;

&lt;p&gt;Pillaro is a &lt;strong&gt;plugin development framework&lt;/strong&gt; with deployment and registration built into that model.&lt;/p&gt;

&lt;p&gt;There is overlap, especially around plugin registration and deployment, but the scope is different.&lt;/p&gt;

&lt;p&gt;The useful question therefore is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which one is better?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If my project uses spkl today, which parts can Pillaro replace, what will change, and what will I need to handle differently?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is what this article covers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What spkl typically does in a Dataverse project
&lt;/h2&gt;

&lt;p&gt;A typical spkl-based plugin project can use spkl for several responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deploying plugin assemblies,&lt;/li&gt;
&lt;li&gt;defining plugin step registration using &lt;code&gt;[CrmPluginRegistration]&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;configuring registrations through &lt;code&gt;spkl.json&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;registering images and filtering attributes,&lt;/li&gt;
&lt;li&gt;reverse-engineering an existing deployment using &lt;code&gt;spkl instrument&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;generating early-bound classes,&lt;/li&gt;
&lt;li&gt;deploying web resources,&lt;/li&gt;
&lt;li&gt;unpacking and packing Dataverse solutions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was one of spkl's strengths: a relatively small tool could cover a surprisingly large part of the Dynamics/Dataverse developer workflow.&lt;/p&gt;

&lt;p&gt;But those responsibilities do not necessarily need to stay in one tool forever.&lt;/p&gt;

&lt;p&gt;Microsoft's own tooling has evolved significantly since spkl was introduced. Today, operations such as solution management and model generation have first-party alternatives in the Power Platform CLI.&lt;/p&gt;

&lt;p&gt;Pillaro therefore focuses on the part we believe belongs closest to the plugin code itself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;plugin structure, registration, deployment and testing.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Pillaro can replace
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Plugin assembly deployment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;spkl:&lt;/strong&gt; yes &lt;br&gt;
&lt;strong&gt;Pillaro:&lt;/strong&gt; yes&lt;/p&gt;

&lt;p&gt;Both approaches can deploy a compiled plugin assembly into Dataverse.&lt;/p&gt;

&lt;p&gt;The important difference is what happens around the assembly.&lt;/p&gt;

&lt;p&gt;With spkl, deployment tooling discovers plugin registration metadata from your assembly and configuration.&lt;/p&gt;

&lt;p&gt;With Pillaro, deployment is part of a framework where the registration declared by the plugin code is treated as the desired state of the environment.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Plugin step registration as code
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;spkl:&lt;/strong&gt; yes &lt;br&gt;
&lt;strong&gt;Pillaro:&lt;/strong&gt; yes&lt;/p&gt;

&lt;p&gt;With spkl you typically define registration using attributes such as:&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CrmPluginRegistration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;MessageNameEnum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;StageEnum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PostOperation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ExecutionModeEnum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Synchronous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"name,address1_city"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"Account updated"&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="n"&gt;IsolationModeEnum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pillaro keeps runtime task registration and deployment metadata separate. Deployment metadata is declared through &lt;code&gt;Register(IPluginRegistration registration)&lt;/code&gt; using the fluent registration API:&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;Account&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"00000000-0000-0000-0000-000000000000"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostOperation&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;WhenChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address1_City&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 step ID is part of the registration contract and is required. Filtering attributes are declared with &lt;code&gt;WithFilteringAttributes(...)&lt;/code&gt;, or its more readable alias &lt;code&gt;WhenChanged(...)&lt;/code&gt;, rather than being hidden in deployment configuration.&lt;/p&gt;

&lt;p&gt;The exact syntax differs from spkl, but the important property is the same: the registration is explicit in source control and can travel through environments with the code.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Filtering attributes, images and execution order
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;spkl:&lt;/strong&gt; yes &lt;br&gt;
&lt;strong&gt;Pillaro:&lt;/strong&gt; yes&lt;/p&gt;

&lt;p&gt;Pillaro registration supports the registration metadata you normally need for Dataverse plugin steps, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;message,&lt;/li&gt;
&lt;li&gt;entity,&lt;/li&gt;
&lt;li&gt;pipeline stage,&lt;/li&gt;
&lt;li&gt;execution mode,&lt;/li&gt;
&lt;li&gt;filtering attributes,&lt;/li&gt;
&lt;li&gt;pre-images,&lt;/li&gt;
&lt;li&gt;post-images,&lt;/li&gt;
&lt;li&gt;execution order / rank.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means the core plugin-registration responsibility that many teams originally adopted spkl for can move into Pillaro.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Removing registrations that no longer exist in code
&lt;/h3&gt;

&lt;p&gt;This is where the approaches start to differ more substantially.&lt;/p&gt;

&lt;p&gt;A common problem in Dataverse projects is &lt;strong&gt;registration drift&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A step existed six months ago. Someone removed or renamed it in code. The assembly gets redeployed — but the old registration remains in one environment.&lt;/p&gt;

&lt;p&gt;Now DEV, TEST and PROD no longer contain the same plugin topology.&lt;/p&gt;

&lt;p&gt;Pillaro deployment treats registration in code as authoritative.&lt;/p&gt;

&lt;p&gt;If a plugin step was previously deployed by Pillaro and is no longer declared, the deployer can detect the difference and remove the obsolete registration.&lt;/p&gt;

&lt;p&gt;The intended model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Code registration
       ↓
Desired state
       ↓
Compare with Dataverse
       ↓
Create / update / remove
       ↓
Environment matches code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For us, this was one of the main reasons to build the deployment model in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Pillaro adds beyond the spkl workflow
&lt;/h2&gt;

&lt;p&gt;Migration does not have to stop at replacing deployment scripts.&lt;/p&gt;

&lt;p&gt;Pillaro also introduces a different way of structuring plugin behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explicit validation and execution
&lt;/h3&gt;

&lt;p&gt;A recurring problem we saw in larger Dataverse implementations was plugin code where everything gradually accumulated inside &lt;code&gt;Execute&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execute
 ├─ get context
 ├─ check stage
 ├─ validate data
 ├─ load records
 ├─ apply business rules
 ├─ execute changes
 └─ handle errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code works.&lt;/p&gt;

&lt;p&gt;Then it grows.&lt;/p&gt;

&lt;p&gt;Pillaro uses an explicit task model where validation and execution are separate phases.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dataverse event
      ↓
Plugin Task
      ↓
Validate
      ↓
Execute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation can fail before the execution phase touches the business operation.&lt;/p&gt;

&lt;p&gt;The goal is not abstraction for abstraction's sake. It is to make the execution flow obvious and make individual pieces easier to test.&lt;/p&gt;




&lt;h3&gt;
  
  
  Integration testing against real Dataverse
&lt;/h3&gt;

&lt;p&gt;Pillaro also includes a separate testing package designed for integration tests against an actual Dataverse environment.&lt;/p&gt;

&lt;p&gt;Tests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;prepare test data,&lt;/li&gt;
&lt;li&gt;create or update records in Dataverse,&lt;/li&gt;
&lt;li&gt;trigger the real deployed plugin,&lt;/li&gt;
&lt;li&gt;verify the resulting data or exception,&lt;/li&gt;
&lt;li&gt;clean up created test data.&lt;/li&gt;
&lt;/ol&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CreateContact_WithoutPostalCode_ShouldFail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;contact&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ContactRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Prague"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;postalCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Throws&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InvalidPluginExecutionException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TestDataService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateTestEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact&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;This is intentionally not a mocked plugin execution.&lt;/p&gt;

&lt;p&gt;Dataverse executes the deployed plugin and the test verifies the real outcome.&lt;/p&gt;

&lt;p&gt;That gives teams another step beyond reproducible deployment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;reproducible verification of the deployed behavior.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Pillaro does NOT replace
&lt;/h2&gt;

&lt;p&gt;This part is important.&lt;/p&gt;

&lt;p&gt;spkl does more than plugin deployment, and Pillaro intentionally does not reproduce every one of those features.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;spkl&lt;/th&gt;
&lt;th&gt;Pillaro&lt;/th&gt;
&lt;th&gt;Recommended direction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plugin assembly deployment&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Pillaro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin step registration as code&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Pillaro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filtering attributes&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Pillaro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre/post images&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Pillaro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution order / rank&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Pillaro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove obsolete plugin steps&lt;/td&gt;
&lt;td&gt;Not clearly documented&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Pillaro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation/execution task model&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Pillaro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration testing against real Dataverse&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Pillaro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modern Custom API metadata registration&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ registration&lt;/td&gt;
&lt;td&gt;Pillaro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early-bound model generation&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ built-in&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pac modelbuilder&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web resource deployment&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Power Platform tooling / CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solution pack/unpack&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;PAC CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse-engineer existing plugin registration&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;instrument&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Manual inventory before migration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on Custom APIs: spkl can register a plugin step against a legacy custom action message, for example through &lt;code&gt;[CrmPluginRegistration("dev1_TestAction", ...)]&lt;/code&gt;. That is not the same capability as managing modern &lt;code&gt;customapi&lt;/code&gt; / &lt;code&gt;customapirequestparameter&lt;/code&gt; metadata. Pillaro's &lt;code&gt;MainOperation()&lt;/code&gt; registration addresses the modern Custom API scenario.&lt;/p&gt;

&lt;p&gt;There is also no Pillaro equivalent of &lt;code&gt;spkl instrument&lt;/code&gt;. If an existing environment contains registrations that are not accurately documented in source control, migration starts with a manual inventory of the deployed state. That limitation is important: Pillaro treats code as the desired state, but it does not automatically reconstruct that code from an unknown Dataverse environment.&lt;/p&gt;

&lt;p&gt;This distinction is intentional.&lt;/p&gt;

&lt;p&gt;We do not think a modern plugin framework needs to own solution unpacking or model generation when Microsoft already provides dedicated tooling for those jobs.&lt;/p&gt;

&lt;p&gt;The migration therefore looks less like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spkl → Pillaro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ┌─ plugin architecture
                 ├─ plugin registration
spkl project ────┼─ plugin deployment     → Pillaro
                 ├─ plugin testing
                 │
                 ├─ model generation      → PAC CLI
                 ├─ solution management   → PAC CLI
                 └─ web resources         → existing CI / Power Platform tooling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What about spkl maintenance and compatibility?
&lt;/h2&gt;

&lt;p&gt;This is one of the practical reasons we started looking for a different long-term foundation.&lt;/p&gt;

&lt;p&gt;As of September 2026, spkl is not under active feature development.&lt;/p&gt;

&lt;p&gt;The SparkleXrm repository still exists and existing projects can continue using it, but the Dataverse platform and Microsoft tooling around it have continued to evolve.&lt;/p&gt;

&lt;p&gt;There are unresolved compatibility issues in the repository. One concrete example is &lt;a href="https://github.com/scottdurow/SparkleXrm/issues/461" rel="noopener noreferrer"&gt;issue #461&lt;/a&gt;, where upgrading &lt;code&gt;Microsoft.CrmSdk.CoreTools&lt;/code&gt; broke spkl early-bound generation. In practice, that means teams cannot assume that newer Microsoft package versions can be adopted independently of spkl; package pinning or workarounds may be required.&lt;/p&gt;

&lt;p&gt;The maintenance gap shows up elsewhere as well. &lt;a href="https://github.com/scottdurow/SparkleXrm/issues/490" rel="noopener noreferrer"&gt;Issue #490&lt;/a&gt;, opened in July 2025, requests Device Code authentication support for spkl. &lt;a href="https://github.com/scottdurow/SparkleXrm/issues/491" rel="noopener noreferrer"&gt;Issue #491&lt;/a&gt;, opened in August 2026, reports &lt;code&gt;get-webresources&lt;/code&gt; silently doing nothing and explicitly asks about maintaining the project.&lt;/p&gt;

&lt;p&gt;These are not arguments that every existing spkl installation is broken. They are evidence that the tool is no longer keeping pace with the current Dataverse tooling stack in the way an actively maintained dependency normally would.&lt;/p&gt;

&lt;p&gt;For a stable legacy project, that may not matter.&lt;/p&gt;

&lt;p&gt;For an actively maintained codebase that needs current SDK/tooling packages, modern authentication, or ongoing fixes, it matters a lot more.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does migration actually look like?
&lt;/h2&gt;

&lt;p&gt;The good news is that moving away from spkl does not necessarily mean rewriting all of your business logic on day one.&lt;/p&gt;

&lt;p&gt;We see migration as two separate steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Replace registration and deployment
&lt;/h3&gt;

&lt;p&gt;Start by moving the operational responsibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spkl attributes / configuration
           ↓
Pillaro registration
           ↓
Pillaro deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first goal is simply to make sure the same plugin assembly and the same logical steps are registered correctly.&lt;/p&gt;

&lt;p&gt;At this stage, avoid unnecessary business-logic changes.&lt;/p&gt;

&lt;p&gt;A migration is much easier to verify when deployment and architecture are not being rewritten at the same time.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2 — Adopt the task model incrementally
&lt;/h3&gt;

&lt;p&gt;Once deployment is stable, plugin implementations can be moved toward Pillaro tasks.&lt;/p&gt;

&lt;p&gt;For example, an existing plugin might contain:&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="n"&gt;IServiceProvider&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Target"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputParameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Target"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidPluginExecutionException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Name is required."&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;The responsibility can gradually become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UpdateAccountTask

Validate
 └─ verify required business conditions

Execute
 └─ perform the business operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do not need to refactor every plugin in the same pull request.&lt;/p&gt;

&lt;p&gt;For a large solution, incremental migration is usually safer.&lt;/p&gt;




&lt;h2&gt;
  
  
  When migration probably makes sense
&lt;/h2&gt;

&lt;p&gt;Consider moving away from spkl when several of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the project is still under active development,&lt;/li&gt;
&lt;li&gt;you need to upgrade Dataverse or Microsoft SDK dependencies,&lt;/li&gt;
&lt;li&gt;existing spkl dependencies are blocking those upgrades,&lt;/li&gt;
&lt;li&gt;plugin registration differs between environments,&lt;/li&gt;
&lt;li&gt;developers still need Plugin Registration Tool to understand deployed state,&lt;/li&gt;
&lt;li&gt;you want automated cleanup of obsolete registrations,&lt;/li&gt;
&lt;li&gt;plugin classes are becoming hard to reason about,&lt;/li&gt;
&lt;li&gt;you want integration tests that exercise deployed plugins in Dataverse,&lt;/li&gt;
&lt;li&gt;you are standardising CI/CD for multiple Dataverse projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In that situation, keeping an unmaintained deployment layer alive can become more expensive than replacing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  When you should probably stay on spkl
&lt;/h2&gt;

&lt;p&gt;There is no value in migrating simply because a newer tool exists.&lt;/p&gt;

&lt;p&gt;If you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a mature solution,&lt;/li&gt;
&lt;li&gt;pinned dependencies,&lt;/li&gt;
&lt;li&gt;reliable deployments,&lt;/li&gt;
&lt;li&gt;very little ongoing plugin development,&lt;/li&gt;
&lt;li&gt;no registration drift,&lt;/li&gt;
&lt;li&gt;and no need for newer tooling,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then leaving the project alone can be the right engineering decision.&lt;/p&gt;

&lt;p&gt;Migration itself has a cost and creates risk.&lt;/p&gt;

&lt;p&gt;The goal should be to remove a real problem, not to modernise code for appearance's sake.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical migration checklist
&lt;/h2&gt;

&lt;p&gt;Before migrating an existing spkl project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Inventory all plugin assemblies.&lt;/li&gt;
&lt;li&gt;[ ] Export or document all current plugin steps.&lt;/li&gt;
&lt;li&gt;[ ] Document filtering attributes.&lt;/li&gt;
&lt;li&gt;[ ] Document pre/post images.&lt;/li&gt;
&lt;li&gt;[ ] Document execution order.&lt;/li&gt;
&lt;li&gt;[ ] Identify configuration values.&lt;/li&gt;
&lt;li&gt;[ ] Identify every spkl command currently used in CI/CD.&lt;/li&gt;
&lt;li&gt;[ ] Separate plugin-related spkl usage from solution/web-resource tooling.&lt;/li&gt;
&lt;li&gt;[ ] Recreate plugin registration in Pillaro.&lt;/li&gt;
&lt;li&gt;[ ] Deploy into a non-production Dataverse environment.&lt;/li&gt;
&lt;li&gt;[ ] Compare deployed registration with the original environment.&lt;/li&gt;
&lt;li&gt;[ ] Add integration tests for critical plugin flows.&lt;/li&gt;
&lt;li&gt;[ ] Remove the old spkl deployment only after the new deployment is verified.&lt;/li&gt;
&lt;li&gt;[ ] Migrate solution/model-generation responsibilities to PAC CLI where appropriate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is that &lt;strong&gt;spkl removal itself should not be the objective&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The objective is a codebase where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plugin behavior is understandable,&lt;/li&gt;
&lt;li&gt;registration is reproducible,&lt;/li&gt;
&lt;li&gt;environments do not silently drift,&lt;/li&gt;
&lt;li&gt;deployment works with the modern toolchain,&lt;/li&gt;
&lt;li&gt;and critical behavior can be verified automatically.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;This article focused specifically on teams already using spkl.&lt;/p&gt;

&lt;p&gt;The next articles in this series will look at different decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain SDK vs a plugin framework&lt;/strong&gt; — when adding a framework actually pays for itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pillaro and XrmFramework&lt;/strong&gt; — two actively maintained but very different approaches to structuring Dataverse development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Dataverse plugin tooling landscape in 2026&lt;/strong&gt; — where the SDK, PAC CLI, deployment tools and frameworks fit together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are currently maintaining a spkl-based project and there is a migration scenario we have not covered here, open an issue or discussion in the &lt;a href="https://github.com/Pillaro-Standard/Dataverse-Plugin-Framework" rel="noopener noreferrer"&gt;Pillaro Dataverse Plugin Framework repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We would rather document the migration gap than pretend it does not exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/scottdurow/SparkleXrm/wiki/spkl" rel="noopener noreferrer"&gt;spkl documentation / SparkleXrm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/scottdurow/SparkleXrm" rel="noopener noreferrer"&gt;SparkleXrm repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/scottdurow/SparkleXrm/blob/master/spkl/spkl/CommandLineArgs.cs" rel="noopener noreferrer"&gt;spkl command-line tasks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/scottdurow/SparkleXrm/issues/461" rel="noopener noreferrer"&gt;Open spkl compatibility issue with Microsoft.CrmSdk.CoreTools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/scottdurow/SparkleXrm/issues/490" rel="noopener noreferrer"&gt;spkl Device Code authentication request&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/scottdurow/SparkleXrm/issues/491" rel="noopener noreferrer"&gt;spkl maintenance / get-webresources issue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Pillaro-Standard/Dataverse-Plugin-Framework" rel="noopener noreferrer"&gt;Pillaro Dataverse Plugin Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Pillaro-Standard/Dataverse-Plugin-Framework/blob/main/docs/tests/testing.md" rel="noopener noreferrer"&gt;Pillaro testing documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I maintain Pillaro. This article is written for teams evaluating a path away from spkl, not to claim that every existing spkl project should migrate.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>powerplatform</category>
      <category>dotnet</category>
      <category>devops</category>
      <category>dataverse</category>
    </item>
    <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/honza/why-dataverse-plug-in-steps-drift-and-why-stable-ids-fix-it-eah</link>
      <guid>https://dev.to/honza/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>
