A configurable data quality engine for Snowflake that runs as a CI/CD gate. Define checks in JSON, execute them against your warehouse, and fail the pipeline when data quality degrades.
DQ Gate solves a specific problem: data pipelines succeed at the infrastructure level (jobs complete, rows load) but silently produce stale, duplicated, or invalid data. This tool lets teams codify their data quality expectations and enforce them automatically, without writing any code.
- Seven built-in check types — freshness, multi-column uniqueness, custom SQL, schema validation, volume spike, referential integrity, and row count
- Configuration-driven — all rules defined in JSON files; no code changes needed to add or modify checks
- Concurrent execution — checks run in parallel, bounded by a configurable semaphore (default: 8 concurrent)
- Multi-format reporting — HTML, JSON, and CSV report writers; old reports are auto-purged after 30 days
- Notification channels — Email (SMTP), Pushover, and Microsoft Teams with configurable trigger conditions
- Incident deduplication and lifecycle tracking — failures are fingerprinted and tracked in a persistent SQLite store; each unique failure opens a single Active incident (New ? Ongoing ? Resolved) and re-notifies only after a configurable interval
- Table profiling and check suggestion — profile a table or entire schema to generate column-level statistics and a ready-to-paste check configuration
- Connection resilience — automatic retry with exponential backoff for transient Snowflake failures via Polly
- SQL safety — identifier sanitization and a statement guard that blocks destructive DML/DDL in custom queries
- Tag-based filtering — run subsets of checks by tag without maintaining separate config files
- Dry-run mode — validate all configuration without making any database connections
- Secret store integration — load sensitive connection credentials from an encrypted file-based secret store
- Structured logging — Serilog with file and console sinks, enriched with run ID and check context
graph TD
CLI["CLI Layer — DataQuality.Cli"] --> Core["Business Logic — DataQuality.Core"]
CLI --> SF["Database Adapter — DataQuality.Snowflake"]
SF --> Core
subgraph Core
Config["Configuration and Validation"]
Orch[CheckOrchestrator]
Runners[Check Runners]
Reports[Report Writers]
Notify[Notification Channels]
Incidents[Incident Store]
Profiler[Table Profiler]
end
CLI -- "builds host, wires DI" --> Orch
Orch -- "resolves via factory" --> Runners
Runners -- "IConnectionFactory" --> SF
Orch -- "after execution" --> Reports
Orch -- "after execution" --> Notify
Orch -- "per result" --> Incidents
| Component | Role |
|---|---|
DataQuality.Cli |
Entry point. Parses CLI arguments, builds the DI container, delegates to the orchestrator. |
DataQuality.Core |
All business logic. Check definitions, runners, orchestration, reporting, notifications, incident tracking, validation, and profiling. |
DataQuality.Snowflake |
Implements IConnectionFactory and IConnection for Snowflake. Wraps connections in a Polly resilience pipeline. |
The dependency direction is strict: Cli ? Core and Snowflake; Snowflake ? Core. Core has zero infrastructure dependencies.
sequenceDiagram
participant CLI as Program.cs
participant Host as DI Container
participant Orch as CheckOrchestrator
participant Factory as CheckRunnerFactory
participant Runner as ICheckRunner
participant DB as Snowflake
participant IS as IncidentStore
CLI->>Host: Build host, register services
Host->>Host: Load and validate JSON configs
CLI->>Orch: RunAsync()
Orch->>Orch: Filter by tags, split enabled/disabled
loop Each enabled check — concurrent
Orch->>Factory: GetCheckRunner(type)
Factory->>Runner: Resolve keyed singleton
Runner->>DB: Execute SQL
DB-->>Runner: Scalar or rows
Runner-->>Orch: Result<CheckResult>
end
Orch->>IS: Upsert incidents per result
Orch->>Orch: Aggregate, determine ExitCode
Orch->>Host: WriteReportsAsync()
Orch->>Host: NotifyAsync()
Orch-->>CLI: ExitCode
- JSON files from the config directory are deserialized into polymorphic
CheckConfigurationobjects. CheckConfigurationValidatorinvokes each config'sValidate()and throws on any errors before any checks run.CheckOrchestratorfilters by tags and enabled state, then runs checks concurrently viaTask.WhenAllbounded by aSemaphoreSlim(8).- Each runner sanitizes SQL identifiers, executes queries, and returns
FluentResults.Result<CheckResult>. CheckResultProcessorconverts raw results into outcomes (Pass, Warn, Fail, Error).IncidentStorefingerprints each result and upserts incident lifecycle records in SQLite.ReportServicewrites reports in all requested formats and purges files older than 30 days.NotificationServicedispatches to channels whose trigger conditions are met.- The exit code propagates to the shell — non-zero on any failure or error.
DQ_GATE.slnx
src/
DataQuality.Cli/ Application host and CLI
Commands/ System.CommandLine subcommands (run, list-rules, profile)
Configuration/ Secret store provider
Hosting/ Host builder extensions, AppRunner
Output/ Console writer implementations
Config/ Check definition JSON files
DataQuality.Core/ All business logic
Configuration/Checks/ Polymorphic CheckConfiguration base class
Configuration/Notifications/ Notification config DTOs
Connections/ IConnection, IConnectionFactory interfaces
Features/ Check runners and their configurations
Notifications/ NotificationService and channel implementations
Orchestration/ CheckOrchestrator, result processing, ExitCode
Profiling/ Table profiler, column stats, check suggestor
Reporting/ ReportService, HTML/JSON/CSV writers
Results/ CheckResult, CheckStatus, error and warning types
Settings/ ApplicationSettings, IncidentStoreSettings
Validation/ SQL sanitization and statement guard
DataQuality.Snowflake/ Snowflake adapter
Resilience/ Polly retry pipeline and settings
SnowflakeConnection.cs IConnection implementation
SnowflakeConnectionFactory.cs IConnectionFactory implementation
tests/
DataQuality.Tests/ xUnit + NSubstitute + FluentAssertions
- .NET 10 SDK
- A Snowflake account with a user that has
SELECTaccess to the tables you want to check
dotnet build
dotnet test
# Run all checks in the default Config/ directory
dotnet run --project src/DataQuality.Cli -- run
# Target a specific config directory
dotnet run --project src/DataQuality.Cli -- run --config-dir path/to/checks/
# Dry-run: validate config without connecting to Snowflake
dotnet run --project src/DataQuality.Cli -- run --dry-run
# Run only checks tagged "daily"
dotnet run --project src/DataQuality.Cli -- run --tags daily
# Write HTML + JSON reports to a custom directory
dotnet run --project src/DataQuality.Cli -- run --output html json --report-dir /tmp/reports
# Print all configured rules and exit
dotnet run --project src/DataQuality.Cli -- list-rules
# Profile a single table and display column statistics
dotnet run --project src/DataQuality.Cli -- profile MyConnection MYDB.PUBLIC.MY_TABLE
# Profile an entire schema and emit suggested check JSON
dotnet run --project src/DataQuality.Cli -- profile MyConnection MYDB.PUBLIC --output jsonDQ Gate uses subcommands. The top-level binary accepts one of three commands: run, list-rules, or profile.
The primary command. Loads check configs, runs all enabled checks, writes reports, and dispatches notifications.
| Option | Alias | Default | Description |
|---|---|---|---|
--config-dir |
-cd |
Config |
Directory containing JSON check configuration files |
--report-dir |
-rd |
Reports |
Directory to write generated reports |
--output |
-o |
html |
Report format(s): html, json, csv (space or comma-separated; repeatable) |
--tags |
-t |
(all) | Filter checks by tag(s); only checks matching at least one tag are executed |
--dry-run |
-d |
false |
Validate configuration without executing any checks |
--silent |
-s |
false |
Suppress all console output; process exit code is the only signal |
--secretfile |
-sf |
~/.secretstore |
Path to the encrypted secrets file for connection credentials |
--secretpassword |
-sp |
(env) | Password for the secrets file |
The --secretfile and --secretpassword options also read from SECRETSTORE_PATH and SECRETSTORE_PASSWORD environment variables respectively.
Loads all check configuration files from the default Config/ directory, prints a formatted summary of every rule, and exits without making any database connections.
dotnet run --project src/DataQuality.Cli -- list-rulesConnects to Snowflake, collects column-level statistics (null rate, distinct count, min/max, sample values), and either displays them in the console or emits a ready-to-paste check configuration in JSON format.
profile <connection> <target> [options]
| Argument / Option | Description |
|---|---|
connection |
Logical connection name from ConnectionStrings in configuration |
target |
db.schema.table (single table) or db.schema (all tables in schema) |
--output / -o |
console (default) or json (emits suggested check config) |
--no-limit |
Scan the full table; by default the profiler samples up to 1,000,000 rows |
--secretfile / -sf |
Path to secrets file |
--secretpassword / -sp |
Password for secrets file |
# Display column stats for a single table
dotnet run --project src/DataQuality.Cli -- profile Snowflake ANALYTICS.PUBLIC.ORDERS
# Generate suggested check config for every table in a schema
dotnet run --project src/DataQuality.Cli -- profile Snowflake ANALYTICS.PUBLIC --output json > checks.jsonConnections are declared in appSettings.json under ConnectionStrings. The key is the logical name referenced in check definitions via the "connection" property.
{
"ConnectionStrings": {
"Snowflake": "account=myaccount;user=myuser;password=mypassword;warehouse=COMPUTE_WH;db=MYDB;schema=PUBLIC"
}
}Sensitive values (passwords, private keys) must not be committed. Use the secret store, User Secrets locally, or environment variables in CI:
# Local development — .NET User Secrets
dotnet user-secrets set "ConnectionStrings:Snowflake" "account=...;password=..." --project src/DataQuality.Cli
# CI/CD — environment variable
$env:ConnectionStrings__Snowflake = "account=...;password=..."The Polly retry pipeline is configured under Resilience in appSettings.json:
{
"Resilience": {
"MaxRetryAttempts": 3,
"BaseDelaySeconds": 2,
"UseExponentialBackoff": true
}
}| Setting | Default | Description |
|---|---|---|
MaxRetryAttempts |
3 |
Maximum retry attempts before propagating the exception |
BaseDelaySeconds |
2 |
Base delay between retries in seconds |
UseExponentialBackoff |
true |
Doubles the delay on each attempt when true; constant delay when false |
DQ Gate maintains a SQLite database to deduplicate and track the lifecycle of failures:
{
"IncidentStore": {
"DatabasePath": "incidents.db",
"DefaultRenotifyHours": 24
}
}| Setting | Default | Description |
|---|---|---|
DatabasePath |
incidents.db |
Path to the SQLite file. Relative paths resolve from the current working directory. |
DefaultRenotifyHours |
24 |
Hours before an Ongoing incident triggers another notification. |
Per-check override: add "renotify_hours" to any check definition to override the global default.
Logging is configured via the Serilog section in appSettings.json. The default configuration writes to the console. Add a file sink for persistent logs:
{
"Serilog": {
"MinimumLevel": { "Default": "Information" },
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": { "path": "logs/dqgate-.log", "rollingInterval": "Day" }
}
]
}
}JSON files in the config directory (default: Config/). Each file is a flat JSON array of check objects. All files in the directory are merged at startup.
Every check shares these base properties:
| Property | Required | Description |
|---|---|---|
type |
? | Check type discriminator (case-sensitive, must match a registered type) |
name |
? | Human-readable name shown in reports |
connection |
? | Logical connection name from ConnectionStrings |
target |
? | Three-part identifier: database.schema.table |
enabled |
true by default; set to false to disable without removing |
|
tags |
String array for --tags filtering |
|
renotify_hours |
Per-check override for the global DefaultRenotifyHours |
Asserts that a timestamp column's maximum value is within an acceptable age from the current time.
{
"type": "Freshness",
"name": "Orders Fresh",
"connection": "Snowflake",
"target": "SALES.PUBLIC.ORDERS",
"tags": ["daily", "sales"],
"dateColumn": "LAST_UPDATED",
"maxAgeHours": 24,
"warnAgeHours": 12,
"timezone": "America/New_York"
}| Property | Required | Description |
|---|---|---|
dateColumn |
? | Timestamp column to evaluate |
maxAgeHours |
? | Hours since the latest value before the check fails |
warnAgeHours |
Hours before maxAgeHours to emit a warning; must be less than maxAgeHours |
|
timezone |
IANA or Windows timezone ID for relative age calculation (defaults to UTC) |
Detects duplicate value combinations across one or more columns.
{
"type": "MultiColumnUniqueness",
"name": "Customer Key",
"connection": "Snowflake",
"target": "SALES.PUBLIC.CUSTOMERS",
"columns": ["CUSTOMER_NUMBER", "SOURCE_SYSTEM"]
}| Property | Required | Description |
|---|---|---|
columns |
? | One or more column names; the check counts rows where the combination is not unique |
Runs an arbitrary read-only scalar query and optionally asserts the result against expected values.
{
"type": "CustomSql",
"name": "No Negative Sales",
"connection": "Snowflake",
"target": "SALES.PUBLIC.ORDERS",
"query": "SELECT COUNT(*) FROM SALES.PUBLIC.ORDERS WHERE TOTAL < 0",
"operator": "=",
"expectedValue": "0",
"warnOperator": ">",
"warnValue": "-1"
}| Property | Required | Description |
|---|---|---|
query |
? | SQL query returning a single scalar value |
operator |
Comparison operator: =, !=, >, >=, <, <= |
|
expectedValue |
Expected scalar value (requires operator) |
|
warnOperator |
Operator for the warning threshold | |
warnValue |
Value for the warning threshold (requires warnOperator) |
Queries are validated by SqlStatementGuard, which blocks: TRUNCATE, DELETE, DROP, ALTER, UPDATE, INSERT, MERGE, CREATE, REPLACE, EXECUTE, EXEC.
Validates a table's column structure against an inline definition or a baseline file, detecting schema drift.
{
"type": "Schema",
"name": "Orders Schema",
"connection": "Snowflake",
"target": "SALES.PUBLIC.ORDERS",
"allow_extra_columns": true,
"columns": [
{ "name": "ORDER_ID", "data_type": "NUMBER", "nullable": false },
{ "name": "LAST_UPDATED", "data_type": "TIMESTAMP_NTZ", "nullable": true }
]
}| Property | Required | Description |
|---|---|---|
columns |
? (unless baseline_file) |
Expected column definitions with name, data_type, and nullable |
baseline_file |
? (unless columns) |
Path to a JSON baseline file for schema drift comparison |
allow_extra_columns |
When true (default), additional columns in the table do not cause a failure |
At least one of columns or baseline_file must be provided.
Compares the latest partition's row count against historical averages and flags abnormal changes.
{
"type": "VolumeSpike",
"name": "Orders Load Volume",
"connection": "Snowflake",
"target": "SALES.PUBLIC.ORDERS",
"partition_column": "LOAD_DATE",
"threshold_type": "Percent",
"error_threshold": 50,
"warn_threshold": 30,
"direction": "Both"
}| Property | Required | Description |
|---|---|---|
partition_column |
? | Date/timestamp column used to define partitions |
threshold_type |
? | Percent or Absolute |
error_threshold |
? | Deviation magnitude that triggers a failure |
warn_threshold |
Deviation magnitude that triggers a warning | |
direction |
Up, Down, or Both (default: Both) |
|
comparison_mode |
How the baseline is computed (e.g., rolling average) | |
count_column |
Column to count instead of * |
|
history_limit |
Number of prior partitions to include in the baseline |
Detects orphaned child records by validating foreign key relationships against parent tables.
{
"type": "ReferentialIntegrity",
"name": "Orders ? Customers FK",
"connection": "Snowflake",
"target": "SALES.PUBLIC.ORDERS",
"relationships": [
{
"child_columns": ["CUSTOMER_ID"],
"parent_target": "SALES.PUBLIC.CUSTOMERS",
"parent_columns": ["CUSTOMER_ID"]
}
]
}| Property | Required | Description |
|---|---|---|
relationships |
? | One or more relationship objects |
child_columns |
? per relationship | Column(s) on the child (target) table |
parent_target |
? per relationship | Three-part identifier of the parent table |
parent_columns |
? per relationship | Column(s) on the parent table; must match child_columns count |
Executes a COUNT(*) (with an optional WHERE filter) and evaluates the result against configurable thresholds.
{
"type": "RowCount",
"name": "Orders Has Rows",
"connection": "Snowflake",
"target": "SALES.PUBLIC.ORDERS",
"operator": ">",
"expectedValue": "0",
"warnOperator": "<",
"warnValue": "1000",
"filter": "STATUS = 'ACTIVE'"
}| Property | Required | Description |
|---|---|---|
operator |
? | Comparison operator: =, !=, >, >=, <, <= |
expectedValue |
? | Expected row count value |
warnOperator |
Operator for the warning threshold | |
warnValue |
Value for the warning threshold | |
filter |
Optional WHERE clause (do not include the WHERE keyword) |
ReportService dispatches to all registered IReportWriter implementations whose format key matches the requested output formats. Reports are written to the directory specified by --report-dir (default: Reports/). Files older than 30 days are automatically purged on each run.
| Format key | Description |
|---|---|
html |
Interactive HTML report with summary cards and per-check detail (default) |
json |
Machine-readable JSON; includes incident context fields per result |
csv |
Flat CSV; suitable for import into spreadsheets or dashboards |
Request multiple formats on the command line:
dotnet run --project src/DataQuality.Cli -- run --output html json csvAll three formats include per-result incident context: IncidentStatus, IncidentId, IncidentOpenedAt, and IncidentOccurrences.
DQ Gate maintains a lightweight SQLite database that deduplicates repeating failures and tracks their lifecycle, eliminating alert fatigue from repeated notifications for the same underlying issue.
- Each failing or warning result is assigned a stable fingerprint (SHA-256 of check type + target + rule-specific fields). Renaming a check does not change its fingerprint.
- The first failure for a fingerprint opens an Active incident with status
New. - Subsequent failures against the same Active incident advance the status to
Ongoing— no duplicate notification is sent unless the re-notify window (DefaultRenotifyHours) has elapsed. - When a check passes after a prior failure, the Active incident is automatically Resolved and a resolved notification is dispatched.
All three report formats include per-result incident context:
| Field | Values |
|---|---|
IncidentStatus |
New, Ongoing, Resolved, or empty for passing checks |
IncidentId |
UUID of the active incident (empty when passing) |
IncidentOpenedAt |
UTC timestamp when the incident was first opened |
IncidentOccurrences |
Count of consecutive failures against this incident |
Separate from run-level notifications, incident lifecycle events (Opened, EscalationDue, Resolved) are dispatched through IIncidentNotificationChannel implementations, using the same Email/Pushover/Teams credentials configured in the Notifications block.
NotificationService dispatches to all enabled INotificationChannel singletons. Channels are configured under Notifications in appSettings.json. Sensitive values (passwords, tokens) should be loaded from the secret store rather than committed to source control.
All channels share these base properties:
| Property | Description |
|---|---|
enabled |
Set to true to activate the channel |
trigger |
OnFailure (default), Always, or OnSuccess |
{
"Notifications": {
"Email": {
"enabled": true,
"trigger": "OnFailure",
"host": "smtp.example.com",
"port": 587,
"enableSsl": true,
"username": "user",
"password": "${secret:Notifications:Email:Password}",
"from": "noreply@example.com",
"to": ["team@example.com"],
"subject": "DQ Gate – Run Summary",
"includeReport": false
}
}
}| Property | Default | Description |
|---|---|---|
host |
SMTP server hostname | |
port |
587 |
SMTP port |
enableSsl |
true |
Enable TLS/SSL |
username |
SMTP authentication username | |
password |
SMTP authentication password | |
from |
Sender address | |
to |
List of recipient addresses | |
subject |
DQ Gate – Run Summary |
Email subject line |
includeReport |
false |
Attach the HTML report to the email |
{
"Notifications": {
"Pushover": {
"enabled": true,
"trigger": "OnFailure",
"appToken": "${secret:Notifications:Pushover:AppToken}",
"userKey": "${secret:Notifications:Pushover:UserKey}",
"title": "Data Quality Check Failed"
}
}
}{
"Notifications": {
"Teams": {
"enabled": true,
"trigger": "Always",
"webhookUrl": "https://outlook.office.com/webhook/..."
}
}
}DQ Gate integrates with a file-based encrypted secret store to keep credentials out of configuration files and source control.
Provide the path and password at runtime via CLI options or environment variables:
# Via CLI
dotnet run --project src/DataQuality.Cli -- run `
--secretfile ~/.secretstore `
--secretpassword "mypassword"
# Via environment variables (recommended for CI)
$env:SECRETSTORE_PATH = "/run/secrets/.secretstore"
$env:SECRETSTORE_PASSWORD = "mypassword"The default secret file location is ~/.secretstore (the user's home directory). The password defaults to the SECRETSTORE_PASSWORD environment variable when not specified on the command line.
Once the store is loaded, secrets are injected into appSettings.json using the ${secret:...} interpolation syntax. The path after secret: maps to the key hierarchy in the store:
{
"ConnectionStrings": {
"Snowflake": "${secret:ConnectionStrings:Snowflake}"
},
"Notifications": {
"Email": {
"password": "${secret:Notifications:Email:Password}"
}
}
}For local development, prefer .NET User Secrets to avoid maintaining a secret file:
dotnet user-secrets set "ConnectionStrings:Snowflake" "account=...;password=..." `
--project src/DataQuality.CliDQ Gate is designed as a pipeline gate. A non-zero exit code signals failure:
| Code | Meaning |
|---|---|
0 |
All enabled checks passed (warnings do not fail the gate) |
1 |
One or more checks failed |
2 |
One or more checks produced an error (configuration or connection fault) |
- name: Run data quality checks
env:
SECRETSTORE_PATH: ${{ secrets.SECRETSTORE_PATH }}
SECRETSTORE_PASSWORD: ${{ secrets.SECRETSTORE_PASSWORD }}
run: |
dotnet run --project src/DataQuality.Cli -- run \
--config-dir Config/ \
--report-dir Reports/ \
--output html json \
--tags daily
- name: Upload DQ reports
if: always()
uses: actions/upload-artifact@v4
with:
name: dq-reports
path: Reports/- task: DotNetCoreCLI@2
displayName: Run data quality checks
env:
SECRETSTORE_PASSWORD: $(SecretStorePassword)
inputs:
command: run
projects: src/DataQuality.Cli/DataQuality.Cli.csproj
arguments: -- run --tags daily --output html jsonThe profile command connects to Snowflake, samples up to 1,000,000 rows per table (configurable), and collects per-column statistics:
- Null rate
- Distinct value count and ratio
- Minimum and maximum values
- Sample values
With --output json, the profiler passes column stats through ProfileSuggestor, which emits a ready-to-paste JSON check configuration covering freshness, uniqueness, and row count candidates. This is the recommended starting point when onboarding new tables.
# Profile a single table and display stats
dotnet run --project src/DataQuality.Cli -- profile Snowflake MYDB.PUBLIC.ORDERS
# Scan full table (no row limit)
dotnet run --project src/DataQuality.Cli -- profile Snowflake MYDB.PUBLIC.ORDERS --no-limit
# Profile all tables in a schema and generate suggested checks
dotnet run --project src/DataQuality.Cli -- profile Snowflake MYDB.PUBLIC --output json > Config/suggested.jsonEvery check type lives under Features/<CheckName>/ in DataQuality.Core with two files:
| File | Purpose |
|---|---|
<Name>Configuration.cs |
Extends CheckConfiguration; declares [JsonPropertyName] config properties; overrides Validate() and Describe() |
<Name>Check.cs |
Implements ICheckRunner; exposes public const string Key; executes SQL; returns Result<CheckResult> |
Three registration steps are required:
1. Add a [JsonDerivedType] attribute on CheckConfiguration:
[JsonDerivedType(typeof(MyCheckConfiguration), "mychecktype")]2. Register the keyed runner in ServiceCollectionExtensions.AddDataQualityCore():
services.AddKeyedSingleton<ICheckRunner, MyCheck>(MyCheck.Key);3. Ensure MyCheck.Key matches the discriminator string (case-insensitive match via ToLowerInvariant()).
Implement IReportWriter with a public const string Key constant and register it:
services.AddKeyedSingleton<IReportWriter, MyReportWriter>(MyReportWriter.Key);Request it at runtime with --output myformat.
Implement INotificationChannel (and optionally IIncidentNotificationChannel for lifecycle events), bind its options from IOptions<MyChannelConfig>, and register it as a singleton in ServiceCollectionExtensions.
dotnet testThe test project at tests/DataQuality.Tests/ uses xUnit v3, NSubstitute for mocking, and FluentAssertions for readable assertions. Test structure mirrors the source tree: Checks/, Validation/, Orchestration/. DataQuality.Core exposes internals to the test project via InternalsVisibleTo.
| Variable | Used by | Description |
|---|---|---|
DOTNET_ENVIRONMENT |
Host | Selects appSettings.{env}.json (default: Production) |
SECRETSTORE_PATH |
--secretfile default |
Path to the encrypted secret store file |
SECRETSTORE_PASSWORD |
--secretpassword default |
Decryption password for the secret store |
ConnectionStrings__<Name> |
Configuration | Override connection strings directly (useful in containers) |