Skip to content

Debugging Plugins

Peter McDonald edited this page Aug 17, 2026 · 10 revisions

Debugging Plugins

Dataverse plug-ins run on the server, so you can't attach a debugger directly. The Plugin Profiler captures a plug-in's exact execution context; Dataverse PowerTools turns that capture into a unit test you debug in VS Code — set breakpoints, F5, step through — with the real server-side context, no live org needed.

The whole loop is one-click inside VS Code on Windows, macOS and Linux. Capturing stopped being Windows-only in 1.0.7 — the extension starts and stops profiling over the Dataverse Web API itself, with no Plugin Registration Tool and no bundled .NET Framework helper. The one step that still needs .NET Framework is running the replay test, because your plug-in test project targets it (Windows, or mono elsewhere).

The plugin card's Debugging section (and the Command Palette) drives all of it.

Plugin debugging in Dataverse PowerTools — the generated replay test, the replay running green in-process (no live org), and a rendered trace log

Step-by-step with screenshots: Profiling a Plug-in Run walks the whole capture → download → replay loop with the button, dialog and picker for each step highlighted.

Prerequisites

Before any of the debugging functions work you need:

  • A deployed plug-in with a registered stepBuild Package & Deploy from the plugin card. There is nothing to profile until the plug-in is on the server with a step that fires.
  • The Plugin Profiler solution installed in the environment — a one-time, per-environment install. Profile next run installs it for you on any OS. (You can still install it via the Plugin Registration Tool if you prefer.)
  • dotnet and pac on your PATH (see Requirements).
  • A plugin test project for the replay step — run Setup Plugin Unit Testing from the plugin card if you don't have one.
  • .NET Framework to RUN the replay — the replay is a test in your plug-in test project, which targets .NET Framework (net471), so executing it (and so Replay & debug) needs Windows, or mono on macOS/Linux. dotnet build compiles it anywhere; dotnet test needs the Framework test host. Capturing, Download a run, Generate Replay Test and the trace logs are all cross-platform. (There is a workaround if you want replays on macOS/Linux today.)
  • The C# extension (ms-dotnettools.csharp) — it provides the .NET debugger Replay & debug attaches. Without it the replay still runs; nothing stops.

The overall process

flowchart TD
    A[Build Package &amp; Deploy<br/>plug-in + registered step] --> C[Debugging → Profile next run<br/>installs profiler, starts capture]
    C --> E[Trigger the plug-in in your app<br/>create/update the record, run the action]
    E --> F[Pick the captured run<br/>saved into profiles/]
    F --> G[Set breakpoints in<br/>Execute / ExecuteDataversePlugin]
    G --> H[Debugging → Replay &amp; debug<br/>generates Replay_Class_timestamp.cs]
    H --> I[Debug the generated test<br/>from Test Explorer / dotnet test]
    I --> J([Breakpoints hit with the exact<br/>captured server-side context — no live org])
Loading

Three stages — profile (capture a real execution), download (bring the capture local), replay & debug (turn it into a test you step through). All three are one-click in VS Code on every OS; only running the generated test needs .NET Framework. Each is detailed below.

1. Profile the next run (any OS — in VS Code)

Deploy your plug-in and register a step first (Build Package & Deploy). Then, in the plugin card's Debugging section, click Profile next run:

  1. If the Plugin Profiler solution isn't installed yet, Dataverse PowerTools installs it for you (one-time, ~30s).
  2. Pick the step to profile (skipped if there's only one).
  3. When prompted, trigger the plug-in — do the action that fires it (create/update the record, run the action) in your app — then click Continue.
  4. The captured executions are listed; pick the run you want (a plug-in that fired many times shows them all, newest first). It's downloaded into profiles/ and profiling is stopped automatically.

Under the hood this installs the profiler solution and starts/stops profiling over the extension's own connection — no Plugin Registration Tool window, no separate sign-in, under both service-principal and interactive auth, on any OS.

Stopping also puts your step back exactly as it was: the profiler works by registering a clone of your step and disabling the original, so stopping restores the original's name, its images and its enabled state. (Before 1.0.7, stopping on macOS/Linux deleted the clone but left your step disabled — if you hit that, re-enable the step once and it won't recur.)

