🥱 TL;DR
FileToFile
mappings for plugin package .nupkg
files in Dataverse Solution Packager repeatedly throw Error: Value cannot be null. Parameter name: key
during pac solution sync
. Everything else (FileToPath
, WebResources, metadata) works.
Workaround:
- Add an MSBuild target that creates a stable, versionless
ava_Plugins.nupkg
alongside the versionedPlugins.x.y.z.nupkg
. - Maintain two mapping files—one without the plugin package line for sync, one (optional) with it for future pack scenarios.
- Use a short PowerShell script to run sync, then clean or regenerate the stable package as needed.
Result: deterministic builds, clean repository, and an isolated workaround until an upstream fix lands.
🚀 1. Introduction
When we work on Dataverse solutions we routinely leverage the Solution Packager mapping file to keep the repository clean and to steer build artifacts. Everything behaves as expected for WebResources and other assets—until we introduce a FileToFile
mapping for a Plugin Package (pluginpackages/.../package/*.nupkg
). Every time we try that, the same failure appears:
Error: Value cannot be null.
Parameter name: key
This post documents the persistent problem, the investigation path, the changes made to the plugin project (Plugins.csproj
in these examples), and the pragmatic workaround we apply each time: maintaining two mapping files plus a tiny automation script.
🤷🏻 Name Mismatch Deep Dive (Why a Stable Copy Is Mandatory)
Here's where things get frustrating. Even before we hit the cryptic null-key bug, there's a sneaky naming mismatch that'll drive you crazy if you don't catch it early.
Picture this: you build your plugin project and get Plugins.1.0.0.nupkg
. Perfect! But when you extract a Dataverse solution, the same logical package shows up as ava_Plugins.nupkg
. Wait, what? 🤔
Source | Default Naming Rule | Example Output | Changes When | Stable? | Used By |
---|---|---|---|---|---|
Plugin project build (dotnet pack implicit/explicit) |
<PackageId> + . + <Version>
|
Plugins.1.0.0.nupkg → Plugins.1.0.1.nupkg
|
Every version bump | No | NuGet feeds, artifact stores, human traceability |
Dataverse solution export (pluginpackages node) |
Publisher prefix + logical package name | ava_Plugins.nupkg |
Rare (only if solution structure/prefix changes) | Yes | Solution Packager re-import, ALM pipelines |
This mismatch will bite you in multiple ways:
-
FileToFile
inmap.xml
demands exact filenames—no wildcards, no "close enough." - Every time you bump versions (1.0.0 → 1.0.1), you'd have to update your mapping file manually. In a fast-moving project, that's a recipe for "oops, forgot to update the map again."
- Dataverse doesn't care about your semantic versioning—it wants that stable
ava_Plugins.nupkg
name, period. - Even if Microsoft fixes the null-key bug tomorrow, you still need that predictable filename or you're back to manual map editing hell.
Our approach: create a "stable twin" during the build. The MSBuild target spots any Plugins.*.nupkg
and copies it to ava_Plugins.nupkg
(keeping both, or deleting the versioned one if you prefer less clutter). Now your mapping file can reference a filename that never changes, and your versioned packages can still live their best life in artifact repositories.
We tried other approaches first:
- Just rename the primary package → Nope, then you lose version traceability when you push to feeds.
- Update the map file every release → High maintenance nightmare, especially when you're shipping weekly.
- Symlinks or hardlinks → Sounds clever until your CI agent doesn't have permission or you hit cross-platform issues.
- Manual rename after sync → Fragile, easy to forget, and definitely not CI-friendly.
The copy approach handles the edge cases too:
- Multiple versioned packages lying around? It grabs them all, picks one to copy, optionally cleans up the rest.
- First build ever? Stable file magically appears.
- Rebuilding the same version? Only copies if the stable file is missing (keeps builds fast).
Bottom line: this isn't some fancy optimization—it's the foundation that makes everything else possible. Without that stable filename, you're constantly fighting the tooling instead of building features.
To address for the Name mismatch issue, we could simply add the following snippet to our Plugins.csproj
file. The snippet simply renames the generated package (Plugins.1.0.0.nupkg
) as expected by the Dataverse solution project (ava_Plugins.nupkg
).
<PropertyGroup>
<DebugBaseDir>$(ProjectDir)bin\Debug\</DebugBaseDir>
<StablePluginPackage>$(DebugBaseDir)ava_Plugins.nupkg</StablePluginPackage>
<DeleteVersionedOnCopy>true</DeleteVersionedOnCopy>
</PropertyGroup>
<Target Name="CopyStablePluginPackage" AfterTargets="Build;Pack">
<ItemGroup>
<VersionedPackages Include="$(DebugBaseDir)Plugins.*.nupkg" Exclude="$(StablePluginPackage)" />
</ItemGroup>
<Copy SourceFiles="@(VersionedPackages)" DestinationFiles="$(StablePluginPackage)"
Condition="!Exists('$(StablePluginPackage)') and '@(VersionedPackages)' != ''" />
<Delete Files="@(VersionedPackages)" Condition="'@(VersionedPackages)' != '' and '$(DeleteVersionedOnCopy)' == 'true'" />
</Target>
📂 2. Why Mapping Files Matter
We have already discussed this topic here.
Mapping files give us two high‑value benefits:
- Keep build artifacts (generated or compiled outputs) out of source control by redirecting extracted solution components to curated locations under
src/
. - Let the .NET build (and related tooling) know where to find externally‑mapped artifacts when we later repack or rebuild the solution (deterministic layout).
For WebResources and most XML/JSON metadata, FileToPath
patterns work flawlessly. The friction appears specifically with Plugin Package .nupkg
binaries when using FileToFile
.
🎯 3. The Issue
Here's what we're trying to solve: we want plugin packages out of source control but available for builds. The FileToFile
mapping should accomplish two things:
-
During
pac solution sync
: Skip creating the plugin package locally (since we're pointing it to an external file) - During solution builds: Let the compiler grab the plugin package from our project's build output instead of the default location
The ideal workflow: sync extracts everything except the plugin package (which stays out of source control), then when you build the solution, it finds the plugin package in src/Plugins/bin/Debug/ava_Plugins.nupkg
where our MSBuild target puts it. Clean repository, deterministic builds, everyone's happy.
So you add a FileToFile
mapping to your map.xml
:
<FileToFile map="pluginpackages\ava_Plugins\package\ava_Plugins.nupkg"
to="..\\..\\..\\..\\..\\src\\Plugins\\bin\\Debug\\ava_Plugins.nupkg" />
Make sure your stable package exists (more on that later), run pac solution sync --map .\map.xml -loc
, and... boom:
Error: Value cannot be null. Parameter name: key
Remove that one line? Works perfectly. Add it back? Instant failure. It's like the tooling has a personal vendetta against plugin package remapping.
🧪 4. Analysis of the Failure
Observations:
- The error fires only when a
FileToFile
directive targets the Plugin Package.nupkg
. - The same mapping file is perfectly accepted later by MSBuild / pack operations (the build does not care about the mapping; it simply uses the stable binary).
- All relative path depth corrections (2, 5, 6
..
) did not resolve the null key error. - Using
FileToPath
wildcards for pluginpackages (pluginpackages\**
) avoided the exception but did not relocate the binary (Solution Packager still emitted the file into its default folder).
Likely internal cause: Solution Packager's plugin package handling expects the physical file to remain in its canonical relative location and does not fully support remapping its binary via the map dictionary used during unpack/localization. When the dictionary lookup for the remapped external target occurs, the expected key is absent → null.
🔧 5. The Practical Workaround: Dual Mapping Files
Because the FileToFile
mapping for the plugin package breaks pac solution sync
, but is still desirable for a future pack/build context, we maintain two mapping files:
Purpose | File | Contains Plugin FileToFile ? |
---|---|---|
Sync from Dataverse → FS | map.sync.xml |
No |
Build / (Optional future pack) | map.build.xml |
Yes |
Workflow:
- Run sync using
map.sync.xml
(no plugin mapping → no error). - Manually (or via script) remove the extracted nupkg under
solutions/.../pluginpackages/.../package/
. - Build solution; the csproj target recreates stable
ava_Plugins.nupkg
. - Use
map.build.xml
only if you later need a pack that references the external stable file (until the PAC CLI issue is fixed, avoid using it for sync).
📝 Solution Project Configuration for Build Mapping
To enable the solution project to use the map.build.xml
file during builds, add the following configuration to your master.cdsproj
file:
<PropertyGroup>
<SolutionPackageMapFilePath>$(MSBuildThisFileDirectory)map.build.xml</SolutionPackageMapFilePath>
<SolutionPackageType Condition="'$(Configuration)' == 'Debug'">Unmanaged</SolutionPackageType>
<SolutionPackageType Condition="'$(Configuration)' == 'Release'">Managed</SolutionPackageType>
<SolutionPackageErrorLevel Condition="'$(Configuration)' == 'Debug'">Verbose</SolutionPackageErrorLevel>
<SolutionPackageErrorLevel Condition="'$(Configuration)' == 'Release'">Info</SolutionPackageErrorLevel>
<SolutionPackageEnableLocalization>true</SolutionPackageEnableLocalization>
<!-- The value for this property has to be passed during runtime when you run dotnet build -->
<SolutionFileVersion></SolutionFileVersion>
<SolutionPackageZipFilePath Condition="'$(SolutionFileVersion)' != '' AND '$(Configuration)' == 'Debug'">
$(OutputPath)$(MSBuildProjectName)_v$(SolutionFileVersion)_unmanaged.zip</SolutionPackageZipFilePath>
<SolutionPackageZipFilePath Condition="'$(SolutionFileVersion)' != '' AND '$(Configuration)' == 'Release'">
$(OutputPath)$(MSBuildProjectName)_v$(SolutionFileVersion)_managed.zip</SolutionPackageZipFilePath>
</PropertyGroup>
This configuration:
- Points the solution build to use
map.build.xml
(which includes the plugin package mapping) - Sets up different package types and error levels for Debug vs Release builds
- Enables localization support
- Configures versioned output file naming for solution packages
With this setup, when you build the solution project (dotnet build
or MSBuild), it will properly locate the plugin package via the FileToFile
mapping in map.build.xml
.
🤖 6. Automation Script (Sync + Cleanup)
To be sure to always perform step 1 and 2 described above, I tend to use a special-purpose powershell script.
sync.ps1
(proposed):
param(
[string]$SolutionFolder = "."
)
$SyncMap = "$SolutionFolder/map.sync.xml"
Write-Host "[Sync] Using map: $SyncMap"
pac solution sync --map $SyncMap -loc || throw "Solution sync failed"
# Find and remove all .nupkg files under the solution folder
$nupkgFiles = Get-ChildItem -Path $SolutionFolder -Recurse -Filter "*.nupkg" -File
if ($nupkgFiles.Count -gt 0) {
Write-Host "[Cleanup] Found $($nupkgFiles.Count) .nupkg file(s) to remove:"
foreach ($file in $nupkgFiles) {
Write-Host " - $($file.FullName)"
Remove-Item $file.FullName -Force
}
Write-Host "[Cleanup] All .nupkg files removed from solution folder."
} else {
Write-Host "[Cleanup] No .nupkg files found in solution folder (already clean)."
}
⚖️ 8. Pros & Cons of Dual-Map Strategy
Pros
- Unblocks daily sync workflow immediately.
- Keeps repository clean of large binary artifacts.
- Build remains deterministic (stable filename always present).
Cons
- Two mapping files add cognitive overhead.
- Extra cleanup step (script) until the underlying issue is fixed.
- Risk of accidental use of the build map during sync (document clearly in README / contributor guide).
⚠️ Disclaimer
If/when the upstream behavior changes, this post can be revisited to retire the workaround.
Feel free to adapt the script & structure to your own Dataverse ALM conventions.
Top comments (0)