dversedev

Guide

Solution layout

How a repository is shaped once it holds several assemblies, web resources, schema and UI.

The dverse solution layout

A best-practice repository shape for a real Dataverse/Dynamics 365 project deployed entirely with dverse — one command, no provision.ps1, no pac. It scales from a single plugin assembly to a multi-team codebase with several assemblies, web resources, PCF controls, declared schema, forms/views, app modules, and seed data.

New here? Start with getting-started.md — it walks one project from empty folder to deployed plugin. This document is the organizing guide: where everything lives, how many assemblies to have, and how deploys compose across environments.

The tree

your-solution/
├─ dataverse.yaml                 # environments, model config, and what each env deploys — committed
├─ dataverse.local.yaml           # personal overrides (a dev org, a default env) — gitignored
├─ dataverse.snapshot.json        # metadata snapshot for early-bound models — committed, `dverse model sync`
├─ schema/                        # declared tables/columns/option sets (`dverse schema adopt` bootstraps)
│  └─ core.yaml
├─ seed/                          # reference-data rows (referenced from `seed:` in dataverse.yaml)
│  └─ settings.yaml
├─ src/
│  ├─ MyCo.Plugins.Core/          # classic strong-named assembly — the default
│  │  ├─ MyCo.Plugins.Core.csproj
│  │  ├─ MyCo.Plugins.Core.snk
│  │  └─ AccountPlugins.cs …
│  └─ MyCo.Plugins.Integration/   # a second assembly — package mode (has NuGet dependencies)
│     ├─ MyCo.Plugins.Integration.csproj
│     └─ ErpSyncPlugins.cs …
├─ tests/
│  ├─ MyCo.Plugins.Core.Tests/
│  └─ MyCo.Plugins.Integration.Tests/
├─ webresources/
│  ├─ src/                        # authored TS/JS/HTML (your build input)
│  └─ dist/                       # build output dverse deploys (path: in dataverse.yaml)
├─ pcf/
│  └─ CreditGauge/                # a PCF control project (pac/dverse pcf init)
├─ forms/                         # committed formxml, {entity}/{type}/{name}.xml (`dverse forms pull`)
│  └─ account/main/Account.xml
├─ views/                         # committed savedquery xml, {entity}/{name}.xml (`dverse views pull`)
│  └─ account/Active Accounts.xml
├─ apps/                          # app module + sitemap customizations fragments ({{webresource:…}} tokens)
│  └─ sales/{appmodule,sitemap}.xml
├─ ribbons/                       # committed RibbonDiffXml, {entity}.xml + application.xml
│  └─ account.xml
├─ commands/                      # modern command-bar buttons (appaction rows) as YAML
│  └─ account/your_account.approve.yaml   # `dverse commands pull` writes {entity}/{uniquename}.yaml
└─ .github/workflows/deploy.yml   # CI: drift gate on PRs, apply on main

Not every repo needs every folder. A plugins-only project is just dataverse.yaml, the snapshot, and src/ — add folders as you adopt each component kind.

Naming and boundaries

dverse owns exactly what you declare, and it recognizes "yours" by a publisher prefix. Keep these consistent across the whole repo:

  • One publisher prefix (prefix: in dataverse.yaml, e.g. myco). Every table, column, option set, web resource, PCF control, and env variable dverse creates is prefixed with it — and prefix-scoping is how prune/adopt know what to manage versus leave alone.
  • One solution per repo (solution: in dataverse.yaml). Web resources conventionally get their own solution (solution: on the webresources: entry) so registration churn and web UI churn don't share a version history, but a single solution is fine to start.
  • Step keys are stable identity. Give a [Step] a Key wherever you want to rename freely: the deterministic step name derives from it, so moving a handler between classes or assemblies keeps the same environment row.
  • Test data gets a prefix. Have integration tests create only recognisably-named records and delete them in teardown — never real data.

How many assemblies?

Start with one. Split only when you have a concrete reason:

Reason Split
A NuGet dependency (Newtonsoft.Json, a shared lib) a package-mode assembly (package: true) — the classic single assembly can't carry dependencies
Team / ownership boundaries, independent release cadence one assembly per team, each its own src/ project
Isolation of risky/experimental handlers a separate assembly so a bad deploy is contained

Don't split for tidiness — every assembly is another build, another pluginassembly row, and another thing to reason about. Classic (strong-named, single DLL) is the default; package mode is opt-in per assembly. See getting-started, "Plugin packages", for what package mode changes.

Deploying the whole repo

dverse plan / dverse apply with no --assembly deploy the entire repo in one pass: every assembly listed under assemblies:, plus the shared environment-level components (schema, web resources, PCF, env vars, seed) diffed and written once.

