- Remote Debugger
The XrmFramework Remote Debugger lets you step through plugin and Custom API execution in your local Visual Studio session while the plugin is triggered by real user actions (or automated processes) on a live Dataverse environment — including dev, UAT, or even production sandboxes.
The mechanism relies on Azure Relay Hybrid Connections: when a plugin fires in Dataverse, the framework checks whether the initiating (or root) user has an active DebugSession record. If so, the full execution context is serialised and forwarded over the relay to your local machine, which executes the plugin code locally. Any IOrganizationService calls made by the local plugin are transparently forwarded back to Dataverse through the same relay channel, so you work with real data.
Dataverse sandbox Azure Relay Developer machine
───────────────── ─────────── ─────────────────
Plugin fires
│
├─ DebugSession found for user?
│ No → execute normally
│ Yes ──────────────────────► Hybrid Connection ──────────► RemoteDebugger receives context
│ │
│ ├─ Resolves plugin type locally
│ ├─ Executes plugin in local process
│ │ (breakpoints, watches, etc.)
│ │
│◄──────────────────────────────── Hybrid Connection ◄───────── Returns modified context (or exception)
│
└─ Dataverse applies the updated context
Every IOrganizationService call emitted by your local plugin is intercepted, forwarded to Dataverse through the relay, and the response is returned to the local plugin — making the debugging experience fully transparent with respect to real data.
- An Azure Relay namespace with a Hybrid Connection endpoint.
- The
DebugSessionentity deployed in your Dataverse environment (it is part of the XrmFramework managed solution). - The
XrmFramework.RemoteDebugger.ClientNuGet package referenced in your local runner project.
Create a DebugSession record in Dataverse (via the model-driven app or via code) with the following fields:
| Field | Description |
|---|---|
DebugeeId |
The Dataverse user whose actions will be intercepted (lookup to systemuser). |
SessionEnd |
Expiry date/time of the session. The plugin checks SessionEnd >= DateTime.Today; expired sessions are ignored automatically. |
RelayUrl |
Base URL of your Azure Relay namespace (e.g. https://mynamespace.servicebus.windows.net). |
HybridConnectionName |
Name of the Hybrid Connection within the namespace. |
SasKeyName |
The Shared Access Signature key name (e.g. RootManageSharedAccessKey). |
SasConnectionKey |
The corresponding SAS key value. |
Tip: Keep sessions short-lived. Set
SessionEndto a few hours ahead of your debugging session to avoid accidental interception after you finish.
Create a console application (or use the template provided in Utils\RemoteDebugger) that references your plugin assembly and the XrmFramework.RemoteDebugger.Client package, then choose a mode (see below).
Simple console mode — blocks until the user presses Enter:
using XrmFramework.RemoteDebugger.Common;
var debugger = new RemoteDebugger<AzureRelayHybridConnectionMessageManager>();
debugger.Start();In standard mode:
- Each intercepted execution is printed to the console.
- You can set breakpoints in your plugin code in Visual Studio, attach to the runner process, and they will be hit when the plugin fires.
The TUI mode provides a rich, real-time terminal interface built with Spectre.Console:
using XrmFramework.RemoteDebugger.Common;
var debugger = new RemoteDebugger<AzureRelayHybridConnectionMessageManager>();
debugger.SessionSavePath = @".\PluginTestSessions"; // optional — enables auto-save
debugger.StartWithConsoleUI();The TUI displays a live table of all intercepted executions, each row showing:
- Execution status (pending / running / completed / failed)
- Plugin type (short name)
- Elapsed duration
- Number of
IOrganizationServicecalls made
| Key | Action |
|---|---|
↑ / ↓ |
Navigate the execution list |
Enter |
Zoom in — show full detail of the selected execution |
Esc |
Zoom out — return to the list view |
R |
Replay the selected execution without the debugger |
D |
Replay the selected execution in debug mode (prompts you to attach the debugger) |
S |
Save the selected execution as a .pluginsession.json file |
Q |
Quit |
Every intercepted execution can be persisted to disk as a PluginTestSession (.pluginsession.json). This file captures the full execution context, all IOrganizationService responses, and the final output context. It can be replayed locally without requiring any live Dataverse connection.
Set SessionSavePath before calling StartWithConsoleUI() or Start():
debugger.SessionSavePath = @".\PluginTestSessions";After each successful execution, the session is automatically saved to that directory.
Replay a saved session directly from code — useful for unit / integration tests:
using XrmFramework.RemoteDebugger.Common;
var session = PluginTestSessionRecorder.Load(@".\PluginTestSessions\mySession.pluginsession.json");
var output = PluginTestRunner.Run(session);
Console.WriteLine($"OutputParameters: {output.OutputParameters?.Count ?? 0}");
Console.WriteLine($"SharedVariables: {output.SharedVariables?.Count ?? 0}");PluginTestRunner.Run resolves the plugin type from the saved assembly-qualified name, re-creates the full IServiceProvider from the recorded responses, and executes the plugin locally — making it ideal for regression tests after a code change.
RemoteDebugger<T> exposes events you can subscribe to in order to integrate with your own logging or monitoring infrastructure:
var debugger = new RemoteDebugger<AzureRelayHybridConnectionMessageManager>();
debugger.ExecutionStarted += record => Console.WriteLine($"[START] {record.PluginShortName}");
debugger.OrgServiceCallStarted += (record, call) => Console.WriteLine($" → {call}");
debugger.OrgServiceCallCompleted += (record, call) => Console.WriteLine($" ← {call.Duration}");
debugger.ExecutionCompleted += record => Console.WriteLine($"[OK] {record.PluginShortName} ({record.Duration})");
debugger.ExecutionFailed += (record, ex) => Console.WriteLine($"[FAIL] {record.PluginShortName}: {ex.Message}");
debugger.StartWithConsoleUI();| Event | Fired when |
|---|---|
ExecutionStarted |
A new plugin execution context is received from the relay. |
OrgServiceCallStarted |
The local plugin issues an IOrganizationService request. |
OrgServiceCallCompleted |
The response from Dataverse is returned to the local plugin. |
ExecutionCompleted |
The local execution finishes successfully and the updated context is sent back. |
ExecutionFailed |
The local execution throws an unhandled exception. |
The redirect logic is compiled out when the DISABLE_REMOTE_DEBUG preprocessor symbol is defined. Add it to your plugin project's release configuration to ensure zero overhead in production:
<!-- In your plugin .csproj -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DefineConstants>DISABLE_REMOTE_DEBUG</DefineConstants>
</PropertyGroup>When the symbol is defined, SendToRemoteDebugger always returns false and no DebugSession lookup is performed.
Inside Plugin.Execute, just before the matching step methods are dispatched, the framework calls SendToRemoteDebugger:
- If the context is already a debug context (i.e. it came from the remote debugger itself), the redirect is skipped.
- The framework queries for an active
DebugSessionwhoseDebugeeIdmatches the initiating or root user. - If a valid session is found, the full
RemoteDebugExecutionContextis serialised and sent over the Hybrid Connection. - The relay blocks, forwarding every
IOrganizationServicerequest/response in both directions until the local plugin returns its updated context (or raises an exception). - The returned context is merged back into the Dataverse execution context, and the plugin returns normally.
- If the relay endpoint is unreachable (e.g. the local runner is not started), an
HttpRequestExceptionis caught silently and the plugin executes normally in Dataverse.
| Symptom | Likely cause | Fix |
|---|---|---|
Plugin executes normally despite an active DebugSession |
The runner is not started, or the Hybrid Connection URL / SAS key is wrong | Start the runner first; verify the relay settings in the DebugSession record |
InvalidPluginExecutionException: No type found |
The plugin type could not be resolved locally | Ensure the plugin assembly is loaded by the runner (add a project reference or load it explicitly) |
| Session expired immediately | SessionEnd set to a past date |
Update the SessionEnd field to a future date/time |
TUI shows execution as failed with HttpRequestException |
Runner stopped while execution was in progress | Restart the runner; the plugin will execute normally in Dataverse for that invocation |
See also: Plugins · Custom APIs