DEV Community

Cover image for When Plugin Packages Break Your Mapping Files (And How to Fix It)
Riccardo Gregori
Riccardo Gregori

Posted on

When Plugin Packages Break Your Mapping Files (And How to Fix It)

🥱 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:

  1. Add an MSBuild target that creates a stable, versionless ava_Plugins.nupkg alongside the versioned Plugins.x.y.z.nupkg.
  2. Maintain two mapping files—one without the plugin package line for sync, one (optional) with it for future pack scenarios.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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.nupkgPlugins.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 in map.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:

  1. Just rename the primary package → Nope, then you lose version traceability when you push to feeds.
  2. Update the map file every release → High maintenance nightmare, especially when you're shipping weekly.
  3. Symlinks or hardlinks → Sounds clever until your CI agent doesn't have permission or you hit cross-platform issues.
  4. 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>
Enter fullscreen mode Exit fullscreen mode

📂 2. Why Mapping Files Matter

We have already discussed this topic here.
Mapping files give us two high‑value benefits:

  1. Keep build artifacts (generated or compiled outputs) out of source control by redirecting extracted solution components to curated locations under src/.
  2. 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:

  1. During pac solution sync: Skip creating the plugin package locally (since we're pointing it to an external file)
  2. 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" />
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Run sync using map.sync.xml (no plugin mapping → no error).
  2. Manually (or via script) remove the extracted nupkg under solutions/.../pluginpackages/.../package/.
  3. Build solution; the csproj target recreates stable ava_Plugins.nupkg.
  4. 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>
Enter fullscreen mode Exit fullscreen mode

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)."
}
Enter fullscreen mode Exit fullscreen mode

⚖️ 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)