2. Download a captured run (any OS)

Debugging → Download a run lists the captured Plug-in Profile rows in your environment (type · message · table · sync/async · time) and saves the ones you pick into profiles/. Use this to grab a run captured earlier, or one captured by a colleague or in the Plugin Registration Tool.

You can also just drop a profile file exported from PRT (View Plug-in Profile → download) into profiles/.

3. Replay and debug (any OS)

  1. Set breakpoints in your plug-in's Execute / ExecuteDataversePlugin.
  2. Debugging → Replay & debug. It runs the captured execution back through your plug-in with the debugger attached, so those breakpoints are hit — with the exact server-side context that was captured, and no live org. If there is no replay test yet it writes one first; with several profiles in profiles/ it asks which one.
  3. Inspect, step, then Continue to let the run finish (or Stop to end the session).

Generate Replay Test — the button next to it — writes the same Replay_<Class>_<timestamp>.cs without running it. That is the one you commit: it decodes the profile, rebuilds the IPluginExecutionContext and invokes your plug-in in-process, so it runs under dotnet test in CI like any other test.

The generated replay test

Requirements: a plugin test project (run Setup Plugin Unit Testing if you don't have one), the C# extension (ms-dotnettools.csharp) for the debugger, and Windows to run it — the plug-in test project targets .NET Framework, the same constraint PRT has.

There is a full walkthrough with screenshots at Profiling a Plug-in Run.

The Profile & Debug… CodeLens above a [CrmPluginRegistration] class is the cross-platform / PRT-guided entry point:

The Profile & Debug CodeLens on a decorated plug-in class, with the Debugging block in the panel

Trace logs (a separate diagnostic — any OS)

Profiling replays one captured execution offline. Trace logs are the complementary live diagnostic: whatever your plug-in writes with ITracingService.Trace(...), captured server-side as it runs. Use trace logs to see what actually happened in production; use profiling to step through it in a debugger. Both are cross-platform (they're plain Web API calls — no profiler assemblies).

The org-wide trace level lives on the organization row and is surfaced as a coloured pill in the panel's org header:

Pill Level Meaning
🟢 Trace: Off 0 No plug-in trace logging
🟠 Trace: Errors 1 Log traces only when a plug-in throws
🔴 Trace: All 2 Log every plug-in execution — has a performance and storage cost
  • Change the level — click the pill, or run Set Plugin Trace Log Level from the palette, and pick Off / Exception only / All. Switching to All asks for confirmation (it's a firehose — remember to turn it back off when you're done). The pill re-colours immediately. Works under both service-principal and interactive auth.
  • Read the logsView Plugin Trace Logs lists the recent plugintracelog rows (most recent first); pick one and it opens as a formatted Markdown document (type name, message, mode, duration, and the trace text / exception):

A rendered plug-in trace log — type, message, timings and the Trace() output

Recommended route today

  • On Windows: Profile next run → trigger the plug-in → pick the run → set breakpoints → Replay & debug. Everything is one-click in VS Code.
  • On macOS / Linux: identical up to the last step — Profile next run → trigger → pick the run → Generate Replay Test, which you can commit and run in CI. Running and debugging that test needs .NET Framework, so use a Windows host (or mono) for the step-through itself.
  • Just want to see what happened in production, without a debugger? Set trace logging to Errors (or All briefly), reproduce, then View Plugin Trace Logs — no profiler, no test project, any OS.

How it works

Capturing requires the profiler to be pipeline-executable. That is more than creating a step: the profiler registers a clone of your step pointing at its own plug-in type, carrying the stage, mode, rank, message, filter and deployment across, moves your step's images onto the clone, and disables your original step — miss any of those and the profiler silently never fires, which is why naive Web-API step edits don't work. It also writes a serialized configuration blob the profiler's own server-side plug-in reads back. Dataverse PowerTools does all of that over the Web API with your existing access token, so Profile next run needs no PRT GUI and no Windows (before 1.0.7 it shelled out to a bundled .NET Framework helper, which is why capture used to be Windows-only). The captured profile is stored as a mbs_pluginprofile row. The replay does not use the profiler's own assemblies: the generated harness decodes the profile (base64 → deflate → report XML) into an IPluginExecutionContext using Microsoft.Xrm.Sdk alone, then calls your plug-in in-process. That is what lets it run as an ordinary unit test — in CI, with no live org and no child AppDomain.

Running replays on macOS / Linux without .NET Framework

Only the deployed plug-in assembly has to target .NET Framework — that is a Dataverse sandbox requirement. The copy your tests run against does not, and the replay harness itself uses nothing Framework-specific. So you can multi-target the plug-in project and run replays on .NET 8 natively.

In your plug-in .csproj, target both and pick the SDK package per target:

<TargetFrameworks>net462;net8.0</TargetFrameworks>

<ItemGroup Condition="'$(TargetFramework)'=='net462'">
  <PackageReference Include="Microsoft.CrmSdk.CoreAssemblies" Version="9.0.2.*" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net8.0'">
  <!-- netstandard2.0 Microsoft.Xrm.Sdk -->
  <PackageReference Include="Microsoft.PowerPlatform.Dataverse.Client" Version="1.2.5" />
</ItemGroup>

Then point the test project at net8.0 and give it the same Microsoft.PowerPlatform.Dataverse.Client reference, in place of the Microsoft.CrmSdk.CoreAssemblies reference Replay & debug adds for you. Deploy the net462 output as usual.

dotnet test then runs the replay on the modern runtime, and breakpoints work too — the extension picks the coreclr debugger automatically for any non-net4 target.

Caveats. A plug-in that uses .NET-Framework-only APIs won't compile for net8.0, and early-bound classes generated for net462 are worth re-checking under net8. This is a hand-rolled recipe, not something the scaffolder does yet — see #269.

Troubleshooting

Symptom Cause / fix
"No registered plugin steps" when profiling The plug-in is deployed but has no registered step, so there's nothing to profile. Register a step (Build Package & Deploy) and retry.
No captured profiles after triggering The trigger didn't fire the profiled step (wrong message/table/stage), or profiling wasn't started. Confirm the step you picked matches the action you performed, then trigger again.
Profiler not installed Profile next run installs it automatically on any OS; if that fails, install the Plugin Profiler solution once via the Plugin Registration Tool. It's per-environment.
Replay does nothing / can't find a test project Run Setup Plugin Unit Testing to create the test project first; Replay & debug writes the generated Replay_*.cs into it.
Replay test won't RUN on macOS / Linux Your plug-in test project targets .NET Framework, and dotnet test needs its test host — on Linux it builds and then aborts with "Could not find 'mono' host". Install mono, run the test on Windows, or multi-target to net8. Capture, Download a run, Generate Replay Test and trace logs all work natively.
A step is stuck disabled after profiling The profiler disables your original step while it profiles the clone, and restores it when you stop. If a session was interrupted (or you stopped on macOS/Linux before 1.0.7), re-enable the step — the panel's Active profiles list shows anything still being profiled.
Trace pill greyed out / trace commands say "Connect to Dataverse first" There's no live connection. Connect the project to Dataverse, then the trace pill and Set/View Plugin Trace Log commands light up.

The screenshots above are captured from the Dataverse PowerTools UI (against a demo connection). Live captures against a running model-driven app — e.g. a form re-rendering after a hot-reload, or a live-triggered trace — are best recorded in normal use.

Clone this wiki locally