The Configuration Pipeline
Five files, one rule: the last file loaded wins. Understanding the order tells you exactly where to put any setting and what will override it.
The Five Layers
appsettings.json committed
Base defaults. Neutral values — no machine names, no passwords. Loaded by ASP.NET before anything else.
appsettings.{env}.json committed
Environment defaults. Loaded automatically by ASP.NET based on ASPNETCORE_ENVIRONMENT. Example: appsettings.Development.json.
appsettings.{env}.Local.json gitignored
Your machine's overrides. SQL Server name, local Redis, local Kafka. Loaded by WebApplicationHelper. The file is optional — silently skipped if absent.
appsettings.Tenant.{code}.json committed
Tenant defaults applying to all environments — CORS origins, Kafka topic names, LLM model choice, feature flags. Only loaded when TENANT_CODE is set.
appsettings.Tenant.{code}.{env}.json deployed
Tenant + environment specific. Real database, Redis cluster, Kafka brokers, secrets. Last file loaded — highest priority.
Precedence — Last Wins
ASP.NET Core's IConfiguration resolves a key by scanning registered sources from last to first
and returning the first match. Because later sources are registered after earlier ones, a key in
Layer 5 always beats the same key in Layer 1.
| Key present in | Value used |
|---|---|
| Layer 1 only | Layer 1 value (used by everyone) |
| Layer 1 and Layer 3 | Layer 3 value (your machine override) |
| Layer 4 and Layer 5 | Layer 5 value (tenant + env specific) |
| All five layers | Layer 5 value |
| None | null — app must handle |
The Code
WebApplicationHelper.CreateWebApplicationBuilder calls WebApplication.CreateBuilder
(which registers Layers 1 and 2), then adds Layers 3–5:
public static WebApplicationBuilder CreateWebApplicationBuilder(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
LoadEnvironment(builder);
LoadTenant(builder);
return builder;
}
private static void LoadEnvironment(WebApplicationBuilder builder)
{
PlatformSettings.Environment.Name =
(builder.Environment.EnvironmentName ?? "production").ToLower();
LoadFile(builder, $"appsettings.{PlatformSettings.Environment.Name}.Local.json");
}
private static void LoadTenant(WebApplicationBuilder builder)
{
PlatformSettings.Tenant.Code =
(Environment.GetEnvironmentVariable("TENANT_CODE") ?? "").ToLower();
if (!string.IsNullOrEmpty(PlatformSettings.Tenant.Code))
{
LoadFile(builder, $"appsettings.Tenant.{PlatformSettings.Tenant.Code}.json");
LoadFile(builder,
$"appsettings.Tenant.{PlatformSettings.Tenant.Code}.{PlatformSettings.Environment.Name}.json");
}
}
private static void LoadFile(WebApplicationBuilder builder, string filePath)
{
builder.Configuration.AddJsonFile(filePath, optional: true, reloadOnChange: true);
}
optional: true — the app never crashes on a missing file.
reloadOnChange: true — edits take effect in development without a restart.
PlatformSettings
After startup, PlatformSettings holds the resolved environment name and tenant code
as static properties, available anywhere in the application:
PlatformSettings.Environment.Name // e.g. "development"
PlatformSettings.Tenant.Code // e.g. "apexretail"
PlatformSettings.Environment.Name is lowercased. File names must match exactly on Linux containers.
Use appsettings.Development.Local.json — not appsettings.development.local.json — because
ASPNETCORE_ENVIRONMENT is typically set as Development (capital D) and ASP.NET
resolves Layer 2 with that exact casing. Layer 3 and beyond use the lowercased value.