<?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>Dataverse Plugin Development in 2026: The Tooling Stack Explained</title>
      <dc:creator>Ján</dc:creator>
      <pubDate>Sun, 04 Oct 2026 11:39:40 +0000</pubDate>
      <link>https://dev.to/honza/dataverse-plugin-development-in-2026-the-tooling-stack-explained-2kfn</link>
      <guid>https://dev.to/honza/dataverse-plugin-development-in-2026-the-tooling-stack-explained-2kfn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Dataverse plugin development no longer has one toolchain. The SDK, PAC CLI, Plug-in Registration Tool, Visual Studio tooling, native Git integration and plugin frameworks solve different layers of the problem. The important question is no longer "Which tool should I use?" but "Which tool should own which responsibility?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few years ago, a Dataverse project could easily grow around one utility.&lt;/p&gt;

&lt;p&gt;The same tool might:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generate early-bound classes,&lt;/li&gt;
&lt;li&gt;deploy the assembly,&lt;/li&gt;
&lt;li&gt;register plugin steps,&lt;/li&gt;
&lt;li&gt;deploy web resources,&lt;/li&gt;
&lt;li&gt;export and import solutions,&lt;/li&gt;
&lt;li&gt;pack and unpack solutions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That made sense when first-party tooling covered less of the lifecycle.&lt;/p&gt;

&lt;p&gt;In 2026, the picture is different.&lt;/p&gt;

&lt;p&gt;Microsoft's Power Platform CLI now owns a much larger part of the developer and ALM surface. Power Platform Tools for Visual Studio can create, deploy, register, profile and debug plugins. The Plug-in Registration Tool still has a clear role. Dataverse now has native Git integration and a source-control-oriented YAML representation for solutions. Frameworks sit above that stack and add architecture, conventions and repeatability.&lt;/p&gt;

&lt;p&gt;The result is not one "best tool".&lt;/p&gt;

&lt;p&gt;It is a &lt;strong&gt;stack of responsibilities&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My rule for a modern Dataverse project is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use first-party tooling where the platform already has a good owner. Add a framework where you need architecture, consistency or stronger delivery guarantees.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Start with the platform: the Dataverse plugin SDK
&lt;/h2&gt;

&lt;p&gt;Everything still starts with the Dataverse event framework.&lt;/p&gt;

&lt;p&gt;A C# plugin executes because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a Dataverse operation raises an event,&lt;/li&gt;
&lt;li&gt;a plugin assembly or package is registered,&lt;/li&gt;
&lt;li&gt;a step connects a plugin type to a message, table and pipeline stage,&lt;/li&gt;
&lt;li&gt;Dataverse executes the code inside its plugin runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At code level, the standard primitives are still familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IPlugin
IPluginExecutionContext
IOrganizationService
ITracingService
Entity
EntityReference
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A framework does not replace that contract.&lt;/p&gt;

&lt;p&gt;It sits above it.&lt;/p&gt;

&lt;p&gt;That matters because every abstraction eventually maps back to platform concepts 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;pipeline stage,&lt;/li&gt;
&lt;li&gt;synchronous or asynchronous execution,&lt;/li&gt;
&lt;li&gt;filtering attributes,&lt;/li&gt;
&lt;li&gt;pre/post images,&lt;/li&gt;
&lt;li&gt;execution order,&lt;/li&gt;
&lt;li&gt;configuration,&lt;/li&gt;
&lt;li&gt;user context,&lt;/li&gt;
&lt;li&gt;transaction behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an abstraction makes those concepts impossible to see, it has probably hidden too much.&lt;/p&gt;




&lt;h2&gt;
  
  
  Microsoft now owns much more of the tooling layer
&lt;/h2&gt;

&lt;p&gt;The biggest change compared with older Dynamics development stacks is how much Power Platform CLI now covers.&lt;/p&gt;

&lt;p&gt;It is no longer just an authentication helper.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plugin projects: &lt;code&gt;pac plugin&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A new plugin class library can be initialized with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and an assembly or plugin package can be pushed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a Plain SDK project, that gives you a supported first-party path for project creation and deployment of the plugin binary/package.&lt;/p&gt;

&lt;p&gt;What it does &lt;strong&gt;not&lt;/strong&gt; solve by itself is the architecture inside the plugin or the full desired-state definition of every step.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Registration tooling: PRT still has a job
&lt;/h2&gt;

&lt;p&gt;The Plug-in Registration Tool is still part of the supported Dataverse development workflow and can be launched through PAC CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;prt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;pac tool&lt;/code&gt; commands are available only in the .NET Framework build of PAC CLI, so in practice on Windows.)&lt;/p&gt;

&lt;p&gt;PRT remains useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;registering and inspecting assemblies,&lt;/li&gt;
&lt;li&gt;creating or editing steps,&lt;/li&gt;
&lt;li&gt;inspecting images and filtering attributes,&lt;/li&gt;
&lt;li&gt;troubleshooting existing environments,&lt;/li&gt;
&lt;li&gt;profiling and replaying plugin executions,&lt;/li&gt;
&lt;li&gt;understanding legacy solutions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Power Platform Tools for Visual Studio can also register and manage plugin packages and provides an integrated profiling/debugging workflow.&lt;/p&gt;

&lt;p&gt;So manual tooling is not obsolete.&lt;/p&gt;

&lt;p&gt;The real question is whether manual state is your &lt;strong&gt;source of truth&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There is a big difference between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I use PRT to inspect and diagnose the environment."&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;blockquote&gt;
&lt;p&gt;"Production is correct because somebody remembered which boxes to click."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first is a useful tool.&lt;/p&gt;

&lt;p&gt;The second is operational risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Typed code generation: &lt;code&gt;pac modelbuilder&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Microsoft's supported typed-code generator is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;modelbuilder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Microsoft documents it as the replacement for &lt;code&gt;CrmSvcUtil.exe&lt;/code&gt; distributed through the older &lt;code&gt;Microsoft.CrmSdk.CoreTools&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;This is a good example of a responsibility that many frameworks no longer need to fully reimplement.&lt;/p&gt;

&lt;p&gt;A framework can still add value through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;project conventions,&lt;/li&gt;
&lt;li&gt;configuration,&lt;/li&gt;
&lt;li&gt;wrappers,&lt;/li&gt;
&lt;li&gt;generated helper files,&lt;/li&gt;
&lt;li&gt;filtering rules,&lt;/li&gt;
&lt;li&gt;onboarding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the underlying code-generation engine can remain first-party.&lt;/p&gt;

&lt;p&gt;That is the approach Pillaro takes today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Solution lifecycle: &lt;code&gt;pac solution&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;pac solution&lt;/code&gt; command group covers a large part of the solution lifecycle, including operations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;initialization,&lt;/li&gt;
&lt;li&gt;cloning,&lt;/li&gt;
&lt;li&gt;synchronization,&lt;/li&gt;
&lt;li&gt;export/import,&lt;/li&gt;
&lt;li&gt;pack/unpack,&lt;/li&gt;
&lt;li&gt;checks,&lt;/li&gt;
&lt;li&gt;versioning,&lt;/li&gt;
&lt;li&gt;upgrades.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Microsoft explicitly states that standalone SolutionPackager is no longer the recommended way to pack and unpack solutions because that capability is incorporated into PAC CLI.&lt;/p&gt;

&lt;p&gt;For a plugin framework, that changes the scope discussion.&lt;/p&gt;

&lt;p&gt;A framework should have a strong reason before it starts owning generic solution packaging in 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  Native Git integration changes the ALM picture again
&lt;/h2&gt;

&lt;p&gt;This is one of the biggest changes to the 2026 tooling stack.&lt;/p&gt;

&lt;p&gt;Dataverse now has native Git integration for solution source control.&lt;/p&gt;

&lt;p&gt;That means a development environment or solution can be connected directly to a supported Git provider, and makers can commit and synchronize Dataverse solution changes with source control from the Power Platform experience.&lt;/p&gt;

&lt;p&gt;At the time of writing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the integration is generally available for Azure DevOps (since April 2025) and requires Managed Environments,&lt;/li&gt;
&lt;li&gt;GitHub support is in public preview (since August 2026),&lt;/li&gt;
&lt;li&gt;Microsoft recommends using the integration in development environments and using builds/pipelines to deploy downstream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source-control representation is also changing.&lt;/p&gt;

&lt;p&gt;Dataverse Git integration uses a &lt;strong&gt;YAML-based source format&lt;/strong&gt;, and &lt;code&gt;pac solution clone&lt;/code&gt; can produce the same format for code-first workflows.&lt;/p&gt;

&lt;p&gt;That matters because the ALM stack is moving further toward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Development environment
        ↕
Git source of truth
        ↓
Build
        ↓
Solution artifact
        ↓
Test / Production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Development environment
        ↓
Export ZIP
        ↓
Hope the repository reflects reality
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not remove the need for plugin source code, build pipelines or deployment conventions.&lt;/p&gt;

&lt;p&gt;It does make source control a much more first-class part of the platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  Visual Studio is another valid first-party path
&lt;/h2&gt;

&lt;p&gt;Power Platform Tools for Visual Studio supports a substantial part of the plugin lifecycle.&lt;/p&gt;

&lt;p&gt;It can help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plugin project creation,&lt;/li&gt;
&lt;li&gt;package creation,&lt;/li&gt;
&lt;li&gt;uploading and registering packages,&lt;/li&gt;
&lt;li&gt;registration management,&lt;/li&gt;
&lt;li&gt;profiling,&lt;/li&gt;
&lt;li&gt;replay debugging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because "Plain SDK" no longer means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;write a DLL and manually do everything else.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is now a meaningful first-party developer experience around the SDK.&lt;/p&gt;

&lt;p&gt;A framework therefore has to add value &lt;strong&gt;above that baseline&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack by responsibility
&lt;/h2&gt;

&lt;p&gt;This is how I currently separate the layers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;First-party / platform layer&lt;/th&gt;
&lt;th&gt;Optional higher-level layer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plugin execution contract&lt;/td&gt;
&lt;td&gt;Dataverse SDK&lt;/td&gt;
&lt;td&gt;Framework execution model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create plugin project&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pac plugin init&lt;/code&gt;, Visual Studio tools&lt;/td&gt;
&lt;td&gt;Framework templates/scaffolding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build/package plugin&lt;/td&gt;
&lt;td&gt;.NET + plugin package tooling&lt;/td&gt;
&lt;td&gt;Framework build conventions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push plugin binary/package&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pac plugin push&lt;/code&gt;, Visual Studio tools&lt;/td&gt;
&lt;td&gt;Framework deployer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Register/inspect steps&lt;/td&gt;
&lt;td&gt;PRT, Visual Studio tools&lt;/td&gt;
&lt;td&gt;Framework registration model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Define registration as code&lt;/td&gt;
&lt;td&gt;Team automation&lt;/td&gt;
&lt;td&gt;Plugin framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate typed entities&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pac modelbuilder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Framework wrapper/convention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pack/unpack solutions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pac solution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Usually no framework needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source-control solution metadata&lt;/td&gt;
&lt;td&gt;Dataverse Git integration / &lt;code&gt;pac solution clone&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Team ALM conventions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment authentication&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pac auth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CI/CD secret conventions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime architecture&lt;/td&gt;
&lt;td&gt;Your code&lt;/td&gt;
&lt;td&gt;Plugin framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation structure&lt;/td&gt;
&lt;td&gt;Your code&lt;/td&gt;
&lt;td&gt;Plugin framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagnostics&lt;/td&gt;
&lt;td&gt;Dataverse tracing/profiler&lt;/td&gt;
&lt;td&gt;Framework diagnostics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing conventions&lt;/td&gt;
&lt;td&gt;Your test stack&lt;/td&gt;
&lt;td&gt;Framework test package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registration synchronization&lt;/td&gt;
&lt;td&gt;Your CI/tooling&lt;/td&gt;
&lt;td&gt;Framework deployer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web-resource delivery&lt;/td&gt;
&lt;td&gt;Platform/CI tooling&lt;/td&gt;
&lt;td&gt;Some broader frameworks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live remote debugging&lt;/td&gt;
&lt;td&gt;Profiler/replay or framework-specific tooling&lt;/td&gt;
&lt;td&gt;XrmFramework-style remote debugging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important pattern is that the modern stack is &lt;strong&gt;composable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You do not need one dependency to own every row.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three reasonable project shapes
&lt;/h2&gt;

&lt;p&gt;Most Dataverse plugin projects can be described by one of three approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Minimal first-party stack
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dataverse SDK
    +
pac plugin
    +
PRT / Visual Studio tools
    +
pac modelbuilder
    +
pac solution
    +
Git / Dataverse Git integration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a strong choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the solution has only a few plugins,&lt;/li&gt;
&lt;li&gt;the team understands the Dataverse pipeline well,&lt;/li&gt;
&lt;li&gt;internal conventions are enough,&lt;/li&gt;
&lt;li&gt;third-party dependencies are undesirable,&lt;/li&gt;
&lt;li&gt;registration complexity is limited.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is nothing second-class about this architecture.&lt;/p&gt;

&lt;p&gt;It has the smallest dependency surface.&lt;/p&gt;

&lt;p&gt;The trade-off is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;your team owns the architecture and conventions.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That includes decisions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how plugin classes are structured,&lt;/li&gt;
&lt;li&gt;how validation is separated from execution,&lt;/li&gt;
&lt;li&gt;how steps are represented,&lt;/li&gt;
&lt;li&gt;how environments are kept consistent,&lt;/li&gt;
&lt;li&gt;how testing is done.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2. Structured framework stack
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dataverse SDK
        ↓
Plugin framework
        ↓
Architecture + registration + diagnostics + testing

plus

PAC CLI / Git / platform ALM tooling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes sense when the recurring problem is no longer project creation.&lt;/p&gt;

&lt;p&gt;The problem is &lt;strong&gt;consistency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A framework can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repeatable plugin structure,&lt;/li&gt;
&lt;li&gt;explicit execution units,&lt;/li&gt;
&lt;li&gt;registration in code,&lt;/li&gt;
&lt;li&gt;validation conventions,&lt;/li&gt;
&lt;li&gt;diagnostics,&lt;/li&gt;
&lt;li&gt;integration testing,&lt;/li&gt;
&lt;li&gt;deterministic deployment,&lt;/li&gt;
&lt;li&gt;onboarding standards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The principle I prefer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use the framework where it adds architecture. Keep first-party tooling where the platform already has a strong owner.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, Pillaro provides project-local helpers for early-bound generation, but the actual generator remains &lt;code&gt;pac modelbuilder&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For solution lifecycle, PAC CLI remains the intended layer.&lt;/p&gt;

&lt;p&gt;That keeps the boundaries clearer.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Existing / legacy tooling stack
&lt;/h3&gt;

&lt;p&gt;Many mature solutions still look more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dataverse SDK
    +
spkl
    +
older SDK tooling
    +
custom scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not automatically wrong.&lt;/p&gt;

&lt;p&gt;If the project is stable, the deployment process is understood and the toolchain still works, migration may create more risk than value.&lt;/p&gt;

&lt;p&gt;But for an actively developed solution, I would periodically ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are the Microsoft dependencies current?&lt;/li&gt;
&lt;li&gt;Does authentication still match modern tenant requirements?&lt;/li&gt;
&lt;li&gt;Is the deployment tool still a good long-term dependency?&lt;/li&gt;
&lt;li&gt;Are solution operations now better owned by PAC CLI?&lt;/li&gt;
&lt;li&gt;Is typed-code generation better handled by &lt;code&gt;pac modelbuilder&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Is plugin registration reproducible?&lt;/li&gt;
&lt;li&gt;Is the tool still present because it adds unique value, or because it historically owned five responsibilities at once?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was the reason for the first article in this series: moving from spkl is not mainly about replacing one executable with another.&lt;/p&gt;

&lt;p&gt;It is about deciding &lt;strong&gt;who should own each responsibility now&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frameworks belong above the generic tooling layer
&lt;/h2&gt;

&lt;p&gt;I find it useful to draw the architecture like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│ Business logic / plugin architecture        │
│ Tasks, services, validation, conventions    │
│                  FRAMEWORK                  │
├─────────────────────────────────────────────┤
│ Registration / deployment invariants        │
│ Framework deployer or team automation       │
├─────────────────────────────────────────────┤
│ Source control / ALM                        │
│ Git integration / pipelines / pac solution  │
├─────────────────────────────────────────────┤
│ Microsoft developer tooling                 │
│ pac plugin / modelbuilder / PRT / VS tools  │
├─────────────────────────────────────────────┤
│ Dataverse SDK + event framework             │
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes framework scope easier to reason about.&lt;/p&gt;

&lt;p&gt;A framework is strongest when it owns the layers where &lt;strong&gt;team consistency, architecture and delivery invariants&lt;/strong&gt; matter.&lt;/p&gt;

&lt;p&gt;It becomes harder to justify when it simply recreates generic platform tooling that Microsoft already maintains.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where XrmFramework fits
&lt;/h2&gt;

&lt;p&gt;As discussed in Part 3, XrmFramework deliberately owns a broad part of the upper stack.&lt;/p&gt;

&lt;p&gt;Its current development model includes areas such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;declarative plugin registration,&lt;/li&gt;
&lt;li&gt;typed metadata/model tooling,&lt;/li&gt;
&lt;li&gt;service architecture,&lt;/li&gt;
&lt;li&gt;Custom API tooling,&lt;/li&gt;
&lt;li&gt;deployment utilities,&lt;/li&gt;
&lt;li&gt;Roslyn generation/analyzers,&lt;/li&gt;
&lt;li&gt;scaffolding,&lt;/li&gt;
&lt;li&gt;web-resource tooling,&lt;/li&gt;
&lt;li&gt;remote debugging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a coherent strategy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;make Dataverse development feel like a more integrated .NET development platform.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For teams that want that breadth, the framework can become the center of the developer experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Pillaro fits
&lt;/h2&gt;

&lt;p&gt;Pillaro intentionally owns a narrower set of upper-layer responsibilities.&lt;/p&gt;

&lt;p&gt;Its current center is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;task-based execution,&lt;/li&gt;
&lt;li&gt;explicit validation and execution phases,&lt;/li&gt;
&lt;li&gt;registration metadata in code,&lt;/li&gt;
&lt;li&gt;stable step identity,&lt;/li&gt;
&lt;li&gt;deployment synchronization,&lt;/li&gt;
&lt;li&gt;structured diagnostics,&lt;/li&gt;
&lt;li&gt;runtime configuration,&lt;/li&gt;
&lt;li&gt;integration testing against real Dataverse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For early-bound generation, Pillaro uses project-local helper tooling around Microsoft's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;modelbuilder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the general solution lifecycle, PAC CLI remains the intended owner.&lt;/p&gt;

&lt;p&gt;For repository and environment ALM, standard Git and Power Platform tooling remain outside the framework.&lt;/p&gt;

&lt;p&gt;The strategy is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;own the plugin architecture and delivery invariants; reuse Microsoft tooling for generic platform lifecycle operations.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pillaro is listed in Microsoft Learn's Community Tools for Microsoft Dataverse catalog and is distributed through Microsoft Marketplace for the Dataverse-side runtime, while the C# framework is consumed through NuGet.&lt;/p&gt;

&lt;p&gt;That improves discovery and deployment, but it does not change the support model: Pillaro remains a community-maintained framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where PRT still fits
&lt;/h2&gt;

&lt;p&gt;There is a temptation to call GUI tools obsolete once CI/CD exists.&lt;/p&gt;

&lt;p&gt;I do not think that is accurate.&lt;/p&gt;

&lt;p&gt;PRT is still useful when you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect what is really deployed,&lt;/li&gt;
&lt;li&gt;diagnose a registration,&lt;/li&gt;
&lt;li&gt;profile a plugin,&lt;/li&gt;
&lt;li&gt;test an idea in a development environment,&lt;/li&gt;
&lt;li&gt;understand a legacy solution,&lt;/li&gt;
&lt;li&gt;compare expected and actual state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is not the GUI.&lt;/p&gt;

&lt;p&gt;The problem is &lt;strong&gt;undocumented state&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A healthy setup can use PRT every week and still have fully reproducible deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where AI-assisted development changes the equation
&lt;/h2&gt;

&lt;p&gt;AI makes code generation cheaper.&lt;/p&gt;

&lt;p&gt;It does not make architecture less important.&lt;/p&gt;

&lt;p&gt;In practice, coding agents work better when a repository has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predictable project structure,&lt;/li&gt;
&lt;li&gt;explicit registration,&lt;/li&gt;
&lt;li&gt;small units of behavior,&lt;/li&gt;
&lt;li&gt;clear validation rules,&lt;/li&gt;
&lt;li&gt;reusable tests,&lt;/li&gt;
&lt;li&gt;deterministic deployment,&lt;/li&gt;
&lt;li&gt;useful diagnostics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same is true for human developers.&lt;/p&gt;

&lt;p&gt;As code becomes easier to generate, the value shifts away from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Who can type the C# fastest?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;toward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is the intended behavior explicit enough to implement, verify, review and operate repeatedly?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is an architecture and delivery problem.&lt;/p&gt;

&lt;p&gt;It is one reason I expect frameworks and internal engineering standards to remain relevant.&lt;/p&gt;




&lt;h2&gt;
  
  
  My default approach for a new Dataverse plugin project in 2026
&lt;/h2&gt;

&lt;p&gt;I would make the decisions in this order.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. First ask whether you need a plugin
&lt;/h3&gt;

&lt;p&gt;Microsoft still recommends considering declarative business-logic options first.&lt;/p&gt;

&lt;p&gt;Use custom C# because the requirement needs transactional code, platform event handling, performance, complex integration logic or another capability that declarative options cannot reasonably provide.&lt;/p&gt;

&lt;p&gt;Not because "we always use plugins."&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Start with current Microsoft tooling
&lt;/h3&gt;

&lt;p&gt;For the responsibilities Microsoft already owns well, use the platform tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pac plugin
pac modelbuilder
pac solution
pac tool prt
Power Platform Tools for Visual Studio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For solution source control, evaluate native Dataverse Git integration and the current YAML source format.&lt;/p&gt;

&lt;p&gt;Avoid adding a second owner for the same responsibility without a clear reason.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Decide how plugin architecture will be standardized
&lt;/h3&gt;

&lt;p&gt;Choose explicitly between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plain SDK + team conventions,&lt;/li&gt;
&lt;li&gt;an internal standard/framework,&lt;/li&gt;
&lt;li&gt;a public framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not let the architecture emerge accidentally plugin by plugin.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Put registration state under control
&lt;/h3&gt;

&lt;p&gt;A build artifact is not enough.&lt;/p&gt;

&lt;p&gt;You also need to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which step runs,&lt;/li&gt;
&lt;li&gt;on which message,&lt;/li&gt;
&lt;li&gt;on which table,&lt;/li&gt;
&lt;li&gt;in which stage,&lt;/li&gt;
&lt;li&gt;synchronously or asynchronously,&lt;/li&gt;
&lt;li&gt;which attributes trigger it,&lt;/li&gt;
&lt;li&gt;which images exist,&lt;/li&gt;
&lt;li&gt;under which user context,&lt;/li&gt;
&lt;li&gt;in what order it executes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That state should be reviewable and reproducible.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Put solution state under source control
&lt;/h3&gt;

&lt;p&gt;Plugin source code and Dataverse solution metadata solve different problems.&lt;/p&gt;

&lt;p&gt;Both matter.&lt;/p&gt;

&lt;p&gt;In 2026 there are stronger first-party options than before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dataverse Git integration,&lt;/li&gt;
&lt;li&gt;YAML-based solution source representation,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pac solution clone&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;standard CI/CD pipelines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact workflow can differ between teams.&lt;/p&gt;

&lt;p&gt;The important part is that source control—not someone's development environment—becomes the durable source of truth.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Decide what "tested" means
&lt;/h3&gt;

&lt;p&gt;There is a large difference between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unit-testing a service,&lt;/li&gt;
&lt;li&gt;simulating a plugin context,&lt;/li&gt;
&lt;li&gt;integration-testing against Dataverse,&lt;/li&gt;
&lt;li&gt;replay-debugging a captured execution,&lt;/li&gt;
&lt;li&gt;end-to-end verification after deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the level that matches the risk.&lt;/p&gt;

&lt;p&gt;But decide deliberately.&lt;/p&gt;




&lt;h3&gt;
  
  
  7. Keep the stack replaceable
&lt;/h3&gt;

&lt;p&gt;Every additional tool should have a clear boundary.&lt;/p&gt;

&lt;p&gt;If the framework owns plugin architecture, let it own plugin architecture.&lt;/p&gt;

&lt;p&gt;If PAC CLI owns generic solution packaging, let PAC CLI own it.&lt;/p&gt;

&lt;p&gt;If Git owns source history, do not hide the only source of truth inside an environment.&lt;/p&gt;

&lt;p&gt;Clear boundaries make future migrations much easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  The series in one picture
&lt;/h2&gt;

&lt;p&gt;The four articles in this series map to four different decisions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Part 1
Existing spkl project
        ↓
What should replace the responsibilities it owns?

Part 2
Plain SDK project
        ↓
When does framework abstraction pay off?

Part 3
Framework decision
        ↓
Pillaro or XrmFramework-style approach?

Part 4
Whole developer stack
        ↓
Which tool should own which responsibility?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the main point of the series.&lt;/p&gt;

&lt;p&gt;There is no single correct Dataverse plugin stack.&lt;/p&gt;

&lt;p&gt;There should, however, be a clear reason for every layer you add.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;A mature development stack is not the stack with the most tooling.&lt;/p&gt;

&lt;p&gt;It is the stack where ownership is obvious.&lt;/p&gt;

&lt;p&gt;When something breaks, you should be able to ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this a Dataverse runtime problem?&lt;/li&gt;
&lt;li&gt;An SDK problem?&lt;/li&gt;
&lt;li&gt;A registration problem?&lt;/li&gt;
&lt;li&gt;A PAC CLI / ALM problem?&lt;/li&gt;
&lt;li&gt;A framework problem?&lt;/li&gt;
&lt;li&gt;A business-logic problem?&lt;/li&gt;
&lt;li&gt;A pipeline problem?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And know where to look.&lt;/p&gt;

&lt;p&gt;That clarity is more valuable than any individual feature.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/plug-ins" rel="noopener noreferrer"&gt;Microsoft: Use plug-ins to extend business processes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/apply-business-logic-with-code" rel="noopener noreferrer"&gt;Microsoft: Apply business logic using code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/register-plug-in" rel="noopener noreferrer"&gt;Microsoft: Register a plug-in&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/tools" rel="noopener noreferrer"&gt;Microsoft: Power Platform developer tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/devtools-vs" rel="noopener noreferrer"&gt;Microsoft: Power Platform Tools for Visual Studio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/plugin" rel="noopener noreferrer"&gt;Microsoft: pac plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/tool" rel="noopener noreferrer"&gt;Microsoft: pac tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/modelbuilder" rel="noopener noreferrer"&gt;Microsoft: pac modelbuilder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/solution" rel="noopener noreferrer"&gt;Microsoft: pac solution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/alm/solution-packager-tool" rel="noopener noreferrer"&gt;Microsoft: SolutionPackager guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/tutorial-debug-plug-in" rel="noopener noreferrer"&gt;Microsoft: Debug a plug-in (Plug-in Profiler and replay)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/alm/git-integration/overview" rel="noopener noreferrer"&gt;Microsoft: Dataverse Git integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/alm/git-integration/connecting-to-github" rel="noopener noreferrer"&gt;Microsoft: Connect Dataverse Git integration to GitHub (preview)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/alm/solution-source-control-yaml-format" rel="noopener noreferrer"&gt;Microsoft: Solution YAML source control format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Dimsi-BS/XrmFramework" rel="noopener noreferrer"&gt;XrmFramework&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://learn.microsoft.com/en-us/power-apps/developer/data-platform/community-tools#pillaro-dataverse-plugin-framework" rel="noopener noreferrer"&gt;Microsoft Learn: Community Tools for Microsoft Dataverse — Pillaro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marketplace.microsoft.com/en-us/product/pillaro.pillaro-dataverse-plugin-framework" rel="noopener noreferrer"&gt;Microsoft Marketplace: Pillaro Dataverse Plugin Framework&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I maintain Pillaro. This article is a tooling map, not a recommendation that every Dataverse project should use Pillaro or any other framework. Tooling status reviewed in October 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>powerplatform</category>
      <category>dotnet</category>
      <category>powerapps</category>
      <category>dataverse</category>
    </item>
    <item>
      <title>Pillaro and XrmFramework: Two Different Approaches to Dataverse Plugin Development</title>
      <dc:creator>Ján</dc:creator>
      <pubDate>Sun, 04 Oct 2026 10:27:14 +0000</pubDate>
      <link>https://dev.to/honza/pillaro-and-xrmframework-two-different-approaches-to-dataverse-plugin-development-kj</link>
      <guid>https://dev.to/honza/pillaro-and-xrmframework-two-different-approaches-to-dataverse-plugin-development-kj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;XrmFramework and Pillaro both try to reduce friction in Dataverse plugin development. They do it from different starting points: one optimizes for a broad productivity stack, the other for an explicit task lifecycle, deterministic registration and real-environment testing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the comparison in this series where the tools really do overlap.&lt;/p&gt;

&lt;p&gt;Both &lt;a href="https://github.com/Dimsi-BS/XrmFramework" rel="noopener noreferrer"&gt;XrmFramework&lt;/a&gt; and &lt;a href="https://github.com/Pillaro-Standard/Dataverse-Plugin-Framework" rel="noopener noreferrer"&gt;Pillaro&lt;/a&gt; are actively maintained Dataverse development frameworks.&lt;/p&gt;

&lt;p&gt;Both move plugin registration closer to code.&lt;/p&gt;

&lt;p&gt;Both provide project structure and deployment tooling.&lt;/p&gt;

&lt;p&gt;Both try to make Dataverse development more predictable than repeating raw SDK plumbing in every project.&lt;/p&gt;

&lt;p&gt;But they are not trying to build the same product.&lt;/p&gt;

&lt;p&gt;That makes a simple feature-count comparison less useful than it first appears.&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What development model does each framework want your team to adopt?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Disclosure: I maintain Pillaro. I have tried to describe XrmFramework from its current repository and documentation, including the parts where it is clearly broader or more mature.&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 published in &lt;strong&gt;Microsoft Marketplace&lt;/strong&gt;. The Marketplace offer installs the Dataverse-side managed solution the framework depends on, including the model-driven app for configuration and diagnostic logs; development teams consume the C# framework through NuGet.&lt;/p&gt;

&lt;p&gt;These listings improve discoverability and distribution, but the Microsoft Learn catalog 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;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;If you want the comparison in one paragraph:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XrmFramework&lt;/strong&gt; is the broader productivity platform. It includes typed model tooling, a service-oriented architecture, declarative plugin registration, Custom API deployment, Roslyn analyzers, project templates, web-resource deployment utilities and a live remote debugger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pillaro&lt;/strong&gt; is narrower and more opinionated around the plugin execution lifecycle. It centers development around small tasks with explicit validation and execution phases, stable registration identity, deterministic deployment, structured diagnostics and integration tests that run against a real Dataverse environment.&lt;/p&gt;

&lt;p&gt;Neither design is universally better.&lt;/p&gt;

&lt;p&gt;They optimize different things.&lt;/p&gt;




&lt;h2&gt;
  
  
  XrmFramework's design center: remove Dataverse development friction
&lt;/h2&gt;

&lt;p&gt;The XrmFramework README describes its goal as making Dynamics 365 / Dataverse development feel more like modern .NET.&lt;/p&gt;

&lt;p&gt;That is visible across the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plugin steps are declared in code
&lt;/h3&gt;

&lt;p&gt;A plugin inherits from the framework &lt;code&gt;Plugin&lt;/code&gt; base class and defines steps in &lt;code&gt;AddSteps()&lt;/code&gt;:&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;protected&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;AddSteps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;AddStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Stages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PreValidation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Modes&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="n"&gt;AccountDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Method1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nf"&gt;AddStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Stages&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;Messages&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="n"&gt;Modes&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="n"&gt;AccountDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Method2&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;Additional registration metadata is attached to the target method with attributes:&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;PreImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AccountDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Columns&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;PostImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AccountDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Columns&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FilteringAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;AccountDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Columns&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;AccountDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Columns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccountNumber&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ExecutionOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&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;Method&lt;/span&gt;&lt;span class="p"&gt;(...)&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;Deployment then registers those steps in Dataverse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strongly typed models are a major part of the experience
&lt;/h3&gt;

&lt;p&gt;XrmFramework 3.1+ makes &lt;code&gt;.table&lt;/code&gt; files the source of truth for Dataverse table metadata. Typed table, column and option-set definitions are generated at compile time by a Roslyn source generator.&lt;/p&gt;

&lt;p&gt;Binding models can be described in &lt;code&gt;.model&lt;/code&gt; files and generated the same way, including the mapping code between Dataverse entities and typed models.&lt;/p&gt;

&lt;p&gt;The files can be produced and maintained through XrmFramework tooling, including the Definition Manager and the &lt;code&gt;xrmframework&lt;/code&gt; CLI.&lt;/p&gt;

&lt;p&gt;The design goal is clear:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;reduce magic strings and make Dataverse metadata refactor-friendly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The 3.1 line is a stable release on NuGet, and the &lt;code&gt;xrmframework&lt;/code&gt; CLI includes a one-time migration path for 2.x solutions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Business logic is organized into services
&lt;/h3&gt;

&lt;p&gt;XrmFramework has an &lt;code&gt;IService&lt;/code&gt; architecture for data access and business logic.&lt;/p&gt;

&lt;p&gt;Those typed services can be injected into plugin methods, Custom APIs and external code.&lt;/p&gt;

&lt;p&gt;That gives the framework a larger architectural surface than only the plugin entry point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom APIs are first-class
&lt;/h3&gt;

&lt;p&gt;The framework supports defining modern Dataverse Custom APIs from C# and deploying the related:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;customapi&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;customapirequestparameter&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;customapiresponseproperty&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;metadata.&lt;/p&gt;

&lt;p&gt;For teams using Custom APIs heavily, that is significant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remote debugging is a differentiator
&lt;/h3&gt;

&lt;p&gt;XrmFramework includes a remote debugger that forwards a real Dataverse plugin execution to the developer's machine over Azure Relay.&lt;/p&gt;

&lt;p&gt;You can set a Visual Studio breakpoint and step through the execution locally.&lt;/p&gt;

&lt;p&gt;Pillaro does not have an equivalent feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tooling surface is broad
&lt;/h3&gt;

&lt;p&gt;The current project also includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;xrmframework&lt;/code&gt; CLI scaffolding for solutions and projects,&lt;/li&gt;
&lt;li&gt;deployment utilities,&lt;/li&gt;
&lt;li&gt;Roslyn source generators, analyzers and code fixes,&lt;/li&gt;
&lt;li&gt;TypeScript tooling,&lt;/li&gt;
&lt;li&gt;web-resource deployment,&lt;/li&gt;
&lt;li&gt;unit-test helpers,&lt;/li&gt;
&lt;li&gt;tracing utilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;XrmFramework has also been around for years and its NuGet packages have accumulated hundreds of thousands of downloads.&lt;/p&gt;

&lt;p&gt;This is a mature, broad toolchain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillaro's design center: make the execution model explicit
&lt;/h2&gt;

&lt;p&gt;Pillaro started from a narrower set of problems.&lt;/p&gt;

&lt;p&gt;The recurring issues we wanted to remove were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plugin classes where validation and execution gradually became mixed,&lt;/li&gt;
&lt;li&gt;registration that drifted between environments,&lt;/li&gt;
&lt;li&gt;duplicate or stale plugin steps,&lt;/li&gt;
&lt;li&gt;tests that did not verify the real deployed behavior,&lt;/li&gt;
&lt;li&gt;and solutions where every plugin ended up with a slightly different internal structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The framework therefore makes the execution unit a &lt;strong&gt;Task&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  A task has an explicit lifecycle
&lt;/h3&gt;

&lt;p&gt;The simplified 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;Plugin
  ↓
Task
  ├─ Validation
  └─ Execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A task declares its validations separately from its business logic:&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;class&lt;/span&gt; &lt;span class="nc"&gt;MyFirstTask&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="n"&gt;TaskContext&lt;/span&gt; &lt;span class="n"&gt;taskContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskBase&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Logic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taskContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;ICompleteValidation&lt;/span&gt; &lt;span class="nf"&gt;AddValidations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IBasicModeValidation&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PluginMode&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithStage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PluginStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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;WithMessages&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"Create"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ContextEntity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogicalName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&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;DoExecute&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framework is deliberately opinionated here.&lt;/p&gt;

&lt;p&gt;The split between validation and execution is not a convention that every team member has to remember. It is part of the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime task registration and deployment registration are separate
&lt;/h3&gt;

&lt;p&gt;The plugin decides which tasks participate in runtime execution (in these samples, &lt;code&gt;Task&lt;/code&gt; is the Dataverse activity table, not the framework concept):&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="n"&gt;RegisterTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyFirstTask&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;PluginStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Preoperation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Create"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityLogicalName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PluginMode&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deployment metadata is declared separately:&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;OnCreate&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"8c46d6e6-3c25-4b9d-9264-6c0d02b4d2f1"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The explicit step ID gives registration a stable identity across deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment is treated as synchronization
&lt;/h3&gt;

&lt;p&gt;Pillaro's deployment tooling reads registration metadata from code and synchronizes the assembly, steps, images, filtering attributes, rank, configuration and solution membership.&lt;/p&gt;

&lt;p&gt;The desired direction is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Registration in code
        ↓
Compare with Dataverse
        ↓
Synchronize state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is intentionally different from a deployment model where code only adds what is missing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing targets real Dataverse
&lt;/h3&gt;

&lt;p&gt;Pillaro's test package is built around integration tests against an actual Dataverse environment with deployed plugins.&lt;/p&gt;

&lt;p&gt;A test can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create tracked test data,&lt;/li&gt;
&lt;li&gt;trigger the real operation,&lt;/li&gt;
&lt;li&gt;let Dataverse execute the deployed plugin,&lt;/li&gt;
&lt;li&gt;verify the outcome,&lt;/li&gt;
&lt;li&gt;clean up test data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives a different testing emphasis than a unit-test helper package.&lt;/p&gt;

&lt;h3&gt;
  
  
  Diagnostics are part of the runtime model
&lt;/h3&gt;

&lt;p&gt;The framework includes execution-flow logging, task-level diagnostics, timings and depth information.&lt;/p&gt;

&lt;p&gt;The design preference is to make production behavior inspectable without making live debugging the primary development path.&lt;/p&gt;

&lt;p&gt;Pillaro does &lt;strong&gt;not&lt;/strong&gt; provide XrmFramework-style remote debugging.&lt;/p&gt;




&lt;h2&gt;
  
  
  The comparison
&lt;/h2&gt;

&lt;p&gt;Here is the current high-level picture.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;XrmFramework&lt;/th&gt;
&lt;th&gt;Pillaro&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main design center&lt;/td&gt;
&lt;td&gt;Broad Dataverse developer productivity&lt;/td&gt;
&lt;td&gt;Explicit plugin/task lifecycle and repeatable delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution unit&lt;/td&gt;
&lt;td&gt;Plugin step routed to a method&lt;/td&gt;
&lt;td&gt;Plugin orchestrates one or more Tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation/execution split&lt;/td&gt;
&lt;td&gt;Team convention&lt;/td&gt;
&lt;td&gt;Explicit framework lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step declaration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AddStep(...)&lt;/code&gt; + method attributes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Register(IPluginRegistration)&lt;/code&gt; fluent API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable explicit step ID&lt;/td&gt;
&lt;td&gt;Not the central public API model&lt;/td&gt;
&lt;td&gt;Required by registration API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typed metadata&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.table&lt;/code&gt; / &lt;code&gt;.model&lt;/code&gt; source files + Roslyn generation + XrmFramework tooling&lt;/td&gt;
&lt;td&gt;Early-bound helper tooling around &lt;code&gt;pac modelbuilder&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service architecture&lt;/td&gt;
&lt;td&gt;Typed &lt;code&gt;IService&lt;/code&gt; classes and injection&lt;/td&gt;
&lt;td&gt;Task/context model + framework services/features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom API tooling&lt;/td&gt;
&lt;td&gt;Custom API, parameters and response properties deployed from C#&lt;/td&gt;
&lt;td&gt;Main-operation tasks; the Custom API definition stays in the solution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build-time diagnostics&lt;/td&gt;
&lt;td&gt;Roslyn analyzers and fixes&lt;/td&gt;
&lt;td&gt;No equivalent analyzer suite today&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live remote debugging&lt;/td&gt;
&lt;td&gt;Yes, over Azure Relay&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime diagnostics&lt;/td&gt;
&lt;td&gt;Rich tracing&lt;/td&gt;
&lt;td&gt;Structured plugin/task diagnostics and performance data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit-test helpers&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Framework focus is not mocked unit execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests against real Dataverse&lt;/td&gt;
&lt;td&gt;Build your own approach&lt;/td&gt;
&lt;td&gt;Built-in integration testing stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web-resource tooling&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project scaffolding&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;xrmframework new ...&lt;/code&gt; CLI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dotnet new&lt;/code&gt; + Visual Studio VSIX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin deployment&lt;/td&gt;
&lt;td&gt;Built-in deployment utilities&lt;/td&gt;
&lt;td&gt;Built-in synchronization/deployment tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solution pack/unpack&lt;/td&gt;
&lt;td&gt;Not the core comparison&lt;/td&gt;
&lt;td&gt;Delegated to PAC CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project maturity&lt;/td&gt;
&lt;td&gt;Established, years of usage&lt;/td&gt;
&lt;td&gt;Newer framework, first stable generation in 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There are deliberately no "winner" ticks in this table.&lt;/p&gt;

&lt;p&gt;A feature only matters if your team needs it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The biggest architectural difference
&lt;/h2&gt;

&lt;p&gt;The most important distinction is not remote debugging or model generation.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;where each framework places the center of gravity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;XrmFramework's center is the broader developer experience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Typed models
Services
Plugin methods
Custom APIs
Analyzers
Deployment
Remote debugging
Web resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pillaro's center is the execution and delivery model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plugin
  ↓
Task
  ├─ explicit validation
  └─ explicit execution
  ↓
registration as desired state
  ↓
deployment synchronization
  ↓
real-environment tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That difference affects how teams structure their code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Typed metadata: two valid approaches
&lt;/h2&gt;

&lt;p&gt;Both frameworks try to reduce raw logical-name strings, but the tooling is different.&lt;/p&gt;

&lt;h3&gt;
  
  
  XrmFramework
&lt;/h3&gt;

&lt;p&gt;In XrmFramework 3.1+, &lt;code&gt;.table&lt;/code&gt; files are the source of truth for table metadata. A Roslyn source generator turns them into typed &lt;code&gt;*Definition&lt;/code&gt; classes and option-set enums at compile time.&lt;/p&gt;

&lt;p&gt;The same model extends to binding models: &lt;code&gt;.model&lt;/code&gt; files describe the typed shape and the generator emits the corresponding class and entity-mapping code.&lt;/p&gt;

&lt;p&gt;The files can be created or refreshed from Dataverse through XrmFramework tooling such as the &lt;code&gt;xrmframework&lt;/code&gt; CLI, while the generated C# does not need to be checked into source control.&lt;/p&gt;

&lt;p&gt;This is a framework-owned typed metadata experience with a source-generation model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pillaro
&lt;/h3&gt;

&lt;p&gt;Pillaro currently builds its typed entity workflow around Microsoft's Power Platform CLI.&lt;/p&gt;

&lt;p&gt;The plugin package generates helper files under &lt;code&gt;Tools/EarlyBound/&lt;/code&gt;, but the underlying generator is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;modelbuilder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a deliberate design choice.&lt;/p&gt;

&lt;p&gt;Pillaro provides the project convention and wrapper; Microsoft owns the actual model generator.&lt;/p&gt;

&lt;p&gt;Neither approach is inherently superior.&lt;/p&gt;

&lt;p&gt;One provides a more integrated framework experience.&lt;/p&gt;

&lt;p&gt;The other keeps the metadata generation boundary closer to first-party tooling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debugging vs observability
&lt;/h2&gt;

&lt;p&gt;This is one of the clearest differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  XrmFramework: debug the live execution
&lt;/h3&gt;

&lt;p&gt;Its remote debugger is compelling when the team wants to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reproduce a difficult runtime path,&lt;/li&gt;
&lt;li&gt;attach a breakpoint,&lt;/li&gt;
&lt;li&gt;inspect local state step by step,&lt;/li&gt;
&lt;li&gt;debug against a real environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing in Pillaro currently replaces that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pillaro: make executions inspectable
&lt;/h3&gt;

&lt;p&gt;Pillaro takes a different direction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;structured execution flow,&lt;/li&gt;
&lt;li&gt;task-level logging,&lt;/li&gt;
&lt;li&gt;performance timing,&lt;/li&gt;
&lt;li&gt;depth tracking,&lt;/li&gt;
&lt;li&gt;repeatable real-environment tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The aim is to reduce the number of cases where a developer needs an attached debugger at all.&lt;/p&gt;

&lt;p&gt;Those are different engineering preferences.&lt;/p&gt;

&lt;p&gt;Some teams will want both ideas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing: simulated isolation vs deployed behavior
&lt;/h2&gt;

&lt;p&gt;Testing is another place where the word "testing" can hide very different things.&lt;/p&gt;

&lt;p&gt;XrmFramework publishes &lt;code&gt;XrmFramework.Tests&lt;/code&gt;, described in its package list as unit-test helpers for plugins and services.&lt;/p&gt;

&lt;p&gt;That is useful when you want fast isolated feedback around framework code.&lt;/p&gt;

&lt;p&gt;Pillaro's built-in testing stack intentionally does something else.&lt;/p&gt;

&lt;p&gt;It requires a Dataverse environment with the plugin already deployed.&lt;/p&gt;

&lt;p&gt;The test then creates or modifies actual Dataverse records and verifies the resulting behavior.&lt;/p&gt;

&lt;p&gt;That makes the tests slower and dependent on an environment.&lt;/p&gt;

&lt;p&gt;It also means they verify things a mocked test cannot fully verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;actual registration,&lt;/li&gt;
&lt;li&gt;actual pipeline execution,&lt;/li&gt;
&lt;li&gt;platform behavior,&lt;/li&gt;
&lt;li&gt;interactions between deployed plugins,&lt;/li&gt;
&lt;li&gt;real Dataverse data operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A mature project may want both unit-level and integration-level coverage.&lt;/p&gt;

&lt;p&gt;The frameworks simply emphasize different layers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Breadth has a cost — and narrowness has a cost
&lt;/h2&gt;

&lt;p&gt;XrmFramework's breadth is a benefit when you want one coherent stack.&lt;/p&gt;

&lt;p&gt;It also means there is more framework to adopt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;model definitions,&lt;/li&gt;
&lt;li&gt;service architecture,&lt;/li&gt;
&lt;li&gt;deployment utilities,&lt;/li&gt;
&lt;li&gt;analyzers,&lt;/li&gt;
&lt;li&gt;debugger,&lt;/li&gt;
&lt;li&gt;Custom API conventions,&lt;/li&gt;
&lt;li&gt;potentially web-resource tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team wants those things, the integration is valuable.&lt;/p&gt;

&lt;p&gt;If your problem is only plugin structure and deterministic registration, it may be more surface than you need.&lt;/p&gt;

&lt;p&gt;Pillaro has the opposite trade-off.&lt;/p&gt;

&lt;p&gt;It does not currently provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a remote debugger,&lt;/li&gt;
&lt;li&gt;XrmFramework's analyzer suite,&lt;/li&gt;
&lt;li&gt;TypeScript/web-resource tooling,&lt;/li&gt;
&lt;li&gt;Custom API definitions deployed from code (Pillaro runs the main-operation task, but the &lt;code&gt;customapi&lt;/code&gt; records and their parameters stay in your solution),&lt;/li&gt;
&lt;li&gt;the same years of ecosystem maturity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team needs those features, that matters.&lt;/p&gt;

&lt;p&gt;A smaller surface is only an advantage when the smaller surface contains the things you actually need.&lt;/p&gt;




&lt;h2&gt;
  
  
  When XrmFramework is a natural fit
&lt;/h2&gt;

&lt;p&gt;XrmFramework deserves serious consideration when your team wants a broad Dataverse development platform and values several of these together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;framework-owned typed model tooling,&lt;/li&gt;
&lt;li&gt;a service-oriented architecture across plugins and external code,&lt;/li&gt;
&lt;li&gt;full Custom API tooling,&lt;/li&gt;
&lt;li&gt;Roslyn diagnostics,&lt;/li&gt;
&lt;li&gt;remote debugging,&lt;/li&gt;
&lt;li&gt;web-resource deployment,&lt;/li&gt;
&lt;li&gt;a mature package ecosystem,&lt;/li&gt;
&lt;li&gt;years of production usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is especially interesting when the goal is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;remove as much recurring Dataverse development friction as possible through one integrated framework.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  When Pillaro is a natural fit
&lt;/h2&gt;

&lt;p&gt;Pillaro is aimed more directly at teams that care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit task-sized units of business logic,&lt;/li&gt;
&lt;li&gt;enforced validation → execution structure,&lt;/li&gt;
&lt;li&gt;stable registration identity,&lt;/li&gt;
&lt;li&gt;registration synchronized from code,&lt;/li&gt;
&lt;li&gt;avoiding duplicate/stale plugin steps,&lt;/li&gt;
&lt;li&gt;structured diagnostics,&lt;/li&gt;
&lt;li&gt;integration tests against real Dataverse,&lt;/li&gt;
&lt;li&gt;using Microsoft tooling such as &lt;code&gt;pac modelbuilder&lt;/code&gt; where it already solves the problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design goal is closer to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;keep the framework surface focused, but make the plugin lifecycle and delivery model highly explicit.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Migration cost matters more than feature tables
&lt;/h2&gt;

&lt;p&gt;If you already have a large XrmFramework solution, this article is not an argument to migrate it.&lt;/p&gt;

&lt;p&gt;A framework replacement changes more than NuGet packages.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;architecture,&lt;/li&gt;
&lt;li&gt;project structure,&lt;/li&gt;
&lt;li&gt;registration,&lt;/li&gt;
&lt;li&gt;tests,&lt;/li&gt;
&lt;li&gt;deployment,&lt;/li&gt;
&lt;li&gt;developer habits,&lt;/li&gt;
&lt;li&gt;operational tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A migration only makes sense when the destination solves a real problem that justifies that cost.&lt;/p&gt;

&lt;p&gt;The same is true in the opposite direction.&lt;/p&gt;

&lt;p&gt;If a Pillaro project needs capabilities that are central to XrmFramework, the feature gap should be evaluated honestly instead of forcing a smaller framework to become something it was not designed to be.&lt;/p&gt;




&lt;h2&gt;
  
  
  My conclusion
&lt;/h2&gt;

&lt;p&gt;I do not think the useful conclusion is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;XrmFramework is for large projects and Pillaro is for small projects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is too simplistic.&lt;/p&gt;

&lt;p&gt;A better distinction is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XrmFramework optimizes for breadth of developer productivity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pillaro optimizes for explicitness of plugin architecture, deployment state and deployed-behavior verification.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those priorities can exist in projects of very different sizes.&lt;/p&gt;

&lt;p&gt;The right choice depends on which source of friction your team actually wants the framework to own.&lt;/p&gt;




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

&lt;p&gt;The final article in this series zooms out.&lt;/p&gt;

&lt;p&gt;Instead of comparing frameworks, Part 4 maps the &lt;strong&gt;Dataverse plugin tooling stack in 2026&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what belongs to the SDK,&lt;/li&gt;
&lt;li&gt;what PAC CLI now owns,&lt;/li&gt;
&lt;li&gt;where PRT still fits,&lt;/li&gt;
&lt;li&gt;what frameworks add,&lt;/li&gt;
&lt;li&gt;and where older deployment tools such as spkl fit in a modern codebase.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Dimsi-BS/XrmFramework" rel="noopener noreferrer"&gt;XrmFramework repository and documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nuget.org/packages/XrmFramework.Plugin" rel="noopener noreferrer"&gt;XrmFramework NuGet packages&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://learn.microsoft.com/en-us/power-apps/developer/data-platform/community-tools#pillaro-dataverse-plugin-framework" rel="noopener noreferrer"&gt;Microsoft Learn: Community Tools for Microsoft Dataverse — Pillaro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marketplace.microsoft.com/en-us/product/pillaro.pillaro-dataverse-plugin-framework" rel="noopener noreferrer"&gt;Microsoft Marketplace: 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/plugins/getting-started.md" rel="noopener noreferrer"&gt;Pillaro Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Pillaro-Standard/Dataverse-Plugin-Framework/blob/main/docs/plugins/deployment-plugins.md" rel="noopener noreferrer"&gt;Pillaro Deployment Plugins&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 Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/modelbuilder" rel="noopener noreferrer"&gt;Microsoft: pac modelbuilder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I maintain Pillaro. XrmFramework descriptions in this article are based on its current public repository and documentation as reviewed in October 2026. Corrections are welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>powerplatform</category>
      <category>dataverse</category>
      <category>dotnet</category>
      <category>powerapps</category>
    </item>
    <item>
      <title>Plain SDK vs a Plugin Framework: When Does the Abstraction Pay Off?</title>
      <dc:creator>Ján</dc:creator>
      <pubDate>Mon, 28 Sep 2026 07:09:00 +0000</pubDate>
      <link>https://dev.to/honza/plain-sdk-vs-a-plugin-framework-when-does-the-abstraction-pay-off-jcg</link>
      <guid>https://dev.to/honza/plain-sdk-vs-a-plugin-framework-when-does-the-abstraction-pay-off-jcg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The Dataverse SDK is not something you graduate from. A framework only earns its place when it removes recurring engineering cost without hiding the platform you still need to understand.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A Dataverse plug-in can be very small.&lt;/p&gt;

&lt;p&gt;You implement &lt;code&gt;IPlugin&lt;/code&gt;, read the execution context, do the work, register the step, and move on.&lt;/p&gt;

&lt;p&gt;For a single internal plug-in, that can be exactly the right solution.&lt;/p&gt;

&lt;p&gt;The interesting question starts later.&lt;/p&gt;

&lt;p&gt;What happens when one plug-in becomes twenty? When multiple developers touch the same solution? When DEV, TEST and PROD no longer have exactly the same registrations? When every new plug-in starts by copying the same context, tracing, validation and registration patterns?&lt;/p&gt;

&lt;p&gt;That is where teams usually start asking whether they need a framework.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Should every Dataverse project use a plug-in framework?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;At what point does the cost of repeating the same engineering decisions become higher than the cost of adopting an abstraction?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain SDK is a valid architecture.&lt;/strong&gt; A small, stable plug-in does not need a framework just because one exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frameworks start paying for themselves when repetition becomes delivery risk&lt;/strong&gt; — more developers, more environments, more registrations, longer solution lifetime and a stronger need for repeatable tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real decision is who owns the conventions the SDK leaves open:&lt;/strong&gt; each developer, your internal standards, or a framework.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What the Plain SDK gives you in 2026
&lt;/h2&gt;

&lt;p&gt;The standard Dataverse plug-in model is still the foundation.&lt;/p&gt;

&lt;p&gt;A plug-in is a class that implements &lt;code&gt;IPlugin&lt;/code&gt; and executes because a registered Dataverse step connects that class to an event in the Dataverse execution pipeline.&lt;/p&gt;

&lt;p&gt;Microsoft's supported model still revolves around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IPlugin&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IPluginExecutionContext&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IOrganizationService&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;plug-in assemblies and packages,&lt;/li&gt;
&lt;li&gt;message processing steps,&lt;/li&gt;
&lt;li&gt;images,&lt;/li&gt;
&lt;li&gt;filtering attributes,&lt;/li&gt;
&lt;li&gt;the Plug-in Registration Tool,&lt;/li&gt;
&lt;li&gt;Power Platform CLI,&lt;/li&gt;
&lt;li&gt;and Power Platform Tools for Visual Studio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first-party tooling around the SDK has improved considerably.&lt;/p&gt;

&lt;p&gt;Power Platform CLI can initialize a plug-in project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and can import a plug-in assembly or package into Dataverse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Plug-in Registration Tool is still supported and can be launched through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;prt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Power Platform Tools for Visual Studio can create, register, deploy, debug and profile plug-ins as well.&lt;/p&gt;

&lt;p&gt;So the reason to adopt a framework in 2026 is &lt;strong&gt;not&lt;/strong&gt; that Microsoft gives you no tooling.&lt;/p&gt;

&lt;p&gt;It is also important to separate two concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Getting plug-in code into Dataverse
              ≠
Defining exactly when and how it executes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An assembly can be present while the registration around it is still wrong.&lt;/p&gt;

&lt;p&gt;That distinction becomes increasingly important as a solution grows.&lt;/p&gt;




&lt;h2&gt;
  
  
  A small plug-in may not need a framework
&lt;/h2&gt;

&lt;p&gt;Imagine a plug-in with one responsibility:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On Account update, reject a change if a specific business condition is not satisfied.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A straightforward SDK implementation may be entirely reasonable:&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;class&lt;/span&gt; &lt;span class="nc"&gt;ValidateAccountPlugin&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IPlugin&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;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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPluginExecutionContext&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="nf"&gt;GetService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPluginExecutionContext&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;TryGetValue&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;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;targetValue&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetValue&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;Entity&lt;/span&gt; &lt;span class="n"&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="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="n"&gt;LogicalName&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="k"&gt;return&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;"Account name is required."&lt;/span&gt;&lt;span class="p"&gt;);&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;There is nothing inherently wrong with this.&lt;/p&gt;

&lt;p&gt;For a tiny project, adding another dependency, another execution model and another deployment convention may create more complexity than it removes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero framework is a valid architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A framework should have to justify itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the cost starts to appear
&lt;/h2&gt;

&lt;p&gt;The SDK gives you platform primitives.&lt;/p&gt;

&lt;p&gt;It deliberately does not prescribe the architecture of your whole solution.&lt;/p&gt;

&lt;p&gt;That freedom is useful, but the team has to provide the missing discipline.&lt;/p&gt;

&lt;p&gt;As the solution grows, the same categories of work tend to repeat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Execution boilerplate
&lt;/h3&gt;

&lt;p&gt;Every plug-in needs some combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;execution context,&lt;/li&gt;
&lt;li&gt;target handling,&lt;/li&gt;
&lt;li&gt;pre/post images,&lt;/li&gt;
&lt;li&gt;organization services,&lt;/li&gt;
&lt;li&gt;tracing,&lt;/li&gt;
&lt;li&gt;configuration,&lt;/li&gt;
&lt;li&gt;stage/message checks,&lt;/li&gt;
&lt;li&gt;error handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good team can centralize this themselves.&lt;/p&gt;

&lt;p&gt;A less disciplined codebase gradually gets twenty slightly different versions of the same thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validation mixed with execution
&lt;/h3&gt;

&lt;p&gt;The SDK does not force you to separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can this operation run?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What should this operation do?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation is an architectural decision.&lt;/p&gt;

&lt;p&gt;Without a convention, plug-in classes often accumulate both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registration lives somewhere else
&lt;/h3&gt;

&lt;p&gt;The business code can be perfectly version controlled while the actual plug-in step still exists only as a Dataverse record.&lt;/p&gt;

&lt;p&gt;That means the complete behavior 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 + deployed registration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;not just:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If registration is not reproducible, source control is only telling part of the story.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment drift
&lt;/h3&gt;

&lt;p&gt;A common operational problem is not a broken DLL.&lt;/p&gt;

&lt;p&gt;It is an environment that contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an old step,&lt;/li&gt;
&lt;li&gt;a disabled step,&lt;/li&gt;
&lt;li&gt;different filtering attributes,&lt;/li&gt;
&lt;li&gt;a missing image,&lt;/li&gt;
&lt;li&gt;a different execution order,&lt;/li&gt;
&lt;li&gt;or a duplicate registration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application can compile successfully while DEV and PROD still behave differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing becomes inconsistent
&lt;/h3&gt;

&lt;p&gt;Plain SDK code can absolutely be tested.&lt;/p&gt;

&lt;p&gt;The SDK does not make code untestable.&lt;/p&gt;

&lt;p&gt;But the team has to decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what is isolated from the plug-in entry point,&lt;/li&gt;
&lt;li&gt;what gets mocked,&lt;/li&gt;
&lt;li&gt;whether pipeline behavior is simulated,&lt;/li&gt;
&lt;li&gt;whether anything is tested against real Dataverse,&lt;/li&gt;
&lt;li&gt;how test data is created and removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again: freedom means the team owns the convention.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five signals that an abstraction may start paying for itself
&lt;/h2&gt;

&lt;p&gt;There is no plug-in count at which a framework suddenly becomes mandatory.&lt;/p&gt;

&lt;p&gt;But there are useful signals.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. You are repeating the same structure
&lt;/h3&gt;

&lt;p&gt;If every new plug-in starts by copying another plug-in and deleting half of it, you already have an implicit framework.&lt;/p&gt;

&lt;p&gt;It just lives in copy-paste.&lt;/p&gt;

&lt;p&gt;At that point, formalizing the pattern can reduce variance.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Registration is part of delivery risk
&lt;/h3&gt;

&lt;p&gt;If deployment requires someone to open the Plug-in Registration Tool and verify steps manually, registration has become part of your delivery process.&lt;/p&gt;

&lt;p&gt;For a small solution, that may be acceptable.&lt;/p&gt;

&lt;p&gt;For a team shipping frequently across several environments, it becomes an operational dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The project needs repeatable tests
&lt;/h3&gt;

&lt;p&gt;The value of reusable test infrastructure increases with the lifetime of the solution.&lt;/p&gt;

&lt;p&gt;A plug-in that changes once a year can survive manual verification.&lt;/p&gt;

&lt;p&gt;A business-critical implementation that changes every sprint benefits much more from repeatable regression tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. More developers need to understand the same codebase
&lt;/h3&gt;

&lt;p&gt;Frameworks can reduce local creativity.&lt;/p&gt;

&lt;p&gt;That sounds negative, but in a large codebase it is often useful.&lt;/p&gt;

&lt;p&gt;If every plug-in has the same shape, a developer can move between features without first reverse-engineering the author's preferred pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The solution will live for years
&lt;/h3&gt;

&lt;p&gt;The longer a solution lives, the more expensive inconsistency becomes.&lt;/p&gt;

&lt;p&gt;The first plug-in is cheap.&lt;/p&gt;

&lt;p&gt;The fiftieth deployment, third developer and fourth environment are where architectural decisions start compounding.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a framework should actually buy you
&lt;/h2&gt;

&lt;p&gt;A framework should not be measured by how many classes it adds.&lt;/p&gt;

&lt;p&gt;It should be measured by which recurring problems disappear.&lt;/p&gt;

&lt;p&gt;For a Dataverse plug-in project, useful framework responsibilities can include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Plain SDK&lt;/th&gt;
&lt;th&gt;What a framework can add&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plug-in execution&lt;/td&gt;
&lt;td&gt;Platform primitives&lt;/td&gt;
&lt;td&gt;Consistent execution pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;Team convention&lt;/td&gt;
&lt;td&gt;Explicit validation lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step registration&lt;/td&gt;
&lt;td&gt;PRT / VS tooling / custom automation&lt;/td&gt;
&lt;td&gt;Registration defined in code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment consistency&lt;/td&gt;
&lt;td&gt;Team/CI responsibility&lt;/td&gt;
&lt;td&gt;Deterministic registration synchronization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagnostics&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ITracingService&lt;/code&gt; + custom patterns&lt;/td&gt;
&lt;td&gt;Standard structured logging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Build your own approach&lt;/td&gt;
&lt;td&gt;Reusable unit/integration test foundation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project structure&lt;/td&gt;
&lt;td&gt;Team decision&lt;/td&gt;
&lt;td&gt;Consistent templates and conventions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding&lt;/td&gt;
&lt;td&gt;Learn project-specific patterns&lt;/td&gt;
&lt;td&gt;Predictable project shape&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A framework does not remove the SDK.&lt;/p&gt;

&lt;p&gt;It organizes how the team uses it.&lt;/p&gt;

&lt;p&gt;There is also an emerging AI-assisted development benefit to making those conventions explicit. Small task-sized units, code-defined registration and executable tests give coding agents clearer boundaries and feedback than a large plug-in class whose behavior has to be inferred. Pillaro explicitly treats this as part of its &lt;strong&gt;AI-Ready Standard&lt;/strong&gt; — not as autonomous development magic, but as a way to make the codebase easier for both developers and AI tools to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  A concrete example: the Pillaro approach
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Pillaro-Standard/Dataverse-Plugin-Framework" rel="noopener noreferrer"&gt;Pillaro&lt;/a&gt; is one possible answer to these problems.&lt;/p&gt;

&lt;p&gt;Disclosure: I maintain it.&lt;/p&gt;

&lt;p&gt;It is open source under &lt;strong&gt;Apache-2.0&lt;/strong&gt;, free to use in commercial projects, and production-proven. The framework's first production adopter was &lt;strong&gt;Seyfor&lt;/strong&gt;, where it has been deployed in enterprise Dynamics 365 and Power Platform solutions.&lt;/p&gt;

&lt;p&gt;Starting a new solution does not have to mean wiring the framework, testing package and early-bound tooling by hand. Pillaro ships official starter templates for both &lt;a href="https://www.nuget.org/packages/Pillaro.Dataverse.PluginTemplate.DotNetNew" rel="noopener noreferrer"&gt;&lt;code&gt;dotnet new&lt;/code&gt;&lt;/a&gt; (CLI, VS Code, Visual Studio) and a &lt;a href="https://marketplace.visualstudio.com/items?itemName=Pillaro.PillaroDataversePluginVisualStudioTemplate" rel="noopener noreferrer"&gt;Visual Studio VSIX template&lt;/a&gt;, generated from the same shared source so both produce an identical project structure.&lt;/p&gt;

&lt;p&gt;For the fastest path to a running task-based plug-in project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Pillaro.Dataverse.PluginTemplate.DotNetNew&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pillaro-dataverse-plugin-dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MySolution&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is often the practical answer to "I just need one plug-in fast" — you get the task/validation structure, code-defined registration, testing package and early-bound tooling already wired, instead of assembling them by hand.&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 and is published in &lt;strong&gt;Microsoft Marketplace&lt;/strong&gt;. The Marketplace offer installs the Dataverse-side runtime used by the framework, while the C# framework itself is consumed as a NuGet package.&lt;/p&gt;

&lt;p&gt;Microsoft Learn explicitly notes 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;The framework uses a task-based model where validation and execution are explicit phases:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fta6m4i7h4w3dvoqibfht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fta6m4i7h4w3dvoqibfht.png" alt="Pillaro runtime deployment diagram" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A task follows a consistent lifecycle:&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;class&lt;/span&gt; &lt;span class="nc"&gt;MyFirstTask&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="n"&gt;TaskContext&lt;/span&gt; &lt;span class="n"&gt;taskContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskBase&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Logic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taskContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;ICompleteValidation&lt;/span&gt; &lt;span class="nf"&gt;AddValidations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IBasicModeValidation&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PluginMode&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithStage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PluginStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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;WithMessages&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"Create"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ContextEntity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogicalName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&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;DoExecute&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plug-in then registers that task for runtime execution:&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="n"&gt;RegisterTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyFirstTask&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;PluginStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Preoperation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Create"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityLogicalName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PluginMode&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deployment metadata is declared separately:&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;OnCreate&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"8c46d6e6-3c25-4b9d-9264-6c0d02b4d2f1"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation is intentional.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RegisterTask(...)&lt;/code&gt; tells the runtime which task to execute.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Register(IPluginRegistration registration)&lt;/code&gt; tells deployment tooling which Dataverse registration should exist.&lt;/p&gt;

&lt;p&gt;The stable step ID gives the deployment model a persistent identity for the step, while the deployment tooling synchronizes the declared registration with Dataverse.&lt;/p&gt;

&lt;p&gt;Pillaro also provides functional testing infrastructure that runs against a real Dataverse environment with deployed plug-ins and cleans up tracked test data afterwards.&lt;/p&gt;

&lt;p&gt;That is one design choice.&lt;/p&gt;

&lt;p&gt;Other frameworks make different choices.&lt;/p&gt;

&lt;p&gt;The point is not that this is the only valid architecture. It is an example of what it means for a framework to &lt;strong&gt;own repeated conventions on purpose&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a framework should not do
&lt;/h2&gt;

&lt;p&gt;A framework can solve real problems and still be the wrong choice.&lt;/p&gt;

&lt;p&gt;There are several failure modes worth watching for.&lt;/p&gt;

&lt;h3&gt;
  
  
  It should not hide Dataverse from you
&lt;/h3&gt;

&lt;p&gt;A developer using a framework still needs to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pipeline stages,&lt;/li&gt;
&lt;li&gt;execution depth,&lt;/li&gt;
&lt;li&gt;transactions,&lt;/li&gt;
&lt;li&gt;images,&lt;/li&gt;
&lt;li&gt;filtering attributes,&lt;/li&gt;
&lt;li&gt;synchronous vs asynchronous execution,&lt;/li&gt;
&lt;li&gt;recursion,&lt;/li&gt;
&lt;li&gt;security context,&lt;/li&gt;
&lt;li&gt;service calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the abstraction makes those concepts invisible, debugging becomes harder rather than easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  It should not own everything just because it can
&lt;/h3&gt;

&lt;p&gt;In 2026, Microsoft already provides first-party tooling for large parts of the development lifecycle.&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 plaintext"&gt;&lt;code&gt;Plug-in project      → pac plugin
Typed model code     → pac modelbuilder
Solution lifecycle   → pac solution
PRT                  → pac tool prt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A framework does not need to reimplement every Power Platform CLI feature.&lt;/p&gt;

&lt;p&gt;Sometimes the better design is to integrate with the Microsoft tool instead of competing with it.&lt;/p&gt;

&lt;p&gt;Pillaro follows that approach for early-bound generation: its project tooling prepares a local &lt;code&gt;Tools/EarlyBound/&lt;/code&gt; workflow, but the actual generator underneath is Microsoft's &lt;code&gt;pac modelbuilder&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  It should not make leaving impossible
&lt;/h3&gt;

&lt;p&gt;A healthy abstraction sits on top of understandable platform concepts.&lt;/p&gt;

&lt;p&gt;You should be able to explain what the framework is doing in Dataverse without treating it as magic.&lt;/p&gt;

&lt;p&gt;That keeps migration possible and reduces long-term dependency risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Plain SDK is probably the better choice
&lt;/h2&gt;

&lt;p&gt;There are situations where I would deliberately stay with the SDK.&lt;/p&gt;

&lt;h3&gt;
  
  
  One or two small plug-ins
&lt;/h3&gt;

&lt;p&gt;If the entire solution contains a few simple steps, there may be no recurring cost worth abstracting.&lt;/p&gt;

&lt;h3&gt;
  
  
  You are learning Dataverse plug-in execution
&lt;/h3&gt;

&lt;p&gt;Learning the raw SDK first is useful.&lt;/p&gt;

&lt;p&gt;Understanding &lt;code&gt;IPluginExecutionContext&lt;/code&gt; before wrapping it gives you a much better mental model when something eventually goes wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Third-party dependencies are restricted
&lt;/h3&gt;

&lt;p&gt;Some customers or regulated environments do not allow additional runtime libraries without a review process.&lt;/p&gt;

&lt;p&gt;The plain SDK may be the simplest acceptable option.&lt;/p&gt;

&lt;h3&gt;
  
  
  The existing codebase is stable
&lt;/h3&gt;

&lt;p&gt;If a mature project has working deployment, documented conventions and little ongoing development, introducing a framework can create migration cost without enough return.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your architecture is already standardized internally
&lt;/h3&gt;

&lt;p&gt;A strong engineering team may already have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a plug-in base,&lt;/li&gt;
&lt;li&gt;registration automation,&lt;/li&gt;
&lt;li&gt;test infrastructure,&lt;/li&gt;
&lt;li&gt;logging,&lt;/li&gt;
&lt;li&gt;templates,&lt;/li&gt;
&lt;li&gt;CI/CD.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point you may already have the useful parts of a framework — just internally.&lt;/p&gt;

&lt;p&gt;Replacing them with a public framework is not automatically an improvement.&lt;/p&gt;




&lt;h2&gt;
  
  
  When a framework becomes easier to justify
&lt;/h2&gt;

&lt;p&gt;The case gets stronger when several of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;many plug-in steps are under active development,&lt;/li&gt;
&lt;li&gt;several developers work in the same solution,&lt;/li&gt;
&lt;li&gt;deployment spans multiple environments,&lt;/li&gt;
&lt;li&gt;registration drift has already caused incidents,&lt;/li&gt;
&lt;li&gt;business logic is expected to evolve for years,&lt;/li&gt;
&lt;li&gt;automated regression testing matters,&lt;/li&gt;
&lt;li&gt;onboarding speed matters,&lt;/li&gt;
&lt;li&gt;the same architecture is reused across multiple customer projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice what is missing from that list:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The plug-in has a lot of lines of code."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Line count is not the main issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repeated engineering decisions are.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision I would make
&lt;/h2&gt;

&lt;p&gt;For a small, isolated plug-in, I would start with the SDK.&lt;/p&gt;

&lt;p&gt;For a long-lived solution, I would make one explicit decision early:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Who owns the conventions that the SDK intentionally leaves open?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are three reasonable answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. The individual developer
2. The team / internal standards
3. A framework
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dangerous answer is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nobody — we'll decide separately every time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is where accidental architecture starts.&lt;/p&gt;




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

&lt;p&gt;Before adding a framework, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What recurring problem are we solving?&lt;/li&gt;
&lt;li&gt;Can Microsoft tooling already solve it?&lt;/li&gt;
&lt;li&gt;Can a small internal convention solve it?&lt;/li&gt;
&lt;li&gt;Does the framework make deployment more reproducible?&lt;/li&gt;
&lt;li&gt;Does it make plug-in behavior easier to understand?&lt;/li&gt;
&lt;li&gt;Does it improve testability in the way we actually test?&lt;/li&gt;
&lt;li&gt;Will a new developer understand the architecture faster?&lt;/li&gt;
&lt;li&gt;Can we still reason about the underlying Dataverse execution model?&lt;/li&gt;
&lt;li&gt;What happens if the framework is no longer maintained?&lt;/li&gt;
&lt;li&gt;Is the adoption cost lower than the repetition cost over the expected lifetime of the solution?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those questions have good answers, the abstraction may pay for itself.&lt;/p&gt;

&lt;p&gt;If they do not, the SDK is still there — and it is still a perfectly valid choice.&lt;/p&gt;




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

&lt;p&gt;In Part 3, I will compare two actively maintained frameworks with very different design centers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pillaro and XrmFramework.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not to pick a winner, but to look at what each one chooses to own: execution structure, typed models, deployment, debugging, testing and developer tooling.&lt;/p&gt;

&lt;p&gt;If you want to try the approach on a real project, &lt;strong&gt;Pillaro is Apache-2.0 licensed and free for commercial use&lt;/strong&gt;. The &lt;a href="https://github.com/Pillaro-Standard/Dataverse-Plugin-Framework" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; contains the getting-started guide, NuGet packages, templates, deployment documentation and testing stack. It is already used in production, with Seyfor documented as the first production adopter.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/plug-ins" rel="noopener noreferrer"&gt;Microsoft: Use plug-ins to extend business processes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-apps/developer/data-platform/register-plug-in" rel="noopener noreferrer"&gt;Microsoft: Register a plug-in&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/plugin" rel="noopener noreferrer"&gt;Microsoft: Power Platform CLI — pac plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/cli/reference/tool" rel="noopener noreferrer"&gt;Microsoft: Power Platform CLI — pac tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/tools" rel="noopener noreferrer"&gt;Microsoft: Power Platform developer tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/power-platform/developer/devtools-vs" rel="noopener noreferrer"&gt;Microsoft: Power Platform Tools for Visual Studio&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#readme" rel="noopener noreferrer"&gt;Pillaro README — production adoption, Apache-2.0 license and AI-Ready Standard&lt;/a&gt;&lt;/li&gt;
&lt;li&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: Community Tools for Microsoft Dataverse — Pillaro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marketplace.microsoft.com/en-us/product/pillaro.pillaro-dataverse-plugin-framework" rel="noopener noreferrer"&gt;Microsoft Marketplace: 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/plugins/getting-started.md" rel="noopener noreferrer"&gt;Pillaro Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Pillaro-Standard/Dataverse-Plugin-Framework/blob/main/docs/plugins/plugin-registration-api.md" rel="noopener noreferrer"&gt;Pillaro Plugin Registration API&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 Overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I maintain Pillaro. The goal of this article is not to argue that every Dataverse project needs a framework. It is to make the trade-off explicit.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>powerplatform</category>
      <category>dotnet</category>
      <category>devops</category>
      <category>dataverse</category>
    </item>
    <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;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>
