Every configuration key a scaffolded solution reads, what it means, what it defaults to, what it
changes, and the code path that reads it. Feature flags are the one section this page does not
restate — they have their own table in
template-features.md, which is the source of
truth for class default versus each environment overlay.
Paths below use the template’s own project names (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.Section__Key with a double underscore, e.g.
FeatureManagement__RequireAuthorization=false. These outrank every JSON file.FeatureManagement:EnableAzureAppConfig is on and its connection
string resolves. Minimal.Api/Configs/AzureAppConfig/AzureAppConfigSetup.cs 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. Minimal.Api/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.
ConnectionStrings| Key | Type | Shipped default | Effect | Read by |
|---|---|---|---|---|
AppDb |
string | "" in the Development overlay; absent from the base file |
The PostgreSQL connection for CoreDbContext. Without it the API cannot open a database connection. Supplied automatically when you launch through the Aspire host, which injects it from the AppDb resource. |
SharedConsts.DbConnectionString; Minimal.Infra/Extensions/InfraSetup.cs (AddInfraServices) and Minimal.Api/Configs/DbMigration.cs (RunMigrationAsync) |
Redis |
string | not shipped in any file | Selects the distributed-cache backing 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. Injected by the Aspire host from the Redis resource. |
SharedConsts.RedisConnectionString; Minimal.Api/Configs/CacheConfig.cs and Minimal.Api/Configs/AppConfig.cs |
AzureBus |
string | "" in the Development overlay |
The Azure Service Bus namespace connection string. Non-empty and FeatureManagement:EnableServiceBus true is what adds the AzureBus child bus; either one missing leaves external messaging off while in-memory dispatch keeps working. |
SharedConsts.AzureBusConnectionString; Minimal.Infra/Extensions/ServiceBusSetup.cs |
AzureAppConfig |
string | not shipped | The Azure App Configuration endpoint URI. AzureAppConfigSetup looks it up under the name in AzureAppConfig:ConnectionStringName, which defaults to AzureAppConfig. Without it the integration silently no-ops even with the flag on. |
Minimal.Api/Configs/AzureAppConfig/AzureAppConfigSetup.cs |
The base
appsettings.jsonalso shipsTEMPDb,AppConfigandAzureAppConfigurationunderConnectionStrings. No code reads any of the three — see Keys that ship but are never read.
Authentication:Schemes:BearerBound by ASP.NET Core’s own AddJwtBearer() configuration binding, which reads
Authentication:Schemes:<SchemeName>. Minimal.Api/Configs/Auth/AuthConfig.cs 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 | Shipped default | Effect |
|---|---|---|---|
Authentication:DefaultScheme |
string | Bearer |
The scheme used when an endpoint names none. |
Authentication:Schemes:Bearer:MetadataAddress |
string (URL) | https://login.microsoftonline.com/00000000-.../v2.0/.well-known/openid-configuration — placeholder |
The OIDC discovery document the scheme fetches its signing keys from. Rewritten by --TenantId. |
Authentication:Schemes:Bearer:ValidAudiences |
string array | [ "api://your-api" ] — placeholder |
The audiences a token may carry. Exactly one entry by design: a token minted for any other resource is rejected. Rewritten by --ApiAudience. |
Authentication:Schemes:Bearer:ValidIssuer |
string (URL) | https://sts.windows.net/00000000-.../ — placeholder |
The issuer a token must declare. Rewritten by --TenantId. |
Signature validation is never disabled — Minimal.App.Tests/Architecture/JwtSignatureValidationTests.cs
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:
Minimal.Api/Configs/Auth/SampleClaimsTransformation.cs (an IClaimsTransformation) and the
SampleScopePolicy policy backed by HasScopeRequirement/HasScopeHandler. Neither is applied to
any shipped route. See extension-points.md.
Cors| Key | Type | Base appsettings.json |
appsettings.Development.json |
Effect |
|---|---|---|---|---|
Cors:AllowedOrigins |
string array | [] |
[ "http://localhost:3000", "http://localhost:5173" ] |
Deny-by-default allow-list. Empty or all-blank → neither AddCors nor UseCors is registered at all, 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 | [ "GET", "POST", "PUT", "PATCH" ] |
— | The methods reflected in Access-Control-Allow-Methods. DELETE is deliberately absent: add it here if a browser front-end needs it. Widen or narrow the list freely — an entry not listed is never reflected, so a preflight for it fails. |
Cors:AllowedHeaders |
string array | [ "Authorization", "Content-Type", "Accept", "X-Idempotency-Key" ] |
— | The request headers reflected in Access-Control-Allow-Headers. X-Idempotency-Key is there because the template’s own create route requires it (IdempotencyOptions.IdempotencyHeaderKey’s default). No tracing header (traceparent, X-Request-Id, …) is enumerated — add yours if your front-end sends one. |
Entries in AllowedOrigins are absolute origins — scheme included, no trailing slash, no path. 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. Read by
Minimal.Api/Configs/CrosConfig.cs; behaviour pinned by
Minimal.App.Tests/Integration/Cors/CorsPolicyTests.cs.
Security| Key | Type | Base appsettings.json |
appsettings.Development.json |
Effect |
|---|---|---|---|---|
Security:TrustedProxies |
string array of IP addresses | [] |
— | The proxies whose X-Forwarded-For / X-Forwarded-Proto the service believes. Empty — as shipped — means no forwarded information is honoured at all: Minimal.Api/Configs/ForwardedHeadersConfig.cs sets ForwardedHeaders.None, so Connection.RemoteIpAddress stays the immediate peer and rate limiting partitions on it. Non-empty → XForwardedFor | XForwardedProto are applied, but only when the immediate peer is one of the listed addresses. |
This is the one key a production host behind an ingress, load balancer or CDN must supply — until it does, every request appears to come from that ingress and shares one rate-limit partition. List the address the ingress connects from, one entry per proxy:
"Security": {
"TrustedProxies": [ "10.0.0.4", "10.0.0.5" ]
}
Entries are parsed with IPAddress.Parse, so each must be a single literal IPv4 or IPv6 address —
a CIDR range such as 10.0.0.0/8 is not accepted and fails at startup with a FormatException.
KnownProxies and KnownIPNetworks are cleared before the 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.
The whole module is gated on FeatureManagement:EnableForwardedHeaders (default true, false in
the Development overlay). Turning the flag off and leaving the list empty are equivalent in effect;
the flag exists so the middleware can be taken out of the pipeline entirely for local work.
Https| Key | Type | Class default | Base appsettings.json |
Effect |
|---|---|---|---|---|
Https:HstsMaxAgeDays |
int | 365 |
365 |
The max-age announced by Strict-Transport-Security, in days. 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. |
Read by Minimal.Api/Configs/HttpsConfig.cs, and only when FeatureManagement:EnableHttps is
true (the base file’s value; false in the Development and Testing overlays). Lowering it is
the safe way to try HSTS out on a domain you are not ready to commit for a year — "HstsMaxAgeDays":
1 announces one day and no preload. The header itself has exactly one owner: the security-headers
middleware deliberately does not emit Strict-Transport-Security, so it is never sent twice.
RequestBoundsBound to RequestBoundsOptions 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 | Class default | Base appsettings.json |
Effect when relaxed | Framework default it replaces |
|---|---|---|---|---|---|
RequestBounds:RequestTimeoutSeconds |
int | 30 |
30 |
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(). |
none — ASP.NET Core caps request lifetime only if you ask it to |
RequestBounds:MaxRequestBodySizeBytes |
long | 1048576 (1 MB) |
1048576 |
KestrelServerOptions.Limits.MaxRequestBodySize. A larger body is rejected with 413 Payload Too Large. Raise it for file upload; null is not expressible here, so there is no “unlimited” value through configuration. |
~30 MB |
RequestBounds:RequestHeadersTimeoutSeconds |
int | 10 |
10 |
How long Kestrel waits for the complete request headers before dropping the connection — the slow-headers bound. | 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. Read by Minimal.Api/Configs/RequestBoundsConfig.cs.
RateLimitBound to RateLimitOptions 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 | Class default | Base appsettings.json |
appsettings.Development.json |
Effect |
|---|---|---|---|---|---|
RateLimit:DefaultRequestLimit |
int | 2 |
100 |
1 |
PermitLimit on the fixed-window limiter — requests allowed per window, per partition. |
RateLimit:DefaultConcurrentLimit |
int | 2 |
20 |
1 |
PermitLimit on the concurrency limiter — in-flight requests allowed at once, per partition. |
RateLimit:TimeWindowInSeconds |
int | 1 |
1 |
10 |
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
User.Identity.Name, falling back to the remote IP address, falling back to the request host
(Minimal.Api/Configs/RateLimits/RateLimitKeyProvider.cs) — 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 matters to rate limiting: list your ingress
and each client behind it gets its own budget; leave it empty and they all share the ingress’s. The
provider never reads X-Forwarded-For itself, so a peer that is not a configured 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.
Minimal.App.Tests/Architecture/SecureDefaultAppSettingsTests.cs asserts it stays there. The
shipped production numbers are a placeholder ceiling to tune, not a researched limit.
Both IRateLimitKeyProvider and IRateLimitOptionsProvider are public interfaces you can replace
— see extension-points.md.
AzureAppConfigBound to AzureAppConfigOptions and read only when FeatureManagement:EnableAzureAppConfig is
true. The base appsettings.json ships no AzureAppConfig section at all, so every value
below falls through to its class default.
| Key | Type | Class default | Effect | Read by |
|---|---|---|---|---|
AzureAppConfig:ConnectionStringName |
string | AzureAppConfig |
Which ConnectionStrings entry holds the App Configuration endpoint URI. |
AzureAppConfigSetup.AddAzureAppConfig |
AzureAppConfig:Label |
string | null → falls back to SharedConsts.ApiName (Minimal.Api) |
The label filter applied to op.Select(KeyFilter.Any, label), so only values labelled for this API are loaded. |
AzureAppConfigSetup.AddAzureAppConfig |
AzureAppConfig:LoadFeatureFlags |
bool | true |
Declared but never read. UseFeatureFlags() is called unconditionally. |
— |
AzureAppConfig:FeatureFlagPrefix |
string | "" |
Declared but never read. No prefix filter is applied. | — |
AzureAppConfig:RefreshIntervalInMinutes |
int | 300 |
Declared but never read. The refresh interval is hard-coded to 30 minutes in ConfigureRefresh(c => c.RegisterAll().SetRefreshInterval(TimeSpan.FromMinutes(30))). |
— |
The connection is opened with DefaultAzureCredential, so the host needs a managed identity or a
local Azure login with read access to the store. If the connection string is missing or blank the
method returns without adding the source — the flag alone does nothing.
The three “declared but never read” rows are an options-class-versus-setup mismatch in source, not a documentation gap. Wiring them up is a code change; it is recorded on this documentation ticket rather than made here.
Minimal.Api/Configs/LogConfigs.cs runs before anything else in Program.cs. When
FeatureManagement:EnableOpenTelemetry is false (the shipped default) it adds a console logger
in DEBUG builds and returns — no tracing, no metrics, no exporter. When true it clears the
logging providers, adds the OpenTelemetry logger, and registers ASP.NET Core plus HttpClient
tracing and metrics instrumentation, with a console exporter in DEBUG builds only.
| Key | Type | Shipped default | Effect | Read by |
|---|---|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
string (URL) | http://localhost:4317 in the base file |
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. |
LogConfigs.AddLogConfig |
AzureMonitor:ConnectionString |
string | "" in the base file |
Non-blank → UseAzureMonitor() is added, shipping traces, metrics and logs to Application Insights. Blank, as shipped, means no Azure Monitor exporter. |
LogConfigs.AddLogConfig |
Logging:LogLevel:* |
string | Default: Information, Microsoft: Warning, Microsoft.Hosting.Lifetime: Warning; the Development overlay drops to Debug/None/None |
Standard ASP.NET Core log filtering. Note that EnableOpenTelemetry clears the providers, so these filters then apply to the OpenTelemetry logger. |
ASP.NET Core logging |
Both exporter keys are additive: set both and both exporters run.
| Key | Shipped default | Effect |
|---|---|---|
AllowedHosts |
"*" in the Development overlay only |
ASP.NET Core’s host-filtering middleware. Absent from the base file, where the framework default (*) applies. 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. They are inert —
setting them changes no behaviour.
| Key | File | Why it is dead |
|---|---|---|
ConnectionStrings:TEMPDb |
base appsettings.json |
A leftover name. The database connection is read from AppDb (SharedConsts.DbConnectionString). |
ConnectionStrings:AppConfig |
base appsettings.json |
AzureAppConfigOptions.ConnectionStringName defaults to AzureAppConfig, not AppConfig. |
ConnectionStrings:AzureAppConfiguration |
base appsettings.json |
Same reason — the looked-up name is AzureAppConfig. |
The whole AzureAppConfiguration section (KeyPrefix, Label, CacheExpirationInSeconds, LoadFeatureFlags, FeatureFlagPrefix) |
base appsettings.json |
AzureAppConfigOptions.Name is AzureAppConfig. Nothing binds a section called AzureAppConfiguration, and KeyPrefix/CacheExpirationInSeconds are not properties on the options class at all. |
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. |
Removing them is a source change to the shipped appsettings*.json files, out of scope for this
documentation page and recorded on the ticket instead.
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=postgres;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 — they 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.