Skip to content

Writing

Plug-in Package Ignores New Assemblies

I had a Dataverse plug-in package with one plugin project in it. I added a second plugin project to the same package, built it and deployed the update to my development environment.

The deployment succeeded. The new plugins never showed up. There was nothing to register a step on.

I deployed again. Same result.

It took a while to accept what was going on: Dataverse registers the plugin assemblies and plugin types inside a plug-in package when you create the package. When you add an assembly to a package that already exists, it never registers it or its plugin types, however long you wait and however often you retry.

If you have ever split a plugin project in two and watched the new half disappear, this is why, and this is how to fix it.

First, what a plug-in package is

For years a plugin was one DLL. You built it, signed it, and uploaded it with the Plugin Registration Tool.

You never register your plugin classes by hand. On upload, Dataverse does it for you. It creates a plugin assembly record for the DLL, then looks inside and creates a plugin type record for every class that implements IPlugin.

Those plugin types are what you work with next. A step, “run this class when an account is updated”, is always registered on a plugin type. No plugin type, no step.

The catch was dependencies. Need a NuGet library in your plugin? It had to be merged into that one DLL, and ILMerge was never supported.

A plug-in package fixes that. It is a NuGet package (.nupkg) holding your plugin assembly plus every assembly it depends on. You create the project with pac plugin init, build it, and the build produces the package. You upload it with the Plugin Registration Tool or pac plugin push.

The automatic registration still happens, one level up. Dataverse opens the package, finds every assembly with IPlugin classes in it, and creates a plugin assembly record and plugin types for each one. Microsoft explains the details in Build and package plug-in code.

Most packages hold one plugin assembly and a few helper libraries. That shape works fine and is not affected by anything below.

This post is about a package holding two assemblies that both contain plugins.

How you end up with two plugin assemblies

Microsoft’s own advice is to keep your plugins in one assembly. Real projects drift from that.

Say you start with Contoso.Sales.Plugins. It grows. Service plugins move into their own project, Contoso.Service.Plugins, and the Sales project references it. Build, and the package now contains both DLLs.

Or a plugin library shared between projects gets folded into a package that is already deployed. Same result: an existing package gains a second assembly with IPlugin classes in it.

What goes wrong

Take the Contoso example.

You created the package when it only held Contoso.Sales.Plugins. Dataverse registered that assembly and its plugin types automatically. You registered your steps on them. All good.

Now you add Contoso.Service.Plugins, build, and update the package. The new .nupkg uploads. The Service DLL is now inside the package in Dataverse.

But the automatic registration never happens for it. No plugin assembly record, so no plugin types. Your Service plugin classes are in Dataverse, and there is nothing to register a step on.

An update handles every other change correctly:

Change in the packageWhat Dataverse does on update
New plugin class in an existing assemblyRegisters the new plugin type
Changed code in an existing assemblyUpdates it
Assembly removed from the packageRemoves it, once its plugin types have no steps
Assembly added to the packageNothing. No record is created, ever.

Three out of four work. The fourth is the one you hit when a project grows.

What Microsoft says will happen

From the same Microsoft page:

“When you upload your NuGet package, any assemblies that contain classes that implement the IPlugin interface are registered in the PluginAssembly table and associated with the plug-in package.”

The very next sentence says that as you keep updating the package, changes to the related plugin assemblies are managed on the server.

No exception for updates. The documentation describes the opposite of what happens.

How I know it is Dataverse and not my setup

My first suspect was my own setup. So I tried to prove that.

  • It is not slow. 18 minutes later there was still no plugin assembly record, and so no plugin types.
  • It is not my tooling. Microsoft’s own pac plugin push gives the same result, and reports Plug-in package was updated successfully while doing it.
  • Dataverse can register it. Delete the package and upload the exact same .nupkg as a new package, and both assemblies get their record and plugin types.
  • The version is not the trigger. Raising the assembly version and the package version together changes nothing. Microsoft’s PAC CLI package docs say the same: version “is not a factor in any upgrade behaviors.”
  • It is not going away on its own. Six weeks later, on a newer build, it reproduced on the first try.

Same file, same environment. Create registers both assemblies. Update registers only the one Dataverse already knew.

How to fix it

Create the plugin assembly record yourself, then update the package again. In that order.

Step 1. Create the plugin assembly record. With the Dataverse SDK for .NET:

var assembly = new Entity("pluginassembly")
{
    ["name"]           = "Contoso.Service.Plugins",
    ["packageid"]      = new EntityReference("pluginpackage", packageId),
    ["isolationmode"]  = new OptionSetValue(2), // Sandbox
    ["version"]        = "1.0.0.0",
    ["culture"]        = "neutral",
    ["publickeytoken"] = "<your public key token>"
};

service.Execute(new CreateRequest
{
    Target = assembly,
    ["SolutionUniqueName"] = "ContosoPlugins"
});

Every field there is needed:

  • Leave out isolationmode and Dataverse rejects the create: '<assembly>' is not allowed to be registered in full-trust mode, assembly must be registered in isolation.
  • Leave out version, culture and public key token and you get Unable to load plug-in assembly. The record holds no DLL of its own, since the DLL lives in the package. Those three values are how Dataverse finds the right DLL inside it.
  • SolutionUniqueName adds the new record to your solution instead of leaving it loose in the environment.

Step 2. Update the package again. Upload the same .nupkg with the Plugin Registration Tool or pac plugin push.

The new record starts empty, with no plugin types. A package update fills them in, but only for assemblies Dataverse already has a record for. That is why the order matters. Update twice without creating the record first and nothing happens.

After step 2 the plugin types are there, and you register steps as usual.

I also checked that the plugin actually runs after the fix. I changed it to throw an exception with a recognizable message, triggered it with a real record update, and got that exact message back.

Solution import does not save you

The natural next thought: fix it once in development, put it in a solution, and let the import carry it to test and production.

It does not.

I applied the fix in DEV, exported the unmanaged solution and imported it into TEST, where the package already existed. The import reported success. TEST still had one plugin assembly record. No record for the new assembly, and no plugin types.

The DLL itself did arrive. Export the solution again from TEST and the package inside contains both DLLs. Dataverse is holding the exact assembly it declined to register.

The reason is that a solution only references a package’s plugin assemblies. It does not define them. On import, Dataverse would have to open the package and work out the assemblies, which is what a create does and an update skips.

So the fix does not travel. Run steps 1 and 2 in every environment: development, test and production.

Do not delete the package

The fix most people find first is deleting the package and uploading it again as new. That works, because create does register both assemblies.

It also deletes every plugin assembly, plugin type and step registration in the package. In development that is not a problem. In production you probably don’t want to do that.

Reported to Microsoft

I have reported this as powerplatform-build-tools issue 1465. If you hit it too, please add your case there 🙏.

Until it is fixed, when you add a plugin assembly to an existing package, check that Dataverse has registered it correctly. If not, you have to create the plugin assembly record yourself, and update the plugin-package again.

If you use Flowline CLI, my open-source Dataverse deployment tool (why I built it), its push does exactly that. It warns you when Dataverse did not register an added assembly, creates the record, and updates the package again.

Prevent it: one plugin assembly per package

The best fix is never needing one.

Keep one plugin assembly per plug-in package. Microsoft recommends the same: one plugin assembly, as long as it stays under the 16 MB limit. Helper libraries and NuGet dependencies can go in the same package. With no IPlugin classes in them, they never need registering.

Want to split Sales and Service? Use folders and namespaces inside one project, not a second plugin project. If they really have to be separate assemblies, give each its own package.

This bug only bites when an existing package gains a plugin assembly. So do not give it one.

Earns Its Keep

Every Sunday, one post on Dataverse, .NET, and whether the tools we rely on still deserve to be there.