dversedev

For Microsoft Dataverse & Dynamics 365

Close the Plugin
Registration Tool.

dverse keeps plugin registration where the rest of your logic already lives — in the repository, next to the method it belongs to. Run dverse apply and the environment matches your code. Run dverse plan and it tells you, field by field, exactly what doesn't.

$ dotnet tool install --global Dverse.Cli --prerelease

.NET 9 or later · Windows, macOS, Linux · Dataverse online · pre-release, in active development

AccountPlugins.cs — the whole plugin
public partial class AccountPlugins
{
    [Step<Account>(Msg.Update, Stage.PreOperation,
        Filtering = new[] { Account.Cols.CreditLimit })]
    public void BlockCreditLimitDowngrade(IStepContext<Account> ctx)
    {
        if (ctx.Target.CreditLimit < ctx.PreImage.CreditLimit)
            throw ctx.Fail("Credit limits can't be lowered.");
    }
}

What happens to those eight lines

You write the rule.
dverse writes the plumbing.

The IPlugin class

The entry class and the entire context runtime are emitted as internal source into your assembly. No base class to inherit, no runtime DLL to ship, no ILMerge step. Dverse.Sdk is the only package your plugin project references, and its attributes are stripped from the IL.

A pre-image of exactly creditlimit

Inferred from the one thing the code touches: ctx.PreImage.CreditLimit. When the analyser can't prove which columns you read, you get a compile error and an explicit [Image] to write — never a silent "all columns" that quietly costs you throughput.

The registration, as data

Message, stage, mode, rank, filtering and images land in a manifest beside the assembly. That manifest is what gets diffed — so changing a rank never touches the DLL, and re-uploading an unchanged assembly never happens at all.

Types that come from your org

Account, Account.Cols.CreditLimit and the option-set enums are generated in memory from a committed metadata snapshot. Nothing lands in your source tree, and a schema change breaks the build instead of production.

plan · apply

Every deploy starts
as a diff.

dverse reads the environment in a constant number of queries, canonicalises both sides, and PATCHes only the columns that actually differ. Which means the interesting run is the second one: it writes nothing, and it says so.

dverse plan
$ dverse plan

