A CLI for deploying code-first Dataverse / Power Platform artifacts from source or CI/CD. Commands are grouped by artifact type:
dvx plugin …— decorate plugin classes with[PluginStep]and let dvx build the package, upload it to Dataverse, and keep SDK Message Processing Step records in sync with your code.dvx webresource …— upsert and publish JS/CSS/HTML and other web resources from a folder and/or a manifest, optionally pruning ones that have been removed from source.dvx config …— scaffold thedvx.jsonconfiguration file.
- Requirements
- Installation
- Quick start
- Configuration
- Decorating plugins with attributes
- Commands
- Web resources
- Adopting an existing project
- How step registration works
- Pre- and post-images
- Exit codes
- Dataverse tables used
- Project structure
| Requirement | Notes |
|---|---|
| .NET 8 SDK | Runtime for dvx itself |
| Dataverse service principal | ClientId + ClientSecret with the Dynamics CRM System Administrator or other role with privileges allowing plugin / web-resource deployment |
dotnet tool install dvx.cli --global --add-source <your-feed-url>
cd src/dvx
dotnet pack
dotnet tool install dvx.cli --global --source ./bin/Debug dvx
Verify the install:
dvx --version
1. Generate a config file in your plugin repo root and fill in the values:
dvx config create
This creates dvx.json in the current directory:
{
"defaultEnvironment": "dev",
"environments": [
{
"name": "dev",
"url": "https://your-org.crm4.dynamics.com",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"clientSecret": "your-secret"
}
],
"publisherPrefix": "yourprefix"
}2. Reference the attributes package in your plugin project:
<PackageReference Include="dvx.PluginAttributes" Version="1.0.0" />3. Decorate a plugin class:
using dvx.PluginAttributes;
[PluginStep("account", "Create", Stage.PostOperation)]
public class AccountOnPostCreate : IPlugin
{
public void Execute(IServiceProvider serviceProvider) { ... }
}4. Build, deploy, and register in one command (no --env needed when defaultEnvironment is set):
dvx plugin sync --project ./src/MyPlugin/MyPlugin.csproj
Pass all connection details as options or environment variables. No config file required:
# Via environment variables (recommended for secrets)
export DVX_URL=https://your-org.crm4.dynamics.com
export DVX_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export DVX_CLIENT_SECRET=${{ secrets.DATAVERSE_SECRET }}
dvx plugin sync --publisher-prefix yourprefix --project ./src/MyPlugin/MyPlugin.csprojOr pass everything directly as CLI options:
dvx plugin sync \
--url https://your-org.crm4.dynamics.com \
--client-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
--client-secret <secret> \
--publisher-prefix yourprefix \
--project ./src/MyPlugin/MyPlugin.csproj
When --config is not specified, dvx looks for dvx.json starting in the current working
directory and walking up through parent directories to the filesystem root — like git discovering
.git — so commands work from anywhere inside a project. The config is project-local and safe to
check in (omit clientSecret; use DVX_CLIENT_SECRET instead).
Relative paths inside the config file (project, webResources.folder,
webResources.manifest) resolve against the config file's directory — not the directory you run
dvx from — so they keep working from any subdirectory. Paths passed on the command line
(--project, --folder, --manifest, --config) resolve against the current directory, as
usual for CLI tools.
If no file is found, connection details must be supplied entirely via CLI options or environment variables.
{
"defaultEnvironment": "dev",
"environments": [
{
"name": "dev",
"url": "https://your-dev-org.crm4.dynamics.com",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"clientSecret": "your-secret"
},
{
"name": "uat",
"url": "https://your-uat-org.crm4.dynamics.com",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"clientSecret": "your-uat-secret"
}
],
"publisherPrefix": "yourprefix",
"solutionUniqueName": "MySolution",
"webResources": {
"folder": "./WebResources",
"manifest": "./webresources.json",
"publish": true
}
}| Field | Required | Description |
|---|---|---|
defaultEnvironment |
Name of the environment to use when --env is not passed on the command line |
|
name |
✓ (per env) | Environment alias used with --env |
url |
✓ (per env) | Dataverse org URL |
clientId |
✓ (per env) | App registration client ID |
clientSecret |
✓ (per env) | App registration secret |
publisherPrefix |
when no solution given | Dataverse publisher customization prefix (e.g. "pub"). Used to form the pluginpackage unique name ({prefix}_{assemblyName}) and to prefix folder-derived web-resource names. Fallback only — when a solution is provided, its publisher's prefix is used instead (and this value, if also set, is ignored with a warning). Can be supplied via --publisher-prefix. |
solutionUniqueName |
Unique name of the Dataverse solution to add deployed components (plugin steps / web resources) to. Authoritative for the customization prefix: when set, the prefix is read from this solution's publisher rather than publisherPrefix. Can be overridden per-command with --solution-unique-name. |
|
webResources |
Defaults for webresource sync: folder, manifest, and publish (default true). See Web resources. |
For every connection value, dvx resolves in this priority order (highest wins):
| Value | CLI option | Environment variable | Config file |
|---|---|---|---|
| Environment URL | --url |
DVX_URL |
named env entry |
| Client ID | --client-id |
DVX_CLIENT_ID |
named env entry |
| Client Secret | --client-secret |
DVX_CLIENT_SECRET |
named env entry |
This lets you keep non-secret values in dvx.json and inject secrets at runtime:
{
"defaultEnvironment": "dev",
"environments": [
{
"name": "dev",
"url": "https://your-org.crm4.dynamics.com",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
],
"publisherPrefix": "yourprefix"
}export DVX_CLIENT_SECRET=my-secret
dvx plugin sync --project ./src/MyPlugin/MyPlugin.csprojAdd the dvx.PluginAttributes NuGet package to your plugin project and apply
[PluginStep] to each plugin class.
[PluginStep("account", "Create", Stage.PostOperation)]
public class AccountOnPostCreate : IPlugin { ... }[PluginStep(
entity: "account",
message: "Update",
stage: Stage.PreOperation,
// Optional ↓
ExecutionOrder = 1, // rank / execution order within the stage. Default: 1
Async = false, // true = async (background) step. Default: false (synchronous)
Description = "...", // description stored on the step record
RunAsSystem = false, // true = run as the Dataverse system user. Default: false
// Filtering attributes — step only fires when one of these fields changes (Update only)
FilteringAttributes = new[] { "name", "statuscode", "telephone1" },
// Unsecure configuration string passed to the plugin constructor
Configuration = "unsecure-config",
// Pre-image — snapshot of the record BEFORE the operation
UsePreImage = true,
PreImageAttributes = new[] { "name", "address1_city" }, // empty = all attributes
PreImageAlias = "PreImage", // alias to read it by in code; default "PreImage"
// Post-image — snapshot of the record AFTER the operation (PostOperation only)
UsePostImage = true,
PostImageAttributes = new[] { "name" },
PostImageAlias = "PostImage" // alias to read it by in code; default "PostImage"
)]
public class AccountOnPreUpdate : IPlugin { ... }| Value | Integer | When it fires |
|---|---|---|
Stage.PreValidation |
10 | Before the core operation, outside the database transaction |
Stage.PreOperation |
20 | Before the core operation, inside the database transaction |
Stage.PostOperation |
40 | After the core operation, inside the database transaction |
Apply [PluginStep] more than once to register the same class for multiple messages or entities:
[PluginStep("account", "Create", Stage.PostOperation)]
[PluginStep("account", "Update", Stage.PostOperation, FilteringAttributes = new[] { "name" })]
public class AccountOnCreateOrUpdate : IPlugin { ... }Each attribute instance creates one independent sdkmessageprocessingstep record.
When images are registered, access them in the plugin via the execution context.
dvx always uses the aliases PreImage and PostImage:
var preImage = context.PreEntityImages["PreImage"]; // UsePreImage = true
var postImage = context.PostEntityImages["PostImage"]; // UsePostImage = trueCommands are grouped by artifact:
dvx plugin …for plugin assemblies,dvx webresource …for web resources, anddvx config …for configuration. Connection options (--env/--url/--client-id/--client-secret),--config,--dry-run, and--verboseare shared across all commands.
Build, deploy, and register steps in a single operation. This is the plugin command you'll use most.
dvx plugin sync --project <path> [options]
| Option | Required | Default | Description |
|---|---|---|---|
--project |
✓ | Path to the plugin .csproj file |
|
--publisher-prefix |
✓ | from config | Dataverse publisher prefix (e.g. solu). Falls back to publisherPrefix in config |
--env |
from config | Environment name from config. Not needed when defaultEnvironment is set or when connection options are provided directly |
|
--url |
env var / config | Dataverse environment URL | |
--client-id |
env var / config | Service principal client ID | |
--client-secret |
env var / config | Service principal client secret | |
--solution-unique-name |
from config | Add all registered steps to this Dataverse solution | |
--dry-run |
Print what would change without writing to Dataverse | ||
--config |
auto-discovered | Path to config file | |
--verbose |
Log upload details + inner exception details on error |
What it does:
- Runs
dotnet buildon the.csprojto produce a.nupkgand.dll - Looks up the existing
pluginpackagerecord byuniquename({prefix}_{assemblyName}) - Uploads the new
.nupkgby updating thepluginpackagecontentcolumn via the Dataverse SDK - Queries the child
pluginassemblyrecord for the assembly ID - Reflects the
.dllfor[PluginStep]attributes - Fully syncs
sdkmessageprocessingsteprecords — creates new steps, updates changed steps, deletes orphan steps - Syncs
sdkmessageprocessingstepimagerecords (pre/post images) for each step
Note:
syncanddeployonly support updating an existing plugin package. For the very first upload, register the package once with the Plugin Registration Tool. After that, dvx handles all subsequent updates itself using the Dataverse SDK.
Examples:
# Developer local — defaultEnvironment in dvx.json, secret from env var
dvx plugin sync --project ./src/MyPlugin/MyPlugin.csproj
# Explicit environment
dvx plugin sync --env uat --project ./src/MyPlugin/MyPlugin.csproj
# CI/CD pipeline — no config file
dvx plugin sync \
--url https://your-org.crm4.dynamics.com \
--client-id $CLIENT_ID \
--client-secret $CLIENT_SECRET \
--publisher-prefix yourprefix \
--project ./src/MyPlugin/MyPlugin.csproj
# Dry-run (reads Dataverse but writes nothing)
dvx plugin sync --project ./src/MyPlugin/MyPlugin.csproj --dry-runBuild the project and push the plugin package to Dataverse. Does not touch step registrations.
dvx plugin deploy --project <path> [options]
| Option | Required | Default | Description |
|---|---|---|---|
--project |
✓ | Path to the plugin .csproj file |
|
--publisher-prefix |
✓ | from config | Dataverse publisher prefix. Falls back to publisherPrefix in config |
--env |
from config | Environment name from config | |
--url |
env var / config | Dataverse environment URL | |
--client-id |
env var / config | Service principal client ID | |
--client-secret |
env var / config | Service principal client secret | |
--config |
auto-discovered | Path to config file | |
--verbose |
Log upload details + inner exception details on error |
Use deploy when you want to push a new package version without changing step registrations,
or when step registrations are managed separately.
Example:
dvx plugin deploy --env uat --project ./src/MyPlugin/MyPlugin.csproj
Reflect an already-deployed assembly for
[PluginStep]attributes and sync step registrations. Does not build or re-upload the assembly.
dvx plugin register (--project <path> | --assembly-name <name>) [options]
| Option | Required | Description |
|---|---|---|
--project |
one of | Build the project, extract the DLL locally, use that for reflection |
--assembly-name |
one of | Download the DLL bytes from pluginassembly.content in Dataverse for reflection |
--env |
Environment name from config | |
--url |
Dataverse environment URL | |
--client-id |
Service principal client ID | |
--client-secret |
Service principal client secret | |
--solution-unique-name |
Add all registered steps to this Dataverse solution. Falls back to solutionUniqueName in config |
|
--dry-run |
Print what would change without writing to Dataverse | |
--verbose |
Log solution validation and step-assignment details | |
--config |
Path to config file |
--project and --assembly-name are mutually exclusive.
Using --project (recommended — reflects the exact DLL you last built):
dvx plugin register --env dev --project ./src/MyPlugin/MyPlugin.csproj
Using --assembly-name (reflects the DLL currently stored in Dataverse):
dvx plugin register --env dev --assembly-name MyPlugin
Note:
--assembly-nameonly works when the assembly was deployed withsourcetype = Database(the default for all dvx plugin deployments). If the assembly has no content bytes stored, use--projectinstead.
Onboard an existing project. Read the steps already registered on an assembly in Dataverse and write matching
[PluginStep]attributes into your source. A one-time bootstrap — afterwards usesync/register.
dvx plugin adopt --project <path> [options]
| Option | Required | Description |
|---|---|---|
--project |
✓ | Path to the plugin .csproj. Its source files are edited in place. Falls back to config, then a single .csproj in the CWD |
--assembly-name |
Dataverse pluginassembly name. Defaults to the project's assembly name |
|
--env |
Environment name from config | |
--url / --client-id / --client-secret |
Connection values, resolved like every other command | |
--config |
Path to config file | |
--dry-run |
Print the attributes that would be written without modifying any files | |
--verbose |
Log per-file and per-step details |
What it does:
- Resolves the
pluginassemblyin Dataverse by name. - Reads every step on that assembly's plugin types, plus their images and unsecure configuration.
- Matches each step to its class by fully-qualified type name and inserts a
[PluginStep(...)]attribute (addingusing dvx.PluginAttributes;where needed). - Skips classes that already carry an equivalent attribute, and reports any Dataverse step whose class it could not find in the project.
- Detects Custom API registrations (their Main Operation steps, and steps on Custom API
messages) and, instead of writing
[PluginStep], marks the implementing class with[CustomApi]sosync/registerskip it. See Custom API implementations.
adoptnever writes to Dataverse — it only edits source files. Review the result withgit diff, then rundvx plugin syncto bring Dataverse under attribute control.
Upsert and publish Dataverse web resources from a folder and/or a manifest. Upsert-only by default — nothing is deleted unless you pass
--delete-orphaned. Alias:dvx wr sync.
dvx webresource sync [options]
| Option | Required | Default | Description |
|---|---|---|---|
--folder |
one of folder/manifest | from config | Folder to auto-upsert from, recursively. Each file's name is derived as {prefix}_/{relativePath}. Falls back to webResources.folder |
--manifest |
one of folder/manifest | from config | Manifest JSON of explicit { dataverseName, localPath, displayName, type } entries. Falls back to webResources.manifest |
--publisher-prefix |
folder mode (unless a solution is set) | from config | Publisher customization prefix for folder-derived names. Falls back to publisherPrefix in config. Ignored when a solution is provided — the solution's publisher prefix wins |
--solution-unique-name |
for --delete-orphaned |
from config | Add upserted resources to this solution; the scope for orphan deletion; and, when set, the source of the publisher prefix for folder-derived names |
--delete-orphaned |
off | Delete web resources in the solution that are no longer in source. Requires a solution. Destructive — run with --dry-run first |
|
--no-publish |
publish on | Skip the publish step after upsert | |
--env / --url / --client-id / --client-secret |
env var / config | Connection values, resolved like every other command | |
--dry-run |
Print what would change without writing to Dataverse | ||
--config |
auto-discovered | Path to config file | |
--verbose |
Per-resource tracing: resolved inputs, scan results, query hit/miss, diff outcome, solution add, orphan deletes, publish set |
What it does:
- Builds the desired set from the folder (recursive scan) and/or the manifest.
- For each resource: reads the local file, queries Dataverse by
name, and content-diffs (line-ending-normalized for text types; byte-for-byte for binary). Unchanged → skipped; changed → updated; missing → created. - When a solution is set, adds each created/updated resource to it.
- With
--delete-orphaned, deletes resources that are in the solution but not in source. - Publishes all created/updated resources in one
PublishXmlcall (unless--no-publish).
See Web resources for folder layout, naming, and type inference.
Examples:
# Folder + everything from dvx.json (webResources.folder, solutionUniqueName / publisherPrefix)
dvx webresource sync --env dev
# Explicit folder, preview only
dvx webresource sync --env dev --folder ./WebResources --publisher-prefix pub --dry-run --verbose
# Manifest of explicit mappings
dvx webresource sync --env dev --manifest ./webresources.json
# Prune resources removed from source (scoped to the solution)
dvx webresource sync --env dev --solution-unique-name MySolution --delete-orphanedWrite a template
dvx.jsonto the current directory (or--location).
dvx config create [--location <dir>] [--overwrite]
| Option | Default | Description |
|---|---|---|
--location |
current dir | Directory to create dvx.json in |
--overwrite |
Overwrite dvx.json if it already exists |
dvx webresource sync deploys web resources (JavaScript, CSS, HTML, images, …) from your repo to
Dataverse. It content-diffs every resource and only writes what changed, so re-running it is cheap
and idempotent. The source can be a folder (auto-discovered), a manifest (explicit
mappings), or both.
Point dvx at a folder and it upserts every file with a recognized extension, recursively. The
Dataverse name of each resource is derived from its path (forward slashes):
name = {prefix}_/{relativePath}
With pub as the prefix (see below for where it comes from):
WebResources/
account/main.js -> pub_/account/main.js
shared/util.css -> pub_/shared/util.css
The prefix is always the publisher's customization prefix. When a solution is provided
(--solution-unique-name or solutionUniqueName), dvx reads the prefix from that solution's
publisher; otherwise it uses --publisher-prefix / publisherPrefix. If both are supplied the
solution wins and dvx warns that the configured prefix is ignored.
Files with unrecognized extensions are skipped (and listed under --verbose). Dotfiles,
*.map source maps, and node_modules / bin / obj / .git directories are ignored.
dvx webresource sync --env dev --folder ./WebResources --name-prefix pubFor explicit control (custom names, display names, non-derivable types), pass a manifest JSON — an
array of entries matching the legacy Sync-WebResources.ps1 shape:
[
{
"dataverseName": "pub_/account/main.js",
"localPath": "./WebResources/account/main.js",
"displayName": "Account main script",
"type": 3
},
{
"dataverseName": "pub_/shared/logo.png",
"localPath": "./WebResources/shared/logo.png"
}
]| Field | Required | Description |
|---|---|---|
dataverseName |
✓ | The web resource name in Dataverse |
localPath |
✓ | File to read content from. Relative paths resolve against the manifest file's directory |
displayName |
Display name. Defaults to dataverseName |
|
type |
webresourcetype value. Inferred from the file extension when omitted |
dvx webresource sync --env dev --manifest ./webresources.jsonIf both a folder and a manifest resolve, the union is processed; a manifest entry overrides a folder-derived one with the same name.
The type is inferred from the file extension (override per-entry with type in a manifest):
| Extension | Type | Extension | Type | |
|---|---|---|---|---|
.htm / .html |
1 | .gif |
7 | |
.css |
2 | .xap |
8 | |
.js |
3 | .xsl / .xslt |
9 | |
.xml |
4 | .ico |
10 | |
.png |
5 | .svg |
11 | |
.jpg / .jpeg |
6 | .resx |
12 |
Text types (HTML, CSS, JS, XML, XSL, SVG, RESX) are compared with line endings normalized, so a pure CRLF↔LF difference counts as no change. Other types are compared byte-for-byte.
After upserting, dvx publishes all created/updated resources in a single PublishXml request, so
the changes go live without a manual publish. Pass --no-publish (or "publish": false in config)
to skip it.
When a solution is set (--solution-unique-name or solutionUniqueName in config), each
created/updated resource is added to that solution (idempotent).
By default nothing is deleted. Pass --delete-orphaned to remove web resources that are in the
target solution but no longer present in your folder/manifest. Because the deletion scope is the
solution, --delete-orphaned requires a solution. Always preview with --dry-run first:
dvx webresource sync --env dev --solution-unique-name MySolution --delete-orphaned --dry-run
dvx webresource sync --env dev --solution-unique-name MySolution --delete-orphanedSet defaults under webResources in dvx.json so the command needs no extra arguments:
{
"defaultEnvironment": "dev",
"publisherPrefix": "pub",
"solutionUniqueName": "MySolution",
"environments": [ /* … */ ],
"webResources": {
"folder": "./WebResources",
"publish": true
}
}# Everything resolved from config:
dvx webresource sync --env dev| Field | Description |
|---|---|
folder |
Default folder to upsert from |
manifest |
Default manifest path |
publish |
Publish after upsert. Default true |
Already have plugins registered in Dataverse but no [PluginStep] attributes in code? adopt
scaffolds them for you.
1. Reference the attributes package and supply connection details (see Quick start).
2. Preview the attributes that would be written:
dvx plugin adopt --env dev --project ./src/MyPlugin/MyPlugin.csproj --dry-run
3. Write them and review the diff:
dvx plugin adopt --env dev --project ./src/MyPlugin/MyPlugin.csproj
git diff
4. Sync. The first sync adopts the existing steps in place (matching by entity + message + stage + sync/async), so there is no churn — expect updates only:
dvx plugin sync --env dev --project ./src/MyPlugin/MyPlugin.csproj --dry-run
dvx plugin sync --env dev --project ./src/MyPlugin/MyPlugin.csproj
Things to review after adoption:
- Steps whose class can't be found in the project are reported and skipped — manage those by hand.
supporteddeploymentother than Server Only is not represented by[PluginStep].
dvx writes every step with a name in the format:
Namespace.ClassName | entity | message | StageName | sync|async
For example:
MyPlugin.AccountOnPostCreate | account | create | PostOperation | sync
dvx reconciles steps per plugin assembly: it only looks at steps registered on the
plugin types contained in the assembly it just deployed. Steps belonging to other assemblies are
never touched. Within the assembly, each [PluginStep] is matched to an existing step first by
this exact name, then — for steps you registered by hand or with another tool — by identity
(entity + message + stage + sync/async). A match found by identity is adopted in place
(updated and renamed to the convention above) rather than deleted and recreated. See
Adopting an existing project.
Each time register or sync runs, dvx performs a full sync for the target assembly:
| Scenario | What happens |
|---|---|
[PluginStep] attribute exists, no matching step in Dataverse |
Step created |
[PluginStep] attribute exists, matching step (by name) in Dataverse |
Step updated to match attribute values |
[PluginStep] attribute exists, hand-registered step with matching identity |
Step adopted — updated and renamed in place |
| Step on one of this assembly's plugin types with no matching attribute | Step deleted (orphan) |
[PluginStep] attribute removed from code |
Step deleted on next sync |
| Plugin class deleted from code | Step deleted on next sync |
| Step on a plugin type in a different assembly | Untouched |
Adoption note: because any step on this assembly's plugin types with no matching attribute is treated as an orphan and deleted, decorate all existing steps before your first
sync. Theplugin adoptcommand does this for you.
If dvx finds a class that implements IPlugin but has no [PluginStep] attribute, it
logs a warning and skips that class. All other steps are still processed.
Custom APIs are not event plugins — their code runs at the Main Operation stage, bound through
the customapi record rather than an SDK message processing step. Mark a Custom API's
implementation class with [CustomApi] and dvx's discovery skips it silently (no
"missing [PluginStep]" warning), so sync / register never try to manage it as a step:
using dvx.PluginAttributes;
[CustomApi]
public class MyCustomApi : IPlugin { ... }adopt applies this for you: any Custom API step it finds in Dataverse is skipped and its class is
marked [CustomApi] instead of [PluginStep].
When --solution-unique-name (or solutionUniqueName in config) is set, dvx adds each
created or updated step to that solution after writing it to Dataverse. Steps that already belong
to the solution are unaffected (the operation is idempotent). If the solution does not exist,
dvx exits with an error before registering any steps.
Before creating an entity-specific step, dvx checks that Dataverse has an
sdkmessagefilter record for the given entity + message combination. If none exists (e.g. you
specify an entity that doesn't support that message), the step is skipped with a warning
rather than causing an error.
Some messages — such as Associate and Disassociate — are not tied to a specific entity and
have no sdkmessagefilter. Omit Entity on [PluginStep] for these, and dvx plugin registers the
step with no filter (so it fires for the message regardless of entity):
[PluginStep("", "Associate", Stage.PostOperation)]
public class AssociationPlugin : IPlugin { ... }Enable images by setting UsePreImage = true and/or UsePostImage = true on [PluginStep].
[PluginStep("contact", "Update", Stage.PostOperation,
FilteringAttributes = new[] { "firstname", "lastname" },
UsePreImage = true,
PreImageAttributes = new[] { "firstname", "lastname", "emailaddress1" },
UsePostImage = true)]
public class ContactOnPostUpdate : IPlugin { ... }| Property | Default | Description |
|---|---|---|
UsePreImage |
false |
Register a pre-image snapshot on this step |
PreImageAttributes |
[] (all) |
Fields to include in the pre-image. Empty = include all |
PreImageAlias |
"PreImage" |
Entity alias to read the pre-image by |
UsePostImage |
false |
Register a post-image snapshot on this step |
PostImageAttributes |
[] (all) |
Fields to include in the post-image. Empty = include all |
PostImageAlias |
"PostImage" |
Entity alias to read the post-image by |
Constraints:
- Post-images are only valid on
Stage.PostOperation. IfUsePostImage = trueon any other stage, dvx logs a warning and skips the image. - The alias defaults to
"PreImage"/"PostImage", but can be overridden withPreImageAlias/PostImageAlias. Reference the image by whichever alias you set:context.PreEntityImages["PreImage"] // or your custom PreImageAlias context.PostEntityImages["PostImage"] // or your custom PostImageAlias
Plugins can receive an unsecure configuration string at runtime via their constructor. Set it on
[PluginStep]:
[PluginStep("account", "Create", Stage.PostOperation,
Configuration = "<settings>...</settings>")]
public class ConfiguredPlugin : IPlugin { ... }| Property | Maps to | Description |
|---|---|---|
Configuration |
sdkmessageprocessingstep.configuration |
Unsecure config — readable by anyone who can view the step |
It maps to the plugin constructor's first parameter:
public ConfiguredPlugin(string unsecureConfiguration, string secureConfiguration) { ... }Notes:
- Optional. When omitted,
configurationis cleared on the step. - dvx does not manage secure configuration (
sdkmessageprocessingstepsecureconfig). Secure config is environment-specific and not solution-aware, so manage it out of band (e.g. with the Plugin Registration Tool) per environment.
| Code | Meaning |
|---|---|
0 |
Success — all operations completed |
1 |
Fatal error — config missing, auth failed, assembly not found, etc. |
2 |
Partial failure — one or more steps failed but others succeeded |
dvx reads and writes the following Dataverse tables:
| Table (logical name) | Purpose |
|---|---|
pluginpackage |
Stores the plugin package (nupkg) in its content column. Queried by uniquename, then updated with the new .nupkg content on deploy. |
pluginassembly |
Child record created by Dataverse when it processes a plugin package. Queried after deploy to get the ID for step registration. Also queried by --assembly-name to download content bytes. |
plugintype |
One record per plugin class. Queried to resolve class names to GUIDs for step registration. |
customapi |
Queried by adopt to identify Custom API registrations (by plugintypeid / sdkmessageid) so their steps are skipped rather than scaffolded as [PluginStep]. |
sdkmessage |
Lookup table for message names (Create, Update, Delete, …). Loaded once and cached per run. |
sdkmessagefilter |
Associates messages with entity types and indicates whether custom steps are allowed. |
sdkmessageprocessingstep |
The step registration itself. Created, updated, and deleted by dvx. |
sdkmessageprocessingstepimage |
Pre- and post-image registrations attached to a step. |
webresource |
Web resource records. Queried by name, created/updated/deleted, and published by webresource sync. |
solutioncomponent |
Queried (joined to webresource) to find web resources in a solution for --delete-orphaned. |
solution |
Queried by unique name to validate the target solution exists, and to add steps / web resources to it. |
PluginRegistrationTool/
├── src/
│ ├── dvx.PluginAttributes/ # netstandard2.0 NuGet package
│ │ ├── PluginStepAttribute.cs # [PluginStep] attribute with all config
│ │ ├── CustomApiAttribute.cs # [CustomApi] marker — excludes Custom API impls from discovery
│ │ ├── Stage.cs # PreValidation / PreOperation / PostOperation
│ │ └── dvx.PluginAttributes.csproj
│ ├── dvx/ # net8 CLI tool
│ │ ├── Commands/
│ │ │ ├── DeployCommand.cs # dvx plugin deploy
│ │ │ ├── RegisterCommand.cs # dvx plugin register
│ │ │ ├── SyncCommand.cs # dvx plugin sync
│ │ │ ├── AdoptCommand.cs # dvx plugin adopt
│ │ │ ├── WebResourceSyncCommand.cs # dvx webresource sync
│ │ │ ├── CreateConfigCommand.cs # dvx config create
│ │ │ └── Shared/CommandOptions.cs # Shared option definitions
│ │ ├── Config/
│ │ │ └── ConfigLoader.cs # Config discovery, env resolution, prefix resolution
│ │ ├── Models/
│ │ │ ├── AppConfig.cs # Root config model (+ WebResourceConfig)
│ │ │ ├── EnvironmentConfig.cs # Per-environment connection details
│ │ │ ├── WebResourceConfig.cs # webResources config section
│ │ │ ├── WebResourceDefinition.cs # Resolved web resource to upsert
│ │ │ ├── WebResourceManifestEntry.cs# One manifest JSON entry
│ │ │ ├── PluginStepDefinition.cs # Resolved step (from reflection or Dataverse)
│ │ │ ├── ImageDefinition.cs # Pre/post image definition
│ │ │ ├── SyncResult.cs # Created / updated / deleted / skipped / published counters
│ │ │ ├── ImportResult.cs # adopt: imported definitions + warnings
│ │ │ └── AttributeWriteResult.cs # adopt: attributes added / skipped / unmatched
│ │ ├── Output/
│ │ │ └── Out.cs # Console output helper
│ │ ├── Services/
│ │ │ ├── DataverseClientFactory.cs # Constructs ServiceClient from EnvironmentConfig
│ │ │ ├── ProjectBuilder.cs # Runs dotnet build → BuildResult(NupkgPath, DllPath)
│ │ │ ├── PackageDeployer.cs # Looks up pluginpackage ID, uploads .nupkg via SDK content update
│ │ │ ├── AssemblyDownloader.cs # Downloads DLL bytes from Dataverse content field
│ │ │ ├── PluginDiscovery.cs # MetadataLoadContext reflection → step definitions
│ │ │ ├── SdkMetadata.cs # Shared message/filter/plugintype lookups (both directions)
│ │ │ ├── StepImporter.cs # adopt: Dataverse steps → step definitions (reverse of StepRegistrar)
│ │ │ ├── AttributeWriter.cs # adopt: writes [PluginStep] attributes into source via Roslyn
│ │ │ ├── SolutionService.cs # Solution add (steps/web resources); solution web-resource query
│ │ │ ├── StepRegistrar.cs # Full sync: upsert steps + adopt/delete orphans + images
│ │ │ ├── WebResourceTypes.cs # Extension → webresourcetype + text/binary classification
│ │ │ ├── WebResourceFolderScanner.cs# Folder walk → web resource definitions ({prefix}_/path)
│ │ │ └── WebResourceSyncer.cs # Upsert + content-diff + solution add + orphan delete + publish
│ │ ├── Program.cs
│ │ └── dvx.csproj
│ └── dvx.Tests/ # xUnit test project
└── dvx.sln