Status: Reviewed Stack: PowerShell → SQL / stored procs → Blazor Depends on: SQL-CLI-01 helpful for post-deploy verify; SQL-CLI-03 for Backup-SqlDatabase alternatives Goal: Use sqlpackage from the CLI for extract/publish/script/export/import of DACPAC/BACPAC in ways that fit PowerShell deploy pipelines feeding SQL → Blazor.
Rick’s loop: AD/Windows/PowerShell → SQL stored procs → Blazor Server. sqlpackage is the DacFx CLI: schema-as-artifact (.dacpac) and portability (.bacpac). It is not a backup tool and not a bulk loader.
| Need | sqlpackage | SSMS Deploy / DAC UI | Backup / Restore (SQL-CLI-03) | bcp (SQL-CLI-02) | Manual .sql / sqlcmd (SQL-CLI-01) |
|---|---|---|---|---|---|
| Incremental schema deploy from SSDT / SQL project | ✅ Publish | ✅ Same engine, GUI | ❌ | ❌ | Possible; no model diff |
| Review plan before apply (CI gate) | ✅ Script / DeployReport | Preview in UI | N/A | N/A | Diff by eye |
| Capture prod schema → lab | ✅ Extract → Publish | Extract Data-tier App | Full restore (heavy) | ❌ | Script objects by hand |
| Move schema + data to another server/cloud | ✅ Export / Import (BACPAC) | Export Data-tier App | ✅ Native BAK (same edition family) | Partial | Painful |
| Point-in-time / disaster recovery | ❌ | ❌ | ✅ Prefer | ❌ | ❌ |
| Multi-GB inventory CSV → staging | ❌ | ❌ | ❌ | ✅ | ❌ |
| Drop/recreate empty lab DB from model | ✅ Publish + CreateNewDatabase | Yes | Restore over | ❌ | DROP/CREATE scripts |
| Detect drift vs last registered DAC | ✅ DriftReport | Limited | ❌ | ❌ | Custom |
| Agent / CI exit codes | ✅ $LASTEXITCODE |
Manual | Cmdlet exceptions | ✅ | ✅ -b |
Rule of thumb
.dacpac.DACPAC vs BACPAC (memorize this)
| Artifact | Default contents | Primary actions | Use for |
|---|---|---|---|
.dacpac |
Schema only (data optional via Extract props) | Extract, Publish, Script, DeployReport | Deploy / promote schema |
.bacpac |
Schema + data | Export, Import | Portability / copy to empty DB |
Files are compressed, not encrypted. Treat both as sensitive.
Official Learn (sql-server-ver17 / June 2026): latest build 170.4.83.3 (3 Jun 2026). Prefer standalone sqlpackage over the copy bundled with SSMS/VS — release cadence is faster and pipelines stay predictable.
Requires .NET SDK 8+ (Learn recommends the .NET 10 SqlPackage line).
dotnet --list-sdks
dotnet tool install -g microsoft.sqlpackage
# pin when you care:
# dotnet tool install -g microsoft.sqlpackage --version 170.4.83.3
dotnet tool update -g microsoft.sqlpackage
sqlpackage /Version
If NuGet can’t find the package: ensure nuget.org is a source (dotnet nuget list source). Newer SDK without matching runtime: add --allow-roll-forward.
winget search SqlPackage
winget install --id Microsoft.SqlPackage --exact
# or pin:
# winget install --id Microsoft.SqlPackage --exact --version 170.4.83.3
# silent (Intune / SCCM style):
# winget install --id Microsoft.SqlPackage --exact --silent --accept-package-agreements --accept-source-agreements
| Method | Notes | Typical path / use |
|---|---|---|
| dotnet tool | Best for CI + jump boxes; easy update | On PATH as sqlpackage |
| winget → MSI | Machine-wide; good for shared DBA hosts | C:\Program Files\Microsoft SQL Server\170\DAC\bin\SqlPackage.exe |
| DacFramework.msi | Evergreen: https://aka.ms/dacfx-msi |
Same ...\170\DAC\bin |
| Windows .NET 10 ZIP | No SDK; unpack anywhere | Evergreen: https://aka.ms/sqlpackage-windows |
# Verify
sqlpackage /Version
# or full path after MSI:
& "C:\Program Files\Microsoft SQL Server\170\DAC\bin\SqlPackage.exe" /Version
Get-Command sqlpackage -ErrorAction SilentlyContinue | Format-List Source, Version
Pipeline tip: install at job start with dotnet tool install -g microsoft.sqlpackage rather than depending on image-bundled copies (ubuntu-latest no longer guarantees it).
| Action | Artifact | What it does | When Rick uses it |
|---|---|---|---|
| Extract | → .dacpac |
Reverse-engineer connected DB schema (data off by default) | Snapshot prod/lab schema; feed SQL projects; baseline before change |
| Publish | .dacpac → DB |
Incremental desired-state deploy; creates DB if missing + permissions | Deploy procs/tables that Blazor calls; promote lab → UAT |
| Script | .dacpac + target → .sql |
T-SQL incremental script without applying | Code review / change ticket / “what will run?” |
| DeployReport | → .xml |
Machine-readable plan of Publish changes | CI artifact; parse for Drop/Alter warnings |
| DriftReport | → .xml |
Diff vs last registered DAC on that DB | Catch hotfixes applied outside pipeline (RegisterDataTierApplication) |
| Export | → .bacpac |
Schema + data from live DB | Copy small/medium DB to another environment (not DR) |
| Import | .bacpac → new DB |
Create DB and load schema+data | Spin lab from bacpac; Azure SQL portability |
Ops notes
SqlPackage uses Microsoft.Data.SqlClient auth. Prefer /SourceConnectionString + /TargetConnectionString (or short forms /scs / /tcs) so Encrypt/Trust settings live in one place.
| Mode | How | Notes |
|---|---|---|
| Windows Integrated | Omit user/password; run as domain account | Default on-prem AD. Best for Rick’s jump box → SQL. |
| SQL auth | User ID= / Password= in connection string, or /tu /tp |
Prefer secret store; avoid plaintext in scripts committed to git. |
| Entra password | Authentication=Active Directory Password; |
Interactive/automation with UPN + password. |
| Entra integrated | Authentication=Active Directory Integrated; |
Domain-joined + Entra sync scenarios. |
| Managed identity | Authentication=Active Directory Managed Identity; |
Preferred in Azure Pipelines / MI-enabled hosts. Optional User Id=<client-id> for user-assigned. |
| Service principal | Authentication=Active Directory Service Principal; User Id=<appId>; Password=<secret> or /at:<token> |
Token path: you own refresh; SP-in-string lets SqlPackage refresh. |
| Universal / MFA | /UniversalAuthentication:True (+ /TenantId for guests) |
Interactive; poor fit for unattended CI. |
TLS (do this explicitly)
| Setting | Guidance |
|---|---|
Encrypt=True / /tec:Mandatory |
Default expectation for Azure SQL and hardened on-prem. |
TrustServerCertificate=False |
Prod / Azure. |
TrustServerCertificate=True / /ttsc:True |
Lab self-signed only. |
HostNameInCertificate= |
When you connect by short name / alias under Force Encryption. |
| Entra timeouts | /tt:30 or higher recommended. |
# Windows Integrated — on-prem
$tcs = "Server=SQLPROD01;Database=AppDb;Integrated Security=True;Encrypt=True;TrustServerCertificate=False"
# SQL auth
$tcs = "Server=SQLPROD01;Database=AppDb;User ID=deploy;Password=$env:SQL_DEPLOY_PWD;Encrypt=True;TrustServerCertificate=False"
# Azure SQL + managed identity
$tcs = "Server=tcp:myapp.database.windows.net,1433;Initial Catalog=AppDb;Authentication=Active Directory Managed Identity;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30"
Cross-link: for ad-hoc verify after publish, use sqlcmd or Invoke-Sqlcmd (SQL-CLI-03) — same Encrypt/Trust discipline.
Syntax: SqlPackage {parameters} {properties } {SQLCMD variables}
| Flag | Short | Purpose |
|---|---|---|
/Action: |
/a: |
Extract, Publish, Script, Export, Import, DeployReport, DriftReport |
/SourceFile: |
/sf: |
Input .dacpac (Publish/Script) or.bacpac` (Import) |
/TargetFile: |
/tf: |
Output .dacpac / .bacpac / report path as applicable |
/SourceConnectionString: |
/scs: |
Live source DB |
/TargetConnectionString: |
/tcs: |
Live target DB |
/SourceServerName: /SourceDatabaseName: |
/ssn /sdn |
Alternate to full SCS (plus user/pass as needed) |
/TargetServerName: /TargetDatabaseName: |
/tsn /tdn |
Alternate to full TCS |
/AccessToken: |
/at: |
Entra bearer token (omit User Id/Password in CS) |
/Profile: |
/pr: |
Publish profile (.xml) with properties/variables |
/DiagnosticsFile: |
/df: |
Verbose log file for failures |
/Quiet: |
/q: |
Less console noise (CI) |
/OverwriteFiles: |
/of: |
Allow overwrite of outputs |
/DeployScriptPath: |
/dsp: |
Script output path (Script action or alongside Publish) |
/DeployReportPath: |
/drp: |
DeployReport XML path (DeployReport action or alongside Publish) |
/OutputPath: |
/op: |
Generic output path (Script / DeployReport / DriftReport) |
/Variables: |
/v:Name=Value |
SQLCMD variables for pre/post scripts in dacpac |
/Version |
Print build number |
| Property | Default | Ops meaning |
|---|---|---|
/p:BlockOnPossibleDataLoss |
True |
Keep True in shared envs. Blocks type changes / drops that might lose data (even if table empty). Set False only with eyes open. |
/p:DropObjectsNotInSource |
False |
True = make target match dacpac (drops extras). Dangerous without exclusions. |
/p:DoNotDropObjectTypes |
— | Semicolon list preserved when DropObjectsNotInSource=True (e.g. Users;Logins;Permissions;RoleMembership) |
/p:ExcludeObjectTypes |
— | Ignore types entirely during compare/deploy. Quote in PowerShell ("Users;Logins") — ; is a PS statement separator. |
/p:CreateNewDatabase |
False |
Drop + recreate target (lab reset). Not for prod. |
/p:BackupDatabaseBeforeChanges |
False |
On-prem only; not Azure SQL. Prefer real backup jobs (SQL-CLI-03). |
/p:GenerateSmartDefaults |
False |
Helps NOT NULL column adds on tables with data. |
/p:ScriptDatabaseOptions |
True |
Often exclude DatabaseOptions if env-specific. |
/p:VerifyDeployment |
True |
Leave on; disable only when debugging. |
/p:AllowIncompatiblePlatform |
False |
Rare; Azure ↔ on-prem edge cases. |
/p:CommandTimeout |
60 |
Raise for big publishes. |
/p:RegisterDataTierApplication |
False |
Set True if you plan to use DriftReport later. |
/p:IgnorePermissions / IgnoreRoleMembership / IgnoreUserSettingsObjects |
varies | Common when security is managed outside the dacpac. |
Extract data knobs (prefer BACPAC for full data moves): /p:ExtractAllTableData=True or repeated /p:TableData=schema.table.
Export size: /p:TempDirectoryForTableData=D:\sqlpkg-temp when TEMP is small.
sqlpackage is an exe → use $LASTEXITCODE (same discipline as sqlcmd / bcp). Do not assume throw-on-failure.
function Invoke-SqlPackage {
param([Parameter(Mandatory)][string[]]$ArgumentList)
& sqlpackage @ArgumentList
if ($LASTEXITCODE -ne 0) {
throw "sqlpackage failed with exit code $LASTEXITCODE"
}
}
CI-friendly publish
dotnet tool install -g microsoft.sqlpackage).Script or DeployReport → publish artifact for review.Publish with identical /p: set + /df:sqlpackage.log + /q:True.Extract prod → publish lab
prod (Extract) → AppDb.dacpac → lab (Publish, CreateNewDatabase or empty DB)
└─ Script against lab for human review (optional gate)
Exclude users/logins/permissions when lab auth ≠ prod.
Script for review before apply
Invoke-SqlPackage @(
'/Action:Script'
"/SourceFile:$dacpac"
"/TargetConnectionString:$labCs"
"/DeployScriptPath:$outSql" # /dsp — also accepts /OutputPath
'/p:BlockOnPossibleDataLoss=True'
)
# Apply later:
# sqlcmd -S ... -d ... -b -i $outSql # SQL-CLI-01
Publish can emit the same artifacts without applying-only mode via
/dsp:(script) and/drp:(report) alongside/Action:Publish.
Exit codes: treat anything non-zero as failure. Capture /DiagnosticsFile on failure paths in CI.
| Gotcha | Reality |
|---|---|
| Data loss blocks | BlockOnPossibleDataLoss=True fires on possible loss (column type change, drop column, etc.) even on empty tables. Don’t casually set False in shared UAT. |
| Users / logins / permissions | Contained users ship in dacpac/bacpac; passwords are replaced with random values — reset after Import/Publish. Prefer excluding Users/Logins/Permissions/RoleMembership and managing security separately (AD groups). |
| DropObjectsNotInSource | Will drop hotfix objects not in the model. Pair with DoNotDropObjectTypes or keep False and delete deliberately. |
PowerShell ; |
/p:ExcludeObjectTypes=Users;Logins breaks in PS. Use quotes: "/p:ExcludeObjectTypes=Users;Logins". |
| Temporal tables | History table / period changes can force table rebuilds; expect longer publishes and data-motion checks. |
| Always Encrypted | Keys/attestations needed (EnclaveAttestationUrl, AKV auth props). Extract/Publish without key access fails or strips. |
| Azure SQL firewall | Client IP (or agent outbound IPs) must be allowed; MI/SP still need network path. |
| BACPAC ≠ backup | No differential, no log chain, weaker consistency story. Use Backup-SqlDatabase for DR. |
| DACPAC ≠ bulk load | Not a substitute for bcp inventory CSVs. |
| Large Export | Slow / temp-disk hungry; quiesce writers; consider size limits (~200 GB guidance). |
| SSMS-bundled sqlpackage | Stale. Pin standalone 170.x in pipelines. |
| Import destination | Designed for new database — don’t expect merge semantics into busy prod. |
| Telemetry | Opt out: DACFX_TELEMETRY_OPTOUT=1 if policy requires. |
$src = "Server=SQLPROD01;Database=AppDb;Integrated Security=True;Encrypt=True;TrustServerCertificate=False"
sqlpackage /Action:Extract `
/SourceConnectionString:$src `
/TargetFile:"D:\dac\AppDb.dacpac" `
/p:VerifyExtraction=True `
/OverwriteFiles:True
if ($LASTEXITCODE -ne 0) { throw "Extract failed: $LASTEXITCODE" }
$tgt = "Server=SQLLAB01;Database=AppDb;Integrated Security=True;Encrypt=True;TrustServerCertificate=True"
sqlpackage /Action:Publish `
/SourceFile:"D:\dac\AppDb.dacpac" `
/TargetConnectionString:$tgt `
/p:BlockOnPossibleDataLoss=True `
/p:DropObjectsNotInSource=False `
"/p:ExcludeObjectTypes=Users;Logins;Permissions;RoleMembership;ExtendedProperties" `
/DiagnosticsFile:"D:\dac\publish-lab.log" `
/Quiet:True
if ($LASTEXITCODE -ne 0) { throw "Publish failed: $LASTEXITCODE" }
sqlpackage /Action:Script `
/SourceFile:"D:\dac\AppDb.dacpac" `
/TargetConnectionString:$tgt `
/DeployScriptPath:"D:\dac\AppDb-upgrade.sql" `
/p:BlockOnPossibleDataLoss=True `
"/p:ExcludeObjectTypes=Users;Logins;Permissions;RoleMembership"
if ($LASTEXITCODE -ne 0) { throw "Script failed: $LASTEXITCODE" }
# Review AppDb-upgrade.sql, then apply via SQL-CLI-01 sqlcmd -b -i ...
sqlpackage /Action:DeployReport `
/SourceFile:"D:\dac\AppDb.dacpac" `
/TargetConnectionString:$tgt `
/DeployReportPath:"D:\dac\AppDb-deploy.xml" `
/p:BlockOnPossibleDataLoss=True
if ($LASTEXITCODE -ne 0) { throw "DeployReport failed: $LASTEXITCODE" }
# Export (quiesce writes or use a copy)
sqlpackage /Action:Export `
/SourceConnectionString:$src `
/TargetFile:"D:\dac\AppDb.bacpac" `
/OverwriteFiles:True `
/p:TempDirectoryForTableData:"D:\sqlpkg-temp"
if ($LASTEXITCODE -ne 0) { throw "Export failed: $LASTEXITCODE" }
# Import into NEW database name on lab
$newCs = "Server=SQLLAB01;Database=AppDb_Copy;Integrated Security=True;Encrypt=True;TrustServerCertificate=True"
sqlpackage /Action:Import `
/SourceFile:"D:\dac\AppDb.bacpac" `
/TargetConnectionString:$newCs
if ($LASTEXITCODE -ne 0) { throw "Import failed: $LASTEXITCODE" }
# Reset contained-user passwords after import
$LASTEXITCODE + diagnosticsfunction Invoke-SqlPackage {
[CmdletBinding()]
param(
[Parameter(Mandatory)][ValidateSet('Extract','Publish','Script','Export','Import','DeployReport','DriftReport')]
[string]$Action,
[Parameter(Mandatory)][hashtable]$Params,
[string]$DiagnosticsFile
)
$args = @("/Action:$Action")
foreach ($k in $Params.Keys) { $args += "/${k}:$($Params[$k])" }
if ($DiagnosticsFile) { $args += "/DiagnosticsFile:$DiagnosticsFile" }
Write-Verbose ($args -join ' ')
& sqlpackage @args
$code = $LASTEXITCODE
if ($code -ne 0) {
throw "sqlpackage $Action failed with exit code $code$(if ($DiagnosticsFile) { "; see $DiagnosticsFile" })"
}
}
Invoke-SqlPackage -Action Publish -DiagnosticsFile 'D:\dac\pub.log' -Params @{
SourceFile = 'D:\dac\AppDb.dacpac'
TargetConnectionString = $tgt
'p:BlockOnPossibleDataLoss' = 'True'
Quiet = 'True'
}
$tcs = "Server=tcp:myapp.database.windows.net,1433;Initial Catalog=AppDb;Authentication=Active Directory Managed Identity;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30"
sqlpackage /Action:Publish `
/SourceFile:"D:\dac\AppDb.dacpac" `
/TargetConnectionString:$tcs `
/p:BlockOnPossibleDataLoss=True `
"/p:ExcludeObjectTypes=Users;Logins;Permissions;RoleMembership"
if ($LASTEXITCODE -ne 0) { throw "Azure publish failed: $LASTEXITCODE" }
# Requires Az.Accounts; audience for Azure SQL
Connect-AzAccount -ServicePrincipal -Tenant $TenantId -Credential $SpCredential | Out-Null
$token = (Get-AzAccessToken -ResourceUrl "https://database.windows.net/").Token
$tcs = "Server=tcp:myapp.database.windows.net,1433;Initial Catalog=AppDb;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30"
sqlpackage /Action:Publish `
/SourceFile:"D:\dac\AppDb.dacpac" `
/TargetConnectionString:$tcs `
/AccessToken:$token `
/p:BlockOnPossibleDataLoss=True
if ($LASTEXITCODE -ne 0) { throw "Token publish failed: $LASTEXITCODE" }
| Situation | Command shape |
|---|---|
| Promote schema | Publish + BlockOnPossibleDataLoss=True + exclude Users/Logins/Permissions |
| Change ticket | Script / DeployReport → review → Publish or sqlcmd -b -i |
| Refresh lab data+schema | Export → Import to new DB name (or Publish+CreateNewDatabase for schema-only reset) |
| DR / PITO | Not sqlpackage → SQL-CLI-03 Backup/Restore |
| Inventory CSV | Not sqlpackage → SQL-CLI-02 |
| Post-deploy smoke | SQL-CLI-01 / Invoke-Sqlcmd |
Docs (Learn, sql-server-ver17): SqlPackage overview · Download/install · Publish · Extract · Export · Release notes 170.4.83.3