Portal Community

The Five Layers

1

appsettings.json committed

Base defaults. Neutral values — no machine names, no passwords. Loaded by ASP.NET before anything else.

2

appsettings.{env}.json committed

Environment defaults. Loaded automatically by ASP.NET based on ASPNETCORE_ENVIRONMENT. Example: appsettings.Development.json.

3

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.

4

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.

5

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 inValue used
Layer 1 onlyLayer 1 value (used by everyone)
Layer 1 and Layer 3Layer 3 value (your machine override)
Layer 4 and Layer 5Layer 5 value (tenant + env specific)
All five layersLayer 5 value
Nonenull — 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"
Case sensitivity on Linux 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.
Concrete walkthrough See Local Setup for a step-by-step example of what loads on a developer machine, and Tenant Configuration for a full worked example with a real tenant.