The deploy is scoped to the directory you run from. At the repo root that is everything configured; inside one project's directory it is just that project — sibling assemblies, web resource sets, and PCF projects outside the current directory are neither built nor diffed (a note on stderr lists what was skipped). Environment variables have no directory and always stay in scope — the deployed assembly's manifest is validated against them. --all restores the full configured scope from anywhere.

yaml
# dataverse.yaml
default: dev

model:
  entities: [account, contact, myco_contract]

environments:
  dev:
    url: https://myco-dev.crm.dynamics.com
    solution: mycosolution
    prefix: myco
    assemblies:                      # the repo's plugin assemblies, deployed together
      - src/MyCo.Plugins.Core        # classic — a bare path is the simplest form
      - path: src/MyCo.Plugins.Integration
        package: true                # this one carries NuGet dependencies
    webresources:
      - path: webresources/dist
        prefix: myco_/
        solution: mycowebresources
        build: npm run build
    pcf:
      - path: pcf/CreditGauge
    ribbons:
      - path: ribbons            # {entity}.xml + application.xml; the file IS the whole ribbon
    commands:
      - path: commands           # *.yaml; appaction rows, field-level diffed and prefix-pruned
    environmentVariables:
      myco_ApiEndpoint: https://erp.example.com
bash
dverse plan          # from the repo root: builds every assembly, shows the combined diff
dverse apply         # deploys all assemblies + web resources + PCF + env vars in one run
dverse plan --json   # machine-readable: an `assemblies[]` array + the env-level components once

cd src/MyCo.Plugins.Core
dverse apply         # scoped: deploys only this project (no sibling builds or diffs)
dverse apply --all   # full configured scope, regardless of the current directory

An assemblies: entry is a path to a .csproj, a directory containing one, or a prebuilt .dll; { path, package, packageId } is the long form. A repo with no plugin assembly at all — only web resources, schema, or env vars — is a valid environment-only deploy: bare dverse plan/apply does the right thing with an empty assemblies:.

The MSBuild targets (dotnet build -t:Deploy, -t:Plan) still operate on a single project (they pass --assembly), so they remain the right choice inside one project's directory. Use the bare CLI at the repo root for a whole-repo deploy.

Version bumps compose. When content changed but a version didn't increase, apply bumps each assembly's own AssemblyVersion (and each PCF control's manifest), rebuilds, and re-plans — per assembly, so one changed project doesn't perturb the others. See getting-started "Versions increase automatically."

Environments & CI

One environment is the normal case. dverse converges the environment you develop in — your sandbox. Getting from there to production is unchanged: export a managed solution and import it downstream, the way Power Platform ALM already works. dverse does not apply to production, and it never touches components it does not manage. Most repositories therefore declare exactly one environments: block and never pass --env at all.

  • dataverse.local.yaml (gitignored) holds a developer's personal org and default:, so everyone can point at their own sandbox without touching the committed config. This — not a second block in dataverse.yaml — is how a team with several sandboxes works.
  • More than one block is worth it when you genuinely deploy from source to more than one environment: same shape, different URLs and solutions, with --env choosing. A shared integration org alongside personal sandboxes is the usual reason.
  • Service-principal auth in CI: set DVERSE_TENANT_ID / DVERSE_CLIENT_ID / DVERSE_CLIENT_SECRET (never commit them); dverse uses them automatically. Interactive dverse auth login (device code) is for humans.
  • Drift gate on PRs: dverse plan exits 2 when the environment doesn't match the branch (0 = clean, 1 = error) — fail the job on 2. Or dotnet build -t:Plan -p:DversePlanFailOnChanges=true per project. See getting-started for the full CI recipe.
  • Apply on main only, serialized per environment, with the service-principal secrets wired in.

What deploys, in one pass

dverse apply converges everything above in dependency order: declared schema first (steps may bind to its tables), then assemblies/steps/APIs, environment variables, web resources, PCF, views, forms, ribbons, commands, app modules, reference data — and finally exactly ONE PublishXml covering what the apply touched. Capture designer work back into the repo with dverse schema adopt, dverse forms pull, dverse views pull, dverse commands pull, and dverse webresources pull.

Each of those sets is also a prune boundary: dverse deletes environment-only components only inside a scope the repo declares (a webresources:/commands: set's prefix:, or its own reserved namespaces). Declare nothing for a kind and it manages nothing — the safe default when adopting dverse into an org whose publisher prefix is older than the repo. Turn prune off for one kind with --no-prune=commands (or prune: false on the individual set) instead of dropping it everywhere with a bare --no-prune.

Not covered by dverse today, so they still need other tooling or a manual step: managed solution export, security roles, and custom-API parameter metadata.