dvspec 1.0.1
dotnet tool install --global dvspec --version 1.0.1
dotnet new tool-manifest
dotnet tool install --local dvspec --version 1.0.1
#tool dotnet:?package=dvspec&version=1.0.1
nuke :add-package dvspec --version 1.0.1
XrmMetadata (dvspec)
A CLI that applies sparse, declarative schema changes to a Microsoft Dataverse solution. You author a desired-state spec (YAML/JSON); the tool fetches current definitions, computes a minimal diff, previews it, checks nobody else changed the touched objects since planning, and then applies only the necessary changes inside a target unmanaged solution.
It only ever writes the properties in the diff — it never round-trips and re-saves a whole object,
so it can't clobber schema it doesn't manage. Re-running an already-applied spec produces an empty
plan. See .conducktor/SPEC.md for the full specification.
What it can change
Tables, columns (text/memo/integer/decimal/money/boolean/datetime/choice/lookup), global & local
choices, relationships (1:N, N:1, N:N), views (savedquery, authored via a DSL that compiles to
FetchXML + LayoutXML), and forms (systemform, authored via a DSL that sparse-merges into the
existing FormXML). It does not touch business data, managed solutions, or plugins/workflows/web
resources (forms that use scripts round-trip intact but aren't authored by the tool). Destructive changes are never inferred. prune: is parsed but deletion execution is not shipped;
--allow-destroy is rejected before an unexecutable plan can be generated.
Requirements
- .NET 10 SDK (the project targets
net10.0). - A supported Dataverse credential: interactive browser (default), device code, Azure CLI, client secret, or certificate.
- Access to a Dataverse environment and an unmanaged solution to target. The signed-in principal
needs System Customizer (or System Administrator) with
AddSolutionComponent/publish rights.
Build & run
Clone, then build:
dotnet build
During development, run the CLI through dotnet run (everything after -- is passed to dvspec):
dotnet run --project src/XrmMetadata -- <command> [options]
# e.g.
dotnet run --project src/XrmMetadata -- plan --json
To get a plain dvspec binary you can put on your PATH:
dotnet publish src/XrmMetadata -c Release -o ./dist
./dist/dvspec plan --json
The examples below use dvspec … for brevity — substitute dotnet run --project src/XrmMetadata -- …
if you haven't published.
Getting started
1. Configure the Dataverse connection
dvspec uses DataverseConnection 1.2.4
with the same IConfiguration + AddDataverseWithOrganizationServices() registration style as
XrmSync. It searches upward from the working directory for the nearest appsettings.json, optionally
layers appsettings.<env>.json, and finally applies environment variables. Environment variables
always win.
{
"DATAVERSE_URL": "https://org899a6fb0.crm4.dynamics.com",
"DATAVERSE_CREDENTIAL_TYPE": "browser"
}
Supported credential types are browser (default), deviceCode, azcli, clientSecret, and
certificate (also accepted as clientCertificate):
| Type | Additional configuration |
|---|---|
browser |
none; interactive browser sign-in |
deviceCode |
none; interactive device-code sign-in |
azcli |
an existing az login session |
clientSecret |
AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET |
certificate |
AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_CERTIFICATE_PATH; optional AZURE_CLIENT_CERTIFICATE_PASSWORD |
Keep secrets in environment variables or your CI secret provider, not in JSON. dvspec never writes
or prints credentials, secrets, or tokens, and disables persistent token caching for its browser and
device-code credentials.
For a development override, add appsettings.dev.json and pass --env dev. Without --env, the
optional override name is Production. For one transition period, an existing
~/.dvspec/profiles.json entry is used only when no DATAVERSE_URL is found; this emits a deprecation
warning to stderr.
2. Initialize the project
Point a project at a solution. This writes .dvspec/config.json and validates that the solution
exists, is unmanaged, and that the prefix matches its publisher (warns on mismatch):
dvspec init --env dev --solution acme_core --prefix acme --language 1033
--env selects the optional appsettings.<env>.json override; omit it to use Production. --prefix is your 2–8-char publisher prefix; --language is
the default LCID for labels (v1 authors labels in a single base language).
3. Author a spec
Create spec.yaml. Write all names without the publisher prefix — the tool prepends it (table
project → acme_project, column name → acme_name). Lean on healthy defaults and supply only
the fields that carry intent:
version: 1
target:
environment: dev
solution: acme_core
publisherPrefix: acme
language: 1033
choices:
- name: priority
displayName: Priority
options:
- { value: 100000000, label: Low }
- { value: 100000001, label: High }
tables:
- name: project
displayName: Project
displayCollectionName: Projects
ownership: user
columns:
- { name: budget, displayName: Budget, type: money }
- name: status
displayName: Status
type: choice
options:
- { value: 1, label: Planned }
- { value: 2, label: Active }
- { value: 3, label: Closed }
- { name: priority, displayName: Priority, type: choice, choice: priority }
- { name: owner_contact, displayName: Owner Contact, type: lookup, target: contact }
views:
- name: Active Projects
table: project
type: public
default: true
columns:
- { attribute: name, width: 300 }
- { attribute: status, width: 120 }
- { attribute: budget, width: 120 }
sort: [ { attribute: name, direction: asc } ]
filter:
operator: and
conditions:
- { attribute: statecode, operator: eq, value: 0 }
forms:
- name: Project Main
table: project
type: main
default: true
tabs:
- label: General
columns: 2
sections:
- label: Details
column: 1
controls:
- field: name
- field: status
- field: priority
- label: Related
column: 2
controls:
- field: budget
- field: owner_contact
JSON specs with an identical schema are also accepted. A per-object raw: true escape hatch supplies
verbatim FetchXML/FormXML when the DSL can't express something. The SPEC
covers the full DSL (subgrids, relationships, all column types, defaults, and the prune: block).
Before authoring against an existing table, ground yourself so you reuse real schema names instead of inventing them:
dvspec describe contact --json # existing columns, choice values, relationship targets
4. Validate, plan, preview
dvspec validate # lint the spec offline (no environment access)
dvspec plan # compute the diff against live state; persists the plan + baseline
dvspec preview # localhost UI of the plan at http://localhost:5787
plan --watch re-plans on every save and auto-launches the preview, which streams updates over SSE
with no browser refresh. The preview is read-only by default; add --allow-apply to expose an
apply button (the CLI remains the primary apply path). High-risk operations are visually distinguished, and each object
shows healthy-default provenance (which values came from defaults vs. your spec).
5. Apply
dvspec apply --yes
apply re-checks drift against the persisted baseline first. On a clean apply it batches operations
in topological order (choices → tables → columns → lookups/relationships → views/forms), issues a
single publish, records a run journal, and updates the baseline to the freshly-applied state — so an
immediate re-plan is empty.
If a teammate changed a touched object since you planned, apply blocks with a drift report (exit
2). Review it, then either accept current live as the new baseline and re-plan, or edit your spec:
dvspec rebase # accept current live as new baseline, then re-plan
Metadata ops are not transactional across requests, so on partial failure the tool does not
auto-revert — it reports exactly what was applied vs. not; because planning is desired-state, a
re-plan/apply converges on the remaining delta.
Command reference
dvspec init --env dev --solution acme_core --prefix acme [--language 1033]
dvspec describe <table> [--json] # dump a table's resolved definition (agent authoring aid)
dvspec fetch [--tables a,b,...] # refresh cached definitions + re-capture the baseline
dvspec validate [--spec spec.yaml] # lint a spec without touching the environment
dvspec plan [--spec spec.yaml] [--watch] [--json]
dvspec preview [--port 5787] [--allow-apply]
dvspec apply [--spec spec.yaml] [--yes] [--json] [--dry-run]
# --allow-destroy is reserved and currently rejected
dvspec rebase [--spec spec.yaml] # accept current live as the new baseline, then re-plan
Global flags: --env, deprecated alias --profile, --verbose, --dry-run.
Exit codes: 0 success · 1 validation error · 2 drift detected (apply blocked) · 3
immutable-property change required · 4 Dataverse API error · 5 auth/connection error.
--json on describe/fetch/plan/apply emits stable, machine-readable output.
The .dvspec/ state store
init and later commands maintain a per-project .dvspec/ directory:
.dvspec/
config.json # target solution, publisher prefix, language, appsettings environment
baseline.json # metadata version stamp + record ETags used for drift detection
plan.latest.json # last computed plan (machine-readable)
cache/ # optional cached definitions for offline authoring (from `fetch`)
It never contains secrets. config.json is committable and shareable (teams that want to share
it can force-add it — it's git-ignored by default); baseline.json, plan.latest.json, and
cache/ are local working state.
Authoring with an agent
The grounding commands (describe/fetch), the machine-readable --json plan/result, healthy
defaults, and the exit-code contract are designed so an agent (Claude Code) can author, verify, and
self-correct a spec against the real environment. The intended loop — and the MCP-shaped JSON
contract behind it — is documented in docs/AGENT.md.
Offline resolved-schema JSON contract
spec.yaml is the canonical schema source for offline consumers. Do not duplicate its YAML parsing,
publisher-prefix resolution, healthy defaults, lookup/choice resolution, FetchXML compilation, or FormXML
compilation. Invoke:
dvspec resolve --spec spec.yaml --json
resolve is entirely offline: it does not read .dvspec/config.json, load connection configuration, or contact
Dataverse. With --json, stdout contains only JSON and diagnostics go to stderr. Valid models return 0; invalid
specifications return 1. YAML and JSON specs are supported and identical input produces byte-identical output.
The top-level schemaVersion is the compatibility version of this integration contract. Version 1 contains
target, choices, tables, relationships, views, forms, externalTables, and structured warnings.
Fields may be added compatibly within a version; removals, renames, or semantic changes require a new version.
Column type, requirement, ownership, relationship, view, and form values are stable lowercase DSL values—not
C# enum or type names. Values supplied by healthy defaults carry { "source": "default" }; authored values
carry { "source": "spec" }.
A declared table may set entitySetName. Otherwise resolve and apply share a deterministic pluralization rule
(category → categories, box → boxes, otherwise append s). Set entitySetName explicitly for irregular
or organization-specific names.
External/system tables referenced by lookups, plus tables explicitly listed in the authored spec, are emitted under externalTables. For example:
externalTables:
- account
- contact
These tables are reference-only: plan/apply never creates or modifies them. Default fetch includes them. Without cache they are marked
complete: false and a structured externalTableIncomplete warning is emitted; no columns or relationships are
invented. Run dvspec fetch --tables account,contact to populate .dvspec/cache and enrich subsequent offline
resolution with real entity-set, primary-key/name, column, choice, lookup, and relationship metadata.
Local .NET tool
dotnet pack src/XrmMetadata/XrmMetadata.csproj -c Release -o artifacts
dotnet tool install dvspec --tool-path ./tools --add-source ./artifacts --version 0.1.0
./tools/dvspec --version
./tools/dvspec --help
The NuGet package ID and tool command are both dvspec; the preview SPA remains embedded in the tool package.
Publishing the tool
The publish dotnet tool GitHub Actions workflow publishes to NuGet.org when a GitHub Release is
published. The release tag is the package version and must use SemVer with a leading v, for example
v0.2.0 or v0.2.0-rc.1. The workflow checks out that tag, restores, builds, tests, packs, verifies a
local tool installation, uploads the .nupkg as a workflow artifact, and then pushes it to NuGet.org.
Repository setup required:
- In the NuGet.org account that owns
dvspec, create a Trusted Publishing policy for GitHub Actions with:- repository owner:
context-and - repository:
XrmMetadata - workflow file:
publish-tool.yml - environment: leave unspecified
- repository owner:
- Add a repository Actions secret named
NUGET_USERcontaining the NuGet.org account username. No API-key secret is stored in GitHub;NuGet/login@v1exchanges the GitHub OIDC token for a short-lived key. - Create and publish a GitHub Release whose tag matches the desired package version.
The workflow has only contents: read and id-token: write permissions. NuGet package versions are immutable,
so each release tag must use a new version. Draft releases do not publish.
Dry-run safety and destructive changes
dvspec apply --dry-run follows the normal validation, connection, drift-check, live-read, and fresh-plan path,
but never sends a write batch, publishes, confirms, or updates the baseline. JSON output is
{ "status": "dryRun", "plan": { ... } }; human output explicitly says that nothing was written. A preview
started with global --dry-run does not expose its optional apply endpoint. Commands without meaningful dry-run
semantics reject the flag instead of silently ignoring it.
Destructive apply is intentionally not shipped in this version. A prune: block without --allow-destroy is
suppressed with a warning and performs no deletion. Passing --allow-destroy is rejected during planning rather
than producing a plan that apply cannot execute. No deletion is ever inferred from absence.
Dependency audit
The Dataverse client currently brings System.Security.Cryptography.Xml through WCF. The project pins the
patched .NET 10 package line (10.0.11); clean restore auditing remains enabled and produces no high-severity
warnings. The vulnerable 8.0.2 assembly is not present in build or publish output.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.