Plan for Contoso.Plugins 1.0.0.7 → 'dev' (https://contoso.crm4.dynamics.com), solution contoso

  · plugin assembly
      ~ pluginassembly Contoso.Plugins (content changed)
  · steps and images
      + step accountplugins-blockcreditlimitdowngrade-update-20 (account Update, stage 20, sync, rank 1, filter [creditlimit])
          + image PreImage [creditlimit]
      ~ step accountplugins-flaglargedeals-update-40
          rank: 1 → 20
      - step contactplugins-legacyaudit-create-40

Plan: 1 to add, 2 to change, 1 to delete, 14 unchanged.
apply, then plan again
$ dverse apply

  ✓ plugin assembly — 1 write, 2.1s
  ✓ steps and images — 4 writes, 0.9s

$ dverse plan

Plan for Contoso.Plugins 1.0.0.7 → 'dev' (https://contoso.crm4.dynamics.com), solution contoso

Plan: 0 to add, 0 to change, 0 to delete, 17 unchanged.

$ echo $?
0

0

writes on a no-op deploy — the idempotency contract, covered by tests

~5

read round trips to diff a whole assembly, flat in step count

2

the exit code when changes are pending — so CI can fail on drift

42

compile-time diagnostics that catch bad registrations before they ship

Ambiguity is never resolved by guessing. Steps match on an explicit Id, then Key, then the natural key (plugin type, message, entity, stage) when it is unique on both sides, then name. Anything genuinely ambiguous is a hard error.

Where this fits in your ALM

dverse converges the environment you develop in — your sandbox — from the repository. That is the environment you point it at, and usually the only one you ever configure. Shipping onward stays what it already is: export a managed solution from the sandbox and import it downstream. dverse doesn't try to replace Power Platform ALM, and it never touches components it doesn't manage.

Capabilities

One tool for the
whole solution.

Plugins are where it started. Everything else in a Dataverse solution that people still move by hand is declared the same way — and diffed the same way.

WhatHow you declare it
Plugin steps [Step<Account>(Msg.Update, Stage.PreOperation)] on a method taking IStepContext<Account>.
Images Inferred from ctx.PreImage.X / ctx.PostImage.X; explicit [Image] is the escape hatch.
Early-bound models Entities listed once in dataverse.yaml; classes, Cols constants and option-set enums generated in memory.
Custom APIs [CustomApi] on a method with record request/response types. Parameters sync; typed callers are generated.
Computed columns [Derive(Account.Cols.Description)] on a pure static method — compiles to dependency-ordered steps with inferred filtering.
Real-time rollups [Rollup] over Children<Contact>; the aggregate becomes one server-side FetchXML query per affected parent.
Command-bar buttons [Command] next to a [CustomApi] — the button, its JavaScript and its registration are generated.
Ribbons & modern commands Committed RibbonDiffXml diffed per element, and appaction rows with a real field-level diff.
Web resources & PCF A path in dataverse.yaml: built, content-hash diffed, published in a single PublishXml.
Schema, data, forms & views Tables, columns, relationships, seed rows, formxml and saved queries — committed, converged, published once.
Code signing A signing: entry Authenticode-signs each assembly in-process — cross-platform, no signtool, deterministic.
Managed identity A managedIdentity: entry binds the row, so plugins reach Azure with no stored credentials.

Coming from spkl or the PRT

What actually
changes.

spkl already made the case that registration belongs in the repository, and it was right. dverse takes the next step: a real diff instead of a push, generated boilerplate instead of written boilerplate, and errors at compile time instead of at runtime.

Task Before With dverse
Register a step Click through a Windows GUI, and again in every sandbox. An attribute on the handler, and dverse apply.
See what's registered Open the tool and read it off the screen. dverse plan — or the diff in the pull request.
Pre-image columns Typed into a dialog, free to drift from the code that reads them. Inferred from the code that reads them; unprovable cases are compile errors.
Early-bound classes CrmSvcUtil output committed as thousands of lines. Generated in memory from a committed metadata snapshot.
Plugin boilerplate An IPlugin class per handler, casting InputParameters. A method taking IStepContext<Account>.
Environment drift Discovered in production. plan exits 2 and the build fails.
Redeploying nothing Re-uploads the assembly, rewrites the steps. Zero writes. The assembly is skipped on its sha256.

Getting started

Four steps to a
deployed plugin.

  1. Install the CLI

    A .NET global tool that talks to the Dataverse Web API directly — no Windows dependency, nothing to install on a server.

    dotnet tool install --global Dverse.Cli --prerelease
    dverse version
  2. Point it at your sandbox

    One environment is the normal case — the sandbox you develop against. The file is committed and holds no secrets. Anyone who wants their own sandbox adds a gitignored dataverse.local.yaml instead of editing this one.

    dataverse.yaml
    default: dev
    
    environments:
      dev:
        url: https://contoso-dev.crm4.dynamics.com
        solution: contoso        # created if missing
    
        prefix: contoso          # publisher prefix, also created if missing
    
    
    model:
      entities: [account, contact]
  3. Sign in once

    Device code, cached under ~/.dverse. Later runs are silent. CI uses a service principal from environment variables instead.

    dverse auth login
    dverse whoami
  4. Plan, then apply

    From the plugin project directory, or through MSBuild as part of an ordinary build.

    dverse plan            # what would change, and nothing else
    
    dverse apply           # build, diff, write only the deltas
    
    
    # or from MSBuild, as part of an ordinary build:
    
    dotnet build -t:Deploy
    dotnet build -t:Plan -p:DversePlanFailOnChanges=true   # CI drift gate

That's the whole loop.

From there it's ordinary work: write a handler, run plan, commit the diff. The full guide ships with the package.

$ dotnet tool install --global Dverse.Cli --prerelease