When we are generating a plugin project using Power Platform CLI > “pac plugin init” command, the csproj that is being generated it’s different from the one that SparkleXrm by Scott Durrow has (the plugin project generated using “pac plugin init” I believe using minimal csproj file). That is why when we are installing spkl NuGet package to the project, it will not add those files automatically.
Below is the difference comparing both .csproj files (the right side is generated using “pac plugin init“):

Once you generated the plugin project by using “pac plugin init“, what you need to do is to install the spkl NuGet package (here is the csproj I ended up with):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net462</TargetFramework>
<PowerAppsTargetsPath>$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps</PowerAppsTargetsPath>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Blog.snk</AssemblyOriginatorKeyFile>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<ProjectTypeGuids>{4C25E9B5-9FA6-436c-8E19-B395D2A65FAF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
</PropertyGroup>
<Import Project="$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Plugin.props" Condition="Exists('$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Plugin.props')" />
<!--
NuGet pack and restore as MSBuild targets reference:
https://docs.microsoft.com/en-us/nuget/reference/msbuild-targets
-->
<PropertyGroup>
<PackageId>Blog</PackageId>
<Version>$(FileVersion)</Version>
<Authors>temmy</Authors>
<Company>MyCompany</Company>
<Description>This is a sample nuget package which contains a Dataverse plugin and its runtime dependencies like Newtonsoft.Json</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CrmSdk.CoreAssemblies" Version="9.0.2.*" PrivateAssets="All" />
<PackageReference Include="Microsoft.PowerApps.MSBuild.Plugin" Version="1.*" PrivateAssets="All" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.*" PrivateAssets="All" />
<PackageReference Include="spkl" Version="1.0.640" PrivateAssets="All" />
</ItemGroup>
<Import Project="$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Plugin.targets" Condition="Exists('$(PowerAppsTargetsPath)\Microsoft.PowerApps.VisualStudio.Plugin.targets')" />
</Project>
Then, because some files are not automatically added like the old csproj, we need to copy-paste and modify it:
You can get CrmPluginRegistrationAttribute.cs from this GitHub page.
For the spkl.json, I’m using this one:
{
"plugins": [
{
"profile": "default,debug",
"assemblypath": "bin\\Debug\\net462"
}
],
"earlyboundtypes": [
{
"entities": "contact",
"generateOptionsetEnums": "true",
"generateStateEnums": "true",
"filename": "Entities/EarlyBoundTypes.cs",
"classNamespace": "SpklPlugins"
}
]
}
As you can see, for “assemblypath“, we need to add “bin\Debug\net462” (different from the original file).
Then, you can add deploy-plugins.bat as the command prompt to deploy the plugin:
@echo off
set package_root=%userprofile%\.nuget\packages
REM Find the spkl in the package folder (irrespective of version)
For /R %package_root% %%G IN (spkl.exe) do (
IF EXIST "%%G" (set spkl_path=%%G
goto :continue)
)
:continue
@echo Using '%spkl_path%'
REM spkl plugins [path] [connection-string] [/p:release]
"%spkl_path%" plugins "%cd%\.." %*
if errorlevel 1 (
echo Error Code=%errorlevel%
exit /b %errorlevel%
)
pause
Based on my observation, when we are using the minimal csproj file. All the NuGet package files will be stored in different folders (global packages) as described in this link. So for line number 2, we need to adjust the root folder to “%userprofile%\.nuget\packages
“.
This is the sample of my plugin + registration attribute that I demoing for today:
using System.Linq;
using Microsoft.Xrm.Sdk;
using System.Text;
using Microsoft.Xrm.Sdk.Extensions;
namespace Blog
{
[CrmPluginRegistration(MessageNameEnum.Update, "contact", StageEnum.PreOperation, ExecutionModeEnum.Synchronous, "",
"PreContactUpdate", 1, IsolationModeEnum.Sandbox,
Image1Attributes = "", Image1Name = "PreImage", Image1Type = ImageTypeEnum.PreImage)]
public class PreContactUpdate : PluginBase
{
public PreContactUpdate() : base(typeof(PreContactUpdate))
{
}
protected override void ExecuteDataversePlugin(ILocalPluginContext localPluginContext)
{
var entityImage = localPluginContext.PluginExecutionContext.PreEntityImages["PreImage"];
var target = localPluginContext.PluginExecutionContext.InputParameterOrDefault<Entity>("Target");
var sb = new StringBuilder();
var validAttributes = new[] { "jobtitle", "emailaddress1", "telephone1" };
var attributes = entityImage.Attributes
.Where(e => validAttributes.Contains(e.Key)).ToArray();
foreach (var attribute in attributes)
{
sb.AppendLine($"Attribute '{attribute.Key}': {attribute.Value}");
}
throw new InvalidPluginExecutionException(sb.ToString());
}
}
}
Here is the project structure to validate what we have done so far:

We can build, then execute the deploy-plugins.bat to deploy the Plugin:

Using Plugin Registration Tools, I verify the above operation:

The PreImage also generated correctly:

Happy CRM-ing!