Every configuration key a scaffolded service reads, what it changes and when the change takes effect.
The FeatureManagement flags are the one group this page does not list: they have their own table in
Template features, which is the source of truth for
the class default and each environment overlay. Neither sample feature reads a key of its own, so
this page is the whole configuration surface.
Project names below are the template’s own (Minimal.Api, Minimal.Infra, …). A generated solution
renames them to <YourApp>.*.
The API is a stock WebApplication.CreateBuilder(args) host, so the standard ASP.NET Core order
applies — later sources win:
Minimal.Api/appsettings.json — the base file. This is what a deployed service runs with,
because the template ships no appsettings.Production.json.Minimal.Api/appsettings.{Environment}.json — Development and Testing overlays ship; both
are copied into the scaffolded solution.Development only.: with __
(FeatureManagement:RequireAuthorization → FeatureManagement__RequireAuthorization). These
outrank every JSON file.FeatureManagement:EnableAzureAppConfig is on and its connection
string resolves. AzureAppConfigSetup appends it to builder.Configuration after
WebApplication.CreateBuilder(args) has already added every source above, so it wins over the
JSON files and over environment variables and command-line arguments.One exception matters for tests. Program.cs binds FeatureOptions on its first lines, before a
WebApplicationFactory’s ConfigureAppConfiguration overrides are merged. A test fixture therefore
cannot flip a feature flag with an in-memory entry — only an appsettings.{Environment}.json file or
a FeatureManagement__<Flag> environment variable lands early enough.
Every key on this page is read while the host starts: change it, then restart the service. The
Takes effect column says startup for that reason.
ConnectionStrings| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
ConnectionStrings:AppDb |
string | yes | "" in the base file and the Development overlay |
an Npgsql connection string | yes — your secret store (Aspire injects it on local runs) | startup | The PostgreSQL connection for CoreDbContext. Read by InfraSetup.AddInfraServices and by the start-up migration in DbMigration. Left empty with FeatureManagement:RunDbMigrationWhenAppStart on (the Development overlay), the start-up migration fails with The ConnectionString property has not been initialized and the host exits; with the flag off (the base file) the host starts and the first database call fails. Supplied automatically when you launch through the Aspire host, which injects it from the AppDb resource. |
ConnectionStrings:Redis |
string | no | not shipped in any file | a StackExchange.Redis connection string | yes — your secret store (Aspire injects it on local runs) | startup | Selects the distributed-cache store and the idempotency-key store. Set → AddStackExchangeRedisCache plus AddIdempotencyWithRedisStore. Unset → AddDistributedMemoryCache plus the in-process AddIdempotentKey() fallback, which is correct only for a single instance. Read by CacheConfig and AppConfig. |
ConnectionStrings:AzureBus |
string | no | "" in the Development overlay; absent from the base file |
an Azure Service Bus namespace connection string | yes — your secret store | startup | Non-empty and FeatureManagement:EnableServiceBus true is what adds the AzureBus child bus in ServiceBusSetup.AddServiceBus; either one missing leaves external messaging off while in-memory dispatch keeps working. |
ConnectionStrings:AzureAppConfig |
string (URI) | no | "" in the base file |
the App Configuration endpoint URI | no — an endpoint, not a credential: access goes through DefaultAzureCredential |
startup | Looked up under the name in AzureAppConfiguration:ConnectionStringName. Without it the integration silently no-ops even with the flag on. |
AuthenticationBound by ASP.NET Core’s own AddJwtBearer() configuration binding, which reads
Authentication:Schemes:<SchemeName>. AuthConfig calls AddAuthentication().AddJwtBearer() with no
inline options, so this section is the whole configuration surface for token validation. The block is
registered only when FeatureManagement:RequireAuthorization is true.
| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
Authentication:DefaultScheme |
string | yes, with RequireAuthorization on |
Bearer |
a registered scheme name | no | startup | The scheme used when an endpoint names none. |
Authentication:Schemes:Bearer:MetadataAddress |
string (URL) | yes, with RequireAuthorization on |
https://login.microsoftonline.com/00000000-…/v2.0/.well-known/openid-configuration — placeholder |
an OIDC discovery document URL | no | startup | Where the scheme fetches its signing keys. Rewritten by the --TenantId scaffold parameter. |
Authentication:Schemes:Bearer:ValidAudiences |
string array | yes, with RequireAuthorization on |
[ "api://your-api" ] — placeholder |
exactly one entry by design | no | startup | The audiences a token may carry; a token minted for any other resource is rejected. Rewritten by --ApiAudience. |
Authentication:Schemes:Bearer:ValidIssuer |
string (URL) | yes, with RequireAuthorization on |
https://sts.windows.net/00000000-…/ — placeholder |
the issuer your tenant signs with | no | startup | The issuer a token must declare. Rewritten by --TenantId. |
Signature validation is never disabled: JwtSignatureValidationTests in
tests/DKNet.Templates.ScaffoldTests/ fails the build if the API source ever turns it off.
Two extension seams sit next to this section and are registered with it, both marked TODO in source
and both meant to be replaced: SampleClaimsTransformation (an IClaimsTransformation) and the
SampleScopePolicy policy backed by HasScopeRequirement/HasScopeHandler. Neither is applied to any
shipped route. See Extension points.
FeatureManagement:EnableDemoAuthentication (see its row in
Template features) registers a "Demo" authentication
scheme, DemoAuthenticationHandler in DemoAuthConfig, only while RequireAuthorization is false.
Every caller is authenticated as one fixed, self-evidently fake identity: ClaimTypes.Name =
SharedConsts.DemoAccount ("demo-user@not-a-real-identity.invalid", on the .invalid TLD reserved by
RFC 2606 so it can never resolve) and ClaimTypes.NameIdentifier = SharedConsts.SystemAccount.
RequireAuthorization and EnableDemoAuthentication are mutually exclusive: both true throws
InvalidOperationException from AppConfig.AddAppConfig at start-up. Development and demonstration
only — never enable this flag in a deployed service.
CorsRead by CorsConfig in the DKNet.AspCore.Extensions package (DKNet.AspCore.Extensions.Hosting).
| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
Cors:AllowedOrigins |
string array | no | []; the Development overlay ships [ "http://localhost:3000", "http://localhost:5173" ] |
absolute origins: scheme included, no trailing slash, no path | no | startup | Deny-by-default allow-list. Empty or all-blank → neither AddCors nor UseCors is registered, so no Access-Control-Allow-* header is emitted. Non-empty → a default policy allowing exactly those origins, the methods and headers below, and nothing else. Credentials are never allowed on any path. |
Cors:AllowedMethods |
string array | no | [ "GET", "POST", "PUT", "PATCH" ] (base file and package default) |
HTTP method names | no | startup | The methods reflected in Access-Control-Allow-Methods. DELETE is deliberately absent: add it if a browser front-end needs it. An entry not listed is never reflected, so a preflight for it fails. |
Cors:AllowedHeaders |
string array | no | [ "Authorization", "Content-Type", "Accept", "X-Idempotency-Key" ] |
header names | no | startup | The request headers reflected in Access-Control-Allow-Headers. X-Idempotency-Key is there because the purchase-order create route requires it. No tracing header (traceparent, X-Request-Id, …) is listed — add yours if your front-end sends one. |
This is a plain configuration array, not a FeatureManagement flag; the empty array is its off switch,
and it gates the other two keys as well — with no origin listed, CORS is not wired and the method and
header lists are never consulted. AllowedMethods and AllowedHeaders fall back to the lists above
only when the key is absent. A key present but empty ([]) is honoured as “nothing allowed”
rather than widened back to the default, so a preflight for any method (or header) then fails.
SecurityRead by ForwardedHeadersConfig in DKNet.AspCore.Extensions, and only when
FeatureManagement:EnableForwardedHeaders is true.
| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
Security:TrustedProxies |
string array | behind a proxy, this or TrustedNetworks |
[] |
each entry a single literal IPv4 or IPv6 address (IPAddress.Parse); a CIDR range fails start-up with a FormatException |
no | startup | The proxies whose X-Forwarded-For / X-Forwarded-Proto the service believes, by exact address. Non-empty → XForwardedFor | XForwardedProto are applied, but only when the immediate peer is one of the listed addresses. |
Security:TrustedNetworks |
string array | behind a proxy, this or TrustedProxies |
[] |
each entry a CIDR range (IPNetwork.Parse); a malformed entry fails start-up |
no | startup | The same trust expressed as a range, for a proxy whose address is not fixed (a Kubernetes ingress, Container Apps). Either key alone enables forwarded headers; both together are both honoured. |
Empty — as shipped — means no forwarded information is honoured at all: with both lists empty the
module sets ForwardedHeaders.None, so Connection.RemoteIpAddress stays the immediate peer and rate
limiting partitions on it. This is the one pair of keys a production host behind an ingress, load
balancer or CDN must supply — until one of them does, every request appears to come from that
ingress and shares one rate-limit partition. A proxy with a fixed address goes in TrustedProxies, one
entry per proxy:
"Security": {
"TrustedProxies": [ "10.0.0.4", "10.0.0.5" ],
"TrustedNetworks": []
}
A proxy whose address moves goes in TrustedNetworks as a CIDR range instead:
"Security": {
"TrustedProxies": [],
"TrustedNetworks": [ "10.244.0.0/16" ]
}
KnownProxies and KnownIPNetworks are cleared before either list is applied, so ASP.NET Core’s
seeded loopback entry is gone too: 127.0.0.1 is trusted only if you list it. Turning the flag off and
leaving both lists empty are equivalent in effect; the flag exists so the middleware can be taken out of
the pipeline entirely for local work.
HttpsRead by HttpsConfig in DKNet.AspCore.Extensions, and only when FeatureManagement:EnableHttps is
true (the base file’s value; false in the Development and Testing overlays).
| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
Https:HstsMaxAgeDays |
int | no | 365 (package default and base file) |
days | no | startup | The max-age announced by Strict-Transport-Security. Preload is requested only when this value is at least 365 — the preload list’s minimum — and is otherwise switched off, so a shortened max-age never announces preload it cannot qualify for. IncludeSubDomains is always on. |
Lowering it is the safe way to try HSTS on a domain you are not ready to commit for a year —
"HstsMaxAgeDays": 1 announces one day and no preload. The header has exactly one owner: the
security-headers middleware deliberately does not emit Strict-Transport-Security, so it is never sent
twice.
RequestBoundsBound to RequestBoundsOptions by RequestBoundsConfig in DKNet.AspCore.Extensions, and applied
only when FeatureManagement:EnableRequestBounds is true (default true; false in the
Development overlay). The template states all three bounds rather than inheriting Kestrel’s, so a
generated service is bounded with no configuration supplied.
| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
RequestBounds:RequestTimeoutSeconds |
int | no | 30 (class default and base file) |
seconds | no | startup | The default request-timeout policy’s lifetime. A request still running when it elapses is answered 504 Gateway Timeout. Raise it for a long-running endpoint, or opt that endpoint out with .DisableRequestTimeout(). Replaces: none — ASP.NET Core caps request lifetime only if you ask it to. |
RequestBounds:MaxRequestBodySizeBytes |
long | no | 1048576 (1 MB) |
bytes; null is not expressible, so there is no “unlimited” value through configuration |
no | startup | KestrelServerOptions.Limits.MaxRequestBodySize. A larger body is rejected with 413 Payload Too Large. Raise it for file upload. Replaces Kestrel’s ~30 MB. |
RequestBounds:RequestHeadersTimeoutSeconds |
int | no | 10 |
seconds | no | startup | How long Kestrel waits for the complete request headers before dropping the connection — the slow-headers bound. Replaces Kestrel’s 30 s. |
Set only the keys you want to change; a partially specified section keeps the class default for the
rest. The same module also sets KestrelServerOptions.AddServerHeader = false, which is not
configurable: no response names the web-server product. UseRequestTimeouts() is registered after
UseRouting(), as ASP.NET Core requires, so the timeout applies to endpoint execution — a request that
is both oversized and slow is rejected on size first, at the server, before the timeout policy is
reached.
RateLimitBound to RateLimitOptions by RateLimitConfig in DKNet.AspCore.Extensions, and applied only when
FeatureManagement:EnableRateLimit is true. The limiter is a chained PartitionedRateLimiter: a
fixed-window limiter and a concurrency limiter, both partitioned by the same key.
| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
RateLimit:DefaultRequestLimit |
int | no | base file 100; Development overlay 1; class default 2 |
requests per window, per partition | no | startup | PermitLimit on the fixed-window limiter. |
RateLimit:DefaultConcurrentLimit |
int | no | base file 20; Development overlay 1; class default 2 |
in-flight requests, per partition | no | startup | PermitLimit on the concurrency limiter. |
RateLimit:TimeWindowInSeconds |
int | no | base file 1; Development overlay 10; class default 1 |
seconds | no | startup | The fixed window’s length. |
Both limiters use QueueLimit = 0, so an over-limit request is rejected immediately with
429 Too Many Requests rather than queued. The partition key is the signed-in user’s name, else the
remote IP address, else one shared bucket — so unauthenticated callers are limited per IP and
authenticated callers per user. That remote IP is Connection.RemoteIpAddress after the
forwarded-headers middleware has had its say, which is why
Security:TrustedProxies / Security:TrustedNetworks matter to rate limiting: list your
ingress and each client behind it gets its own budget; leave both empty and they all share the
ingress’s. The key never comes from X-Forwarded-For or Host, so a peer that is not a trusted proxy
cannot claim another client’s identity and spend its budget.
The base file must carry this section explicitly, because the class defaults are 2 requests per second
— an outage, not a rate limit. SecureDefaultAppSettingsTests in tests/DKNet.Templates.ScaffoldTests/
asserts it stays there. The shipped production numbers are a placeholder ceiling to tune, not a
researched limit. The per-caller limits are replaceable: register your own IRateLimitOptionsProvider
— see Extension points.
AzureAppConfigurationBound to AzureAppConfigOptions and read only when FeatureManagement:EnableAzureAppConfig is true.
The connection is opened with DefaultAzureCredential, so the host needs a managed identity or a local
Azure login with read access to the store.
| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
AzureAppConfiguration:ConnectionStringName |
string | no | AzureAppConfig (class default; not in the base file) |
a ConnectionStrings entry name |
no | startup | Which ConnectionStrings entry holds the App Configuration endpoint URI. |
AzureAppConfiguration:Label |
string | no | "" in the base file; blank falls back to SharedConsts.ApiName (Minimal.Api) |
— | no | startup | The label filter applied with Select(KeyFilter.Any, label), so only values labelled for this API are loaded. |
AzureAppConfiguration:CacheExpirationInSeconds |
int | no | 300 |
seconds; zero or negative falls back to 300 |
no | startup | The refresh interval registered for every loaded key (ConfigureRefresh(...).SetRefreshInterval(...)). |
AzureAppConfiguration:LoadFeatureFlags |
bool | no | true |
— | no | startup | true → feature flags are loaded from the store; false → every .appconfig.featureflag/ setting is dropped, including flags the label selector fetched. |
AzureAppConfiguration:FeatureFlagPrefix |
string | no | "" |
matched with an ordinal prefix test; \, * and , are escaped for the store filter |
no | startup | Non-blank → only feature flags whose name starts with it are loaded. Blank → all flags. |
LogConfigs.AddLogConfig runs before anything else in Program.cs. When
FeatureManagement:EnableOpenTelemetry is false (the shipped default) it returns without registering
any telemetry — no tracing, no metrics, no exporter — and the service keeps logging through the ASP.NET
Core pipeline under Logging:LogLevel:*. When true it clears the logging providers, adds the
OpenTelemetry logger, and registers ASP.NET Core plus HttpClient tracing and metrics instrumentation.
The console exporter follows the environment the service runs in, not the configuration it was
built in. With telemetry enabled and ASPNETCORE_ENVIRONMENT=Development, traces and metrics are
written to the console from a Release build exactly as from a Debug one; under Production neither
build writes any to the console, so a deployed service exports only to the OTLP or Azure Monitor
destinations below. Application logging is not affected either way.
| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
string (URL) | no | http://localhost:4317 in the base file |
— | no | startup | Non-blank → UseOtlpExporter() is added. The template reads the key only as a presence check; the exporter resolves its own endpoint through the OpenTelemetry SDK’s standard configuration for this key. |
AzureMonitor:ConnectionString |
string | no | "" in the base file |
an Application Insights connection string | yes — your secret store | startup | Non-blank → UseAzureMonitor() is added, shipping traces, metrics and logs to Application Insights. Blank, as shipped, means no Azure Monitor exporter. |
Logging:LogLevel:* |
string | no | Default: Information, Microsoft: Warning, Microsoft.Hosting.Lifetime: Warning; the Development overlay drops to Debug/None/None |
a LogLevel name |
no | startup | Standard ASP.NET Core log filtering. EnableOpenTelemetry clears the providers, so these filters then apply to the OpenTelemetry logger. |
Both exporter keys are additive: set both and both exporters run.
SampleDataThe one key on this page that the API never reads. It lives in Minimal.AppHost/appsettings.json — the
Aspire application host’s own configuration — and is read by Minimal.AppHost/AppHost.cs, which passes
it to SampleDataGenerator. The application host orchestrates a local development environment and is
not part of what gets published, so this key has no effect on, and no presence in, a deployed service.
| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
SampleData:RecordsPerEntity |
int | no | 10000 |
0 or negative turns generation off |
no | startup of the Aspire host | How many records to generate per sample entity (Product and PurchaseOrder) once the application host has started its resources. 0 or less skips generation without opening a database connection and leaves the database exactly as the migration left it — that is how you get an empty database to test empty-state behaviour; the volume is the switch. |
Generation writes into the schema the API’s start-up migration creates, so with
FeatureManagement:RunDbMigrationWhenAppStart off there
is no schema to write into: generation polls for up to 60 seconds waiting for it — the host process
itself starts immediately, only sample-data generation waits — then gives up, and a warning in the
host’s logs names that flag, alongside the last poll failure observed, as the likely reason. Generation
never accumulates into a database that already holds sample rows: products are skipped if the table
holds any row, purchase orders if it holds more than the three the migration seeds, so a database you
chose to keep is skipped rather than topped up and no row you created by hand is ever deleted or
modified. What generation produces: Template features.
| Key | Type | Required | Default | Rules | Secret | Takes effect | Effect |
|---|---|---|---|---|---|---|---|
AllowedHosts |
string | no | "*" in the Development overlay only; absent from the base file, where the framework default (*) applies |
;-separated host names |
no | startup | ASP.NET Core’s host-filtering middleware. Front a deployed service with an ingress that enforces the host, or set this explicitly. |
Each of these appears in a shipped appsettings*.json and is bound by nothing. Setting them changes no
behaviour.
| Key | File | Why it is dead |
|---|---|---|
OTEL_SERVICE_NAME |
base appsettings.json |
No template code reads it. The OpenTelemetry SDK resolves the service name from the environment variable of the same name, not from this configuration entry. |
ApplicationInsights:InstrumentationKey |
appsettings.Development.json |
Azure Monitor is wired from AzureMonitor:ConnectionString; instrumentation keys are not read anywhere. |
Any key above can be set as an environment variable by replacing : with __:
export FeatureManagement__RequireAuthorization=false
export ConnectionStrings__AppDb="Host=localhost;Username=postgres;Password=<password>;Database=AppDb"
export RateLimit__DefaultRequestLimit=500
export Cors__AllowedOrigins__0="https://app.example.com"
Environment variables outrank every JSON file and — unlike an in-memory test override — are in place
before Program.cs binds FeatureOptions. The one source that outranks them is Azure App
Configuration, which is appended after the host builder has already read the environment.