I have good news and bad news.
The good news is there are tons of great features out in the Dataverse, Copilot, AI, and everywhere.
The bad news, plugins are still a reliable way to solve problems when working with model-driven apps that make pipeline decisions most economical. There is a small part of me that believes the advanced features available to plugins (access deleted and pre-image data) will at some point be lost and no other alternative provided.
Until that time, though, we still have to write plugins and good code.
IPluginExecutionContext++
This caught me by surprise a few months ago when someone made a post about their being a seventh iteration of IPluginExecutionContext – which in all honesty, I had no idea there was a version 2 – 6 – but there it was staring me in the face with functionality I had been longing for – primarily access to information about the initiatingUser and whether they were an Application or a User.
You can see more on this version and all other older versions here.
Don’t default to IPluginExecutionContext
PAC Model Builder
I had not used the PAC Model Builder, but like anyone, I wanted to figure out a way to automagically bring down the entity structures into a common, shared library that would make for easier and cleaner use in code.
Using the PAC Model Builder (which I know you have installed, but if not, you can go here). Once set up, you can run a very simple command to pull down all of your entities into your project.
pac modelbuilder build --entitynamesfilter 'account;contact;df_vehicle;df_trust' --namespace 'Beta.Extensions.Model' --outdirectory './Model' --writesettingsTemplateFile --serviceContextName 'DataContext'
In the above script, I select the entities I want to pull down, give them a default namespace in a specified output directory and output my settings. The end result is a very nicely organized metadata structure without the need for any additional libraries.

My only complaint is the serialization between what you download and the actual entity structures themselves, which can result in some additional code to use them.
Account account = TargetEntity.ToEntity<Account>();
Entity accountToUpdate = new Entity(Account.EntityLogicalName)
{
Id = account.Id
};
accountToUpdate[AttributeHelper.AttributeName<Account>(a => a.Name)] = account.Name + " - Processed";
Service.Update(accountToUpdate);
Which I get, “looks” like late binding when I’m doing these kinds of updates, but for reading and processing to other resources, is cleaner to access and consume.
Shared Libraries
Yes, plugins still run in .NET 4.6.2, and yes, you can only deploy your one DLL into the PluginRegistration tool before it barfs on you, but that doesn’t mean your code structure has to be a big grouping of disparate classes that duplicate a helper file or shared class file all over the place.
This is what we want in a perfect world.
--Plugin Solution
----Plugin.Shared
----Plugin.Account
----Plugin.Vehicle
When deployed, you end up with your Plugin.Shared library being bundled into your Plugin.Account and Plugin.Vehicle library. To accomplish this goal, you can use ILRepac,k which takes your compiled dlls, merges them, and creates a new unit of code, which you can then sign and deploy via the Plugin Registration Tool.
For me, this involved building the following script that took my plugins and redeployed them to a shared Deployment folder, repacked, and signed. For every new plugin that I build, I just include this into the project’s configuration, build, and we’re good to go.
I have added my comments inline in bold to help with the walkthrough.
<Target Name="ILRepack" AfterTargets="Build">
<PropertyGroup>
<ILRepackExe>$(SolutionDir)packages\ILRepack.2.0.44\tools\ILRepack.exe</ILRepackExe>
<DeployFolder>$(SolutionDir)Deploy\$(Configuration)\</DeployFolder>
<OutputAssembly>$(TargetDir)$(TargetFileName)</OutputAssembly>
Build the Shared Plugin <PluginDll>$(SolutionDir)Extensions\bin\$(Configuration)\Extensions.dll</PluginDll>
<!-- Remove trailing slashes from lib paths -->
<LibPath1>$(TargetDir.TrimEnd('\'))</LibPath1>
<LibPath2>$(SolutionDir)Extensions\bin\$(Configuration)</LibPath2>
<!-- Use the shared key file -->
<KeyFile>$(SolutionDir)Deploy\Extensions.Plugins.snk</KeyFile>
</PropertyGroup>
Output data into VS Build Console - pretty cool!
<Message Importance="high" Text="ILRepack: $(ILRepackExe)" />
<Message Importance="high" Text="Output: $(OutputAssembly)" />
<Message Importance="high" Text="Plugin: $(PluginDll)" />
<Message Importance="high" Text="KeyFile: $(KeyFile)" />
<Error Condition="!Exists('$(ILRepackExe)')" Text="ILRepack.exe not found" />
<Error Condition="!Exists('$(PluginDll)')" Text="Extensions.dll not found" />
If my deploy directory doesn't exist, create one.
<MakeDir Directories="$(DeployFolder)" Condition="!Exists('$(DeployFolder)')" />
<!-- Use the cleaned paths and run ILRepack -->
<!-- Also note, here is where we sign the fully merged and compiled assembly which is done outside of the individual plugin project. -->
<Exec Command=""$(ILRepackExe)" /lib:"$(LibPath1)" /lib:"$(LibPath2)" /keyfile:"$(KeyFile)" /out:"$(DeployFolder)$(TargetFileName)" "$(OutputAssembly)" "$(PluginDll)" /internalize" />
<ItemGroup>
<PdbFiles Include="$(TargetDir)*.pdb" />
<PluginPdb Include="$(SolutionDir)Extensions\bin\$(Configuration)\*.pdb" />
<ConfigFiles Include="$(TargetDir)*.config" />
</ItemGroup>
<Copy SourceFiles="@(PdbFiles)" DestinationFolder="$(DeployFolder)" SkipUnchangedFiles="true" />
<Copy SourceFiles="@(PluginPdb)" DestinationFolder="$(DeployFolder)" SkipUnchangedFiles="true" />
<Copy SourceFiles="@(ConfigFiles)" DestinationFolder="$(DeployFolder)" SkipUnchangedFiles="true" />
<Message Importance="high" Text="Merged assembly created in: $(DeployFolder)" />
</Target>
When complete, you end up with a much cleaner architecture that cleanly lays out the physical architecture of your code, making it much easier to read, consume, and deploy without duplicate code spread throughout your code.
The Final Result
The end goal of doing all these little things was built with the next developer in mind (always build for the person behind you). All these little things add up, and now, when a developer goes to create a plugin, this is where they start and where their focus lies.

Where PluginBase does all the initialization logic, tracing, context depth checks, etc, of our plugin,s and the developer is left with a great, clean experience to write some plugins (which AI still recommends and are not going away any time soon).