If you have worked on a Dataverse project with more than one developer and more than one environment, you have probably seen this:
A plug-in fires twice. Or three times.
An autonumber skips values.
A validation message appears twice in the same dialog.
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.
The usual reaction is to delete the extra step and move on.
But the duplicate is not the problem.
It is a symptom of where the registration lives.
Where registration actually lives
A plug-in step is a row in Dataverse.
SdkMessageProcessingStep stores information such as:
- message
- table
- execution stage
- execution mode
- filtering attributes
- execution order
- unsecure configuration
Secure configuration is stored separately in SdkMessageProcessingStepSecureConfig, and plug-in images are separate records as well.
None of that is part of the compiled plug-in assembly.
The assembly contains the code.
Dataverse contains the metadata that decides when and how that code executes.
This separation is where drift starts.
Your C# lives in Git, gets code-reviewed, has a history, and is expected to be consistent across environments.
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.
Without a single source of truth connecting the two, code and registration drift apart.
How the duplicate actually appears
The mechanics are worth being precise about, because they explain why the problem is so persistent.
When you register a new step through the Plugin Registration Tool, Dataverse creates a new SdkMessageProcessingStep record with its own ID.
If another logically equivalent step is registered separately, it is still a different Dataverse record.
Both are valid.
Both can fire.
Now add multiple environments:
- development
- test
- UAT
- production
And multiple ways of getting plug-ins there:
- manual registration
- solution transport
- deployment scripts
- different developers doing different things over time
A step may be transported as a solution component while another version of what developers consider the same logical step was created manually.
If those records have different identities, Dataverse does not consider them duplicates.
They are simply two different registered steps with similar configuration.
That distinction is important.
The real problem is identity
The common explanation is that deployment tools should update existing steps instead of creating new ones.
That is only part of the problem.
Dataverse can update an existing component when it knows which component it is updating.
But two registrations with different IDs remain two different registrations, even if they share the same:
- message
- table
- stage
- plug-in type
- execution mode
- filtering attributes
From the platform's perspective, they are still two separate records.
Without a stable identity for the intended step, deployment tooling has no reliable way to say:
This is the step that should already exist.
And that is why duplicate registrations keep coming back.
Things that partially help
There are several approaches that reduce the problem.
Solution-based transport
Add plug-in steps as solution components and move them with the solution.
This works and is the platform-native approach.
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.
The code says what the plug-in does.
The environment still says when it runs.
Configuration-driven deployment
You can describe registrations in deployment configuration and synchronize environments from there.
This improves repeatability and auditability, but the registration still lives somewhere separate from the code it governs.
Attribute-based or declarative deployment tools
Tools such as spkl and XrmFramework have used a declarative approach for years.
The registration is described in source control and deployment tooling synchronizes Dataverse accordingly.
This is the right shape of solution.
And the principle behind it matters more than the specific tool.
The fix is a stable identity declared in code
If a logical plug-in step has a stable identity that lives in source control, deployment can stop being:
Register another step.
and become:
Make the environment match this declaration.
Repeated deployments can then find the registration created previously and update it instead of creating another copy.
That single change has consequences that go far beyond duplicate plug-in steps.
Registration becomes reviewable
Imagine changing a plug-in from:
PreOperation
to:
PostOperation
or adding another filtering attribute.
That can completely change the behavior of the solution.
Yet in many Dataverse projects, that change happens through the Plugin Registration Tool and never appears in a pull request.
When registration metadata lives with the code, the trigger becomes part of the review.
The reviewer no longer sees only:
public void Execute(...)
{
// business logic
}
They also see when that code is supposed to execute.
That is a much more complete review.
Environments become comparable
A very common Dataverse question is:
Is production registered the same way as test?
Without deterministic deployment, answering that often means opening two environments and comparing registrations manually.
With declarative registration, the intended state already exists in the repository.
Deployment tooling can compare the environment against that state and report the difference.
You are no longer comparing environment A with environment B.
You are comparing both environments with the same source of truth.
Rollback becomes reproducible
When registration is source-controlled, rollback stops being:
Does anyone remember what this step looked like before?
Instead:
- revert the declaration
- deploy again
- restore the previous registration
Code and execution metadata move together.
That is what deterministic deployment should mean.
What this looks like in practice
In the Pillaro Dataverse Plugin Framework, the framework I maintain, plug-in registration metadata is declared directly in the plug-in class.
For example:
public override void Register(IPluginRegistration registration)
{
registration
.OnUpdate<Contact>("5056ef4c-0e08-f111-8407-000d3ab261ac")
.PreOperation()
.Synchronous()
.Rank(1)
.WhenChanged(
c => c.FirstName,
c => c.LastName);
}
The explicit identifier represents the identity of the logical Dataverse step.
The same approach is used for plug-in images.
Deployment reads these declarations and builds the desired registration state.
It can then:
- update existing steps
- create missing steps
- synchronize filtering attributes
- synchronize images
- synchronize execution order
- synchronize registration metadata
- remove registrations that are no longer declared
The same source of truth is used for local development and automated deployment.
A developer can run the deployment locally while implementing the plug-in.
The same deployment process can run from Azure DevOps.
There is no need to manually recreate registrations in each environment.
The Plugin Registration Tool still has a place
None of this means the Plugin Registration Tool is a bad tool.
It is extremely useful for:
- inspecting registrations
- troubleshooting
- understanding an unfamiliar environment
- checking execution configuration
But there is an important distinction:
It is a good place to inspect what is registered.
It is a poor place to decide what should be registered.
That decision belongs in source control.
The part worth taking away
Duplicate plug-in steps are usually treated as an annoying deployment issue.
But the duplicate itself is only the visible symptom.
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.
Once registration becomes declarative and each logical step has a stable identity, deployment becomes deterministic.
And once deployment becomes deterministic:
- developers stop recreating registrations manually
- environments stop drifting
- registration changes become reviewable
- repeated deployments become predictable
- rollback becomes reproducible
If your team is deleting duplicate plug-in steps by hand every few weeks, deleting them is not the fix.
Move the declaration into the repository.
Which tool you use matters much less than making that move.
The Pillaro Dataverse Plugin Framework is open source under Apache-2.0 and free for commercial use, including client projects.
If you prefer another implementation of the same idea, also take a look at spkl and XrmFramework.
The important part is not the framework.
The important part is making plug-in registration deterministic.
Top comments (0)