Configuration Settings¶
How .NET configuration is composed in CargonerdsApp, what each layer is for, and how to change values for a given scenario.
The mechanism lives in src/Cargonerds.ServiceDefaults/. Every host project (Cargonerds.HttpApi.Host, Cargonerds.AuthServer, Cargonerds.Web.Public, Cargonerds.Blazor, Cargonerds.DbMigrator) calls builder.AddServiceDefaults() during startup, which wires up the layered loading and the two token-replacement systems described below.
TL;DR — where to put a value¶
| You want to change… | Edit this file |
|---|---|
| Local default for a single service | src/<Project>/appsettings.json |
| Local dev-only override for a single service | src/<Project>/appsettings.Development.json |
| Aspire-orchestrated values (URLs that depend on running services) | src/<Project>/appsettings.aspire.json |
| White-label appearance, OIDC tenant, third-party API keys | src/Cargonerds.AppHost/appsettings.rohlig.json |
Spark-environment-specific value (local/dev/test/prod/prod-read-only) |
src/<Project>/appsettings.spark.<env>.json |
Personal overrides on your machine (gitignored for local) |
src/<Project>/appsettings.spark.<env>.user.json |
Azure-deployed environment (e.g. dev, dev.hub-prod, prod, prod.staging) |
src/Cargonerds.ServiceDefaults/appsettings.azure.<env>.json |
| Migrator-only Azure overrides | src/Cargonerds.DbMigrator/appsettings.azure.migrator.<env>.json |
Reusable parameter substituted into other values via {token} |
ConfigurationParameters section of the relevant azure file |
After choosing a file, restart the affected host (or rebuild the AppHost graph) — only Spark JSON files are loaded with reloadOnChange: true.
What the environment axes mean¶
The loading pipeline combines several layers, but two axes carry most of the per-deployment meaning: Spark and Azure. They are orthogonal — each answers a different question — and understanding the split is the fastest way to decide where a value belongs.
Spark environment — which external backend am I talking to?¶
Controlled by SPARK_ENVIRONMENT (one of local, dev, test, prod, prod-read-only; default dev). The matching appsettings.spark.<env>.json file pins the connections and identifiers that define the external data stack:
ConnectionStrings:Hub— the Cargonerds hub SQL database (each Spark env has its own server and database:sql-cargonerds-db-gerwc-development,…-test,…-production, etc.).ConnectionStrings:BlobStorage— the Azure Storage account (e.g.cnhubgerwcdevelopment,cnhubgerwctest,cnhubgerwcproduction) used for file uploads and derived artifacts.ConnectionStrings:hubServiceBus— the Azure Service Bus namespace (e.g.cn-hub-gerwc-development) used for inter-service messaging.App:CargonerdsCustomerId— the tenant identifier that scopes data access.App:BookingNumberPrefixand other domain values that differ per backend.- (Optionally) third-party API overrides under
WhiteLabelingOptions:ExternalApiswhen a tenant-specific key is needed.
Concretely:
| Spark env | Hub DB | Blob storage | Intended use |
|---|---|---|---|
local |
localhost SQL on :1401 |
Azurite (devstoreaccount1) |
Fully offline development with the Aspire stack. |
dev |
sql-cargonerds-db-gerwc-development |
cnhubgerwcdevelopment |
Shared development backend. |
test |
sql-cargonerds-db-gerwc-test |
cnhubgerwctest |
QA / UAT. |
prod |
sql-cargonerds-db-gerwc-production (read-write user) |
cnhubgerwcproduction |
Live production. |
prod-read-only |
same production DB with a read-only SQL user | cnhubgerwcproduction |
Production backend without write risk — used by the prod.staging Azure slot and for prod investigations. |
The same Spark env is often used by multiple running instances. That's the whole reason the Spark layer is separate from the Azure layer: the backend (DB, storage, bus) is a property of a data stack; the front-of-cluster config (which app URL, which tenant domain, which white-label) can vary per deployment while pointing at the same backend. For example, both appsettings.azure.dev.json and appsettings.azure.dev.hub-prod.json set SPARK_ENVIRONMENT but to different values (dev vs. prod-read-only) — one is a real dev instance, the other is a staging copy of prod running on dev infrastructure.
Only Cargonerds.HttpApi.Host currently ships per-Spark-env files, because it's the service that owns the Hub-side connections; other hosts pick up their Spark env via the SparkEnvironment singleton and environment-variable propagation.
Azure environment — which App Service shape am I impersonating?¶
Controlled by AZURE_ENVIRONMENT. Unset by default. When set, the progressive azure layers kick in (see the loading pipeline section below). The files in src/Cargonerds.ServiceDefaults/appsettings.azure*.json hold the deployment-shape values:
ConnectionStrings:Default— the ABP application database (separate from the Hub DB) for this deployment.ConnectionStrings:redis/ConnectionStrings:messaging— the Redis cache and RabbitMQ instance for this deployment.Services:<name>:https— the public URLs that{service-name}tokens resolve to at runtime. In the azure layer these are real hostnames (e.g.https://api.rt3.rohlig.com,https://api.dev.spark.cargonerds.dev) instead of the localhost Aspire endpoints.ConfigurationParameters— the static values (spark-dev-domain,spark-db-name,rabbit-mq-port) that get stamped into the connection strings and URLs above via{token}substitution.SPARK_ENVIRONMENT— each azure file pins its expected backend, which is howprod.stagingcan point atprod-read-onlywhileproduses live prod.
Primary use: deployed Azure App Services. Each App Service sets AZURE_ENVIRONMENT to the dotted name of its slot (dev, dev.hub-prod, prod, prod.staging, …), and the host composes the right layer stack automatically.
Secondary use: debug locally with production-shape neighbors. If you set AZURE_ENVIRONMENT on your machine you do not need to run the full Aspire stack. Start only the one service you want to debug; the service-discovery rewriter will resolve {api}, {auth}, {realtime}, {web}, {admin} against the Services block from the azure files — i.e. against the live deployed URLs. This is the recommended way to step through a single service while it interacts with a realistic set of neighbors.
Caveats
- Never set
AZURE_ENVIRONMENT=prod(or anyprod.*value) unless you are absolutely sure. Your local process will connect to production databases, storage, and service bus, and any write you trigger hits real customer data. Preferdevorprod.staging(which usesprod-read-only) for investigation.- If you start more than one local service with
AZURE_ENVIRONMENTset, they do not talk to each other — they each independently talk to the corresponding deployed App Service, because{service}tokens resolve to the public URLs, not to your localhost ports. This is usually fine (and often the point), but don't expect cross-service traffic between two locally running hosts in this mode.USE_ASPIRE_CONFIGandAZURE_ENVIRONMENTcan be set at the same time, but then{service}tokens inappsettings.aspire.jsonare re-resolved against theServicesentries from the azure layer (the rewriter runs once, last). If you want local Aspire wiring, leaveAZURE_ENVIRONMENTunset.
The loading pipeline¶
AddServiceDefaults() calls (in order) AddExtraConfigFiles() and AddServiceDiscoveryConfiguration(). The full effective ordering, including the framework defaults that already loaded before AddServiceDefaults runs, is:
-
Framework defaults (always loaded by
Host.CreateApplicationBuilderorWebApplication.CreateBuilder):appsettings.jsonappsettings.{ASPNETCORE_ENVIRONMENT}.json— typicallyDevelopment/Production- User Secrets (only in
Development) - Environment variables
- Command-line args
-
Aspire layer — only when
USE_ASPIRE_CONFIG=true:appsettings.aspire.json
-
Azure layers — only when
AZURE_ENVIRONMENTis set:appsettings.azure.json- For each dot-separated segment of
AZURE_ENVIRONMENT, append progressively:- e.g.
AZURE_ENVIRONMENT=dev.hub-prodloadsappsettings.azure.dev.jsonthenappsettings.azure.dev.hub-prod.json.
- e.g.
- (DbMigrator only) the same progression with prefix
azure.migrator, e.g.appsettings.azure.migrator.dev.json. ConfigurationParameterReplacementSource— re-reads everything loaded so far and rewrites any value containing a{key}token by looking it up in theConfigurationParameterssection.
-
White-label layer:
- Path is
WHITE_LABEL_SETTINGS_PATHif set, otherwiseappsettings.rohlig.json(relative to the host's content root).
- Path is
-
Spark layer:
SPARK_ENVIRONMENTenv var (defaultdev; valid:local,dev,test,prod,prod-read-only) is parsed into aSparkEnvironmentand registered as a singleton service.appsettings.spark.<env>.json(reloadOnChange: true)appsettings.spark.<env>.user.json(reloadOnChange: true) — developer-local overrides; only thelocalvariant is in.gitignore.
-
Service-discovery rewriter —
ServiceDiscoveryConfigurationSourcerewrites any value containing{service-name}using:services:<name>:https:0— Aspire service endpoint (HTTPS)services:<name>:http:0— Aspire service endpoint (HTTP)ConnectionStrings:<name>— named connection string
Each later layer wins. Within the framework's standard chain, environment variables override JSON files. Within the custom layers, the order above is the precedence.
See: src/Cargonerds.ServiceDefaults/HostApplicationBuilderExtensions.cs:54 for AddExtraConfigFiles, and :163 for AddAzureJsonFiles.
Driving environment variables¶
| Variable | Read by | Effect | Default |
|---|---|---|---|
ASPNETCORE_ENVIRONMENT |
Framework | Picks appsettings.{Env}.json and toggles User Secrets. |
Production |
USE_ASPIRE_CONFIG |
AddExtraConfigFiles |
Loads appsettings.aspire.json when true. AppHost sets it via AddProjectWithDefaults. |
false |
AZURE_ENVIRONMENT |
AddExtraConfigFiles, GetAzureEnvironment() |
Triggers the Azure layered load; the dotted value drives which files are appended. | unset |
SPARK_ENVIRONMENT |
AddExtraConfigFiles, SparkEnvironment.FromName |
Picks appsettings.spark.<env>.json and registers SparkEnvironment. |
dev |
WHITE_LABEL_SETTINGS_PATH |
AddExtraConfigFiles |
Absolute or relative path to white-label JSON. AppHost passes the absolute path during run mode (WithWhiteLabelSettingsPath). |
appsettings.rohlig.json |
DEPLOYMENT_DOMAIN |
AppHost (ResourceBuilderExtensions) |
Hostname suffix used in publish mode for services__* env vars. |
required in publish mode |
OTEL_EXPORTER_OTLP_ENDPOINT |
AddOpenTelemetryExporters |
Enables the OTLP exporter when non-empty. | unset |
APPLICATIONINSIGHTS_CONNECTION_STRING |
AddOpenTelemetryExporters |
Enables Azure Monitor when non-empty. | unset |
Token replacement — two systems with the same syntax¶
Both run as IConfigurationSources and both look like {name} — but they have different lookup tables and run at different points.
1. ConfigurationParameters (static parameter inheritance)¶
Source: ConfigurationParameterReplacementProvider.cs. Runs at the end of the Azure layer load.
Use this to keep multi-layer azure files DRY. Define a value once, reference it everywhere:
// appsettings.azure.dev.json
{
"ConnectionStrings": {
"Default": "Server=tcp:sql-spark-dev.database.windows.net,1433;Initial Catalog={spark-db-name}",
"messaging": "amqp://user:pwd@rabbitmq.dev.spark.cargonerds.dev:{rabbit-mq-port}/"
},
"ConfigurationParameters": {
"spark-db-name": "spark-dev",
"rabbit-mq-port": 5672
}
}
A nested layer can override just the parameter to retarget every reference:
// appsettings.azure.dev.hub-prod.json
{
"ConfigurationParameters": {
"spark-db-name": "spark-dev-hub-prod",
"rabbit-mq-port": 5673
}
}
If a token has no matching parameter, the literal {name} remains in the value. If a parameter is neither matched by a Configuration Parameter nor by Service Discovery (next step), an error will be thrown and the app will shut down.
2. Service-discovery tokens (runtime endpoint resolution)¶
Source: ServiceDiscoveryConfigurationProvider.cs. Runs last, after every layer is loaded.
Use this in any value that should resolve to a sibling service's URL or a named connection string. The canonical place is appsettings.aspire.json:
// appsettings.aspire.json (HttpApi.Host)
{
"App": {
"SelfUrl": "{api}",
"CorsOrigins": "{realtime},{admin}"
},
"AuthServer": {
"Authority": "{auth}"
},
"Redis": {
"Configuration": "{redis}"
}
}
Lookup order for {name}:
services:<name>:https:0— Aspire injects this viaservices__<name>__https__0env vars fromWithServiceReference()in the AppHost.services:<name>:http:0ConnectionStrings:<name>
If none match, the host throws on startup with a list of available service names — failing fast prevents silent misconfiguration.
Note:
appsettings.azure.*.jsonfiles use theConfigurationParametersmechanism (static), whileappsettings.aspire.jsonuses service-discovery (runtime). The braces look identical but the resolution scope is different — keep that in mind when copying snippets between files.
How AppHost composes the variables¶
Cargonerds.AppHost controls which env vars each project receives. Key extension methods in src/Cargonerds.AppHost/Extensions/ResourceBuilderExtensions.cs:
AddProjectWithDefaults<T>()— wrapsAddProjectand setsUSE_ASPIRE_CONFIG=trueso the project picks upappsettings.aspire.json.WithSparkEnvironmentName()— propagates the AppHost's ownSPARK_ENVIRONMENTenv var down to the child project.WithWhiteLabelSettingsPath()— in run mode, setsWHITE_LABEL_SETTINGS_PATHto the absolute path ofappsettings.rohlig.jsonso the child finds it regardless of working directory.WithServiceReference(target)— emitsservices__<name>__https__0so the child can resolve{name}tokens. In publish mode it useshttps://<name>.<DEPLOYMENT_DOMAIN>instead of the local Aspire endpoint.WithEnvironmentHttpsEndpoint(...)— same dual run/publish behavior for one-off env vars that must hold a sibling URL.
Recipes¶
Debug one service locally against a deployed Azure environment¶
Use this to step through a single host while it interacts with the real dev (or staging) neighbors — without spinning up the Aspire graph.
- Pick a non-production Azure env (e.g.
dev, orprod.stagingwhich is backed byprod-read-only). Do not useprodor a bareprod.*name unless you have a specific, verified reason. - Launch just the project you want to debug (from your IDE or
dotnet run --project src/Cargonerds.HttpApi.Host) with:AZURE_ENVIRONMENT=dev(or the env you chose)ASPNETCORE_ENVIRONMENT=Developmentif you want dev-friendly logging
- Do not start the AppHost. Skip
USE_ASPIRE_CONFIG. The azure layer provides both the backend connections and theServicesblock, so{api},{auth},{realtime},{web},{admin}resolve to the public URLs. - If you also start a second local service with the same
AZURE_ENVIRONMENT, note that they won't see each other — each still points at the deployed sibling. That's expected.
Add a new Azure deployment environment¶
- Pick a dotted name, e.g.
dev.hub-test. - Create
src/Cargonerds.ServiceDefaults/appsettings.azure.dev.hub-test.jsonwith only the values that differ from the parent layer (appsettings.azure.dev.json). Typically that's a fewConfigurationParametersoverrides and any per-tenantServicesentries. - Set
AZURE_ENVIRONMENT=dev.hub-teston the deployment target. - (Optional) If the migrator needs different settings, add
src/Cargonerds.DbMigrator/appsettings.azure.migrator.dev.hub-test.json.
Override a value just on your machine¶
- Create
src/<Project>/appsettings.spark.local.user.json(gitignored). - Put the override under the right section.
- Run
dotnet run --project src/Cargonerds.AppHostwithSPARK_ENVIRONMENT=local.
Point a service at a different sibling URL when running under Aspire¶
Edit appsettings.aspire.json in the consuming project and add the token:
Then ensure the AppHost wires the reference: consumer.WithServiceReference(web). The service-discovery rewriter resolves {web} to the live Aspire endpoint in run mode and to https://web.<DEPLOYMENT_DOMAIN> in publish mode.
Add a parameter shared across azure layers¶
- In the most general layer that should define a default (often
appsettings.azure.jsonorappsettings.azure.<top>.json), add the value underConfigurationParameters. - Reference it as
{my-parameter}anywhere in any value of any azure file. - Override it in nested layers (
appsettings.azure.<top>.<child>.json) by re-declaring the same key underConfigurationParameters.
Switch white-label tenant locally¶
Set WHITE_LABEL_SETTINGS_PATH to a different file (e.g. appsettings.othertenant.json) before launching, or change the file referenced by WithWhiteLabelSettingsPath() in the AppHost.
Reading values in code¶
src/Cargonerds.Domain.Shared/Extensions/ConfigurationExtensions.cs exposes typed helpers as C# extension members:
configuration.GetRequiredValue<T>("Section")— throws if the section can't bind toT.configuration.GetAppConfiguration()— binds theApp:section toAppConfiguration.configuration.GetRequiredConnectionString("Default")— throws if missing.configuration.GetAzureEnvironment()— returns the currentAZURE_ENVIRONMENT(nullable).
For the Spark environment, inject SparkEnvironment directly — it's registered as a singleton during AddExtraConfigFiles().
Source map¶
| Concern | File |
|---|---|
| Pipeline entry point | src/Cargonerds.ServiceDefaults/HostApplicationBuilderExtensions.cs |
| Azure layer expansion | same file, ConfigurationBuilderExtensions.AddAzureJsonFiles |
{key} parameter rewriter |
src/Cargonerds.ServiceDefaults/ConfigurationParameterReplacementProvider.cs |
{service-name} discovery rewriter |
src/Cargonerds.ServiceDefaults/ServiceDiscoveryConfigurationProvider.cs |
| Spark environment enum | modules/hub/src/Hub.Domain.Shared/Consts/SparkEnvironment.cs |
| AppHost env-var wiring | src/Cargonerds.AppHost/Extensions/ResourceBuilderExtensions.cs |
| Typed config helpers | src/Cargonerds.Domain.Shared/Extensions/ConfigurationExtensions.cs |