Stop Hardcoding AppSettings: A Clean Pattern for Blazor Environment Configuration

 


If your Blazor app works locally but breaks in Azure, there’s a good chance configuration is the real bug.

Blazor developers often treat configuration as a startup concern—something you wire up once and forget.

That works…
until you add environments.
or feature flags.
or secrets.
or a second app.

Then the configuration quietly turns into a distributed liability.

Let’s talk about why hardcoded AppSettings patterns don’t scale—and a cleaner, environment-aware approach that actually survives real projects.


The Common (But Fragile) Pattern

Most Blazor apps start like this:

// appsettings.json

{

  "InCounter": 5,

  "FeatureFlagStyleTable": true

}

Var styleTable = configService.Value.FeatureFlagStyleTable;

At first, this feels fine.

Then you add:

  • appsettings.Development.json
  • appsettings.Test.json
  • appsettings.Production.json
  • Azure App Service settings
  • Feature flags
  • Secrets in Key Vault

Suddenly:

  • Keys are duplicated
  • Values drift between environments
  • Missing settings cause runtime failures
  • No one knows which values are actually being used

And the worst part?

👉 The configuration is invisible at runtime.


Why Hardcoded AppSettings Fail in Blazor

Hardcoding configuration values—or scattering string keys across the codebase—creates several problems:

No compile-time safety
No validation on startup
No single source of truth
Environment logic leaks everywhere
Impossible to reason about in production

Blazor doesn’t need more configuration magic.
It needs strong structure.


A Clean Pattern: Strongly-Typed, Validated Configuration

The goal is simple:

Load configuration once, validate it early, and inject it everywhere.

Step 1: Define a Configuration Contract

public sealed class AppConfig

{

    public required int InCounter { get; init; }

    public bool FeatureFlagStyleTable{ get; init; }

}

This class becomes the contract between your app and its configuration.

No magic strings.
No guessing.


Step 2: Bind and Validate on Startup

builder.Services

    .AddOptions<AppConfig>()

    .Bind(builder.Configuration)

    .ValidateDataAnnotations()

    .ValidateOnStart();

Now your app will:

  • Fail fast if config is missing
  • Catch errors at startup—not at runtime
  • Make configuration explicit and testable

Step 3: Inject Configuration, Not IConfiguration

In Weather.razor

@using BetterConfigService

@using Microsoft.Extensions.Options 

@inject IOptions<AppConfig> configService

 

protected override async Task OnInitializedAsync()

{

    styleTable = configService.Value.FeatureFlagStyleTable;

}


Your services now depend on intent, not infrastructure.


Handling Multiple Environments (Without If Statements)

Blazor already supports layered configuration:

appsettings.json

appsettings.Development.json

appsettings.Production.json

The key insight:

Your code should never care which environment it’s running in.

The environment only decides which values are loaded, not how they’re used.

No if (env.IsDevelopment()) scattered through services.
No feature toggles are buried in components.

 

How This Works with AppSettings, User Secrets, and .env Files

One of the biggest misconceptions around configuration is thinking you have to choose between:

  • appsettings.json
  • User Secrets
  • Environment variables
  • .env files
  • Azure App Settings

You don’t.

👉 They all work together.
The key is understanding precedence and binding, not replacing one with another.


The Configuration Stack (Highest Wins)

In Blazor (and .NET in general), configuration is layered. Later sources override earlier ones.

Typical order:

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. User Secrets (Development only)
  4. Environment variables
  5. Azure App Service/container settings

Your AppConfig binding doesn’t change — only the source of the values does.


AppSettings Files (Defaults and Baselines)

Use these for:

  • Non-secret defaults
  • Shared baseline values
  • Documentation of expected settings

// appsettings.json

{

  "InCounter": "",

  "FeatureFlagStyleTable": false

}

Think of this file as:

“Here’s what the app expects to exist.”

Not:

“Here’s what production should use.”


User Secrets (Local Development, No Secrets in Git)

User Secrets are perfect for:

  • API keys
  • Local URLs
  • Developer-specific overrides

dotnet user-secrets set "ApiBaseUrl" "https://localhost:7071"

They automatically override appsettings.json without changing your code.

Your strongly-typed config still works exactly the same:

var apiUrl = appConfig.ApiBaseUrl;

No branching.
No conditionals.
No environment checks.


.env Files (Local Containers & Tooling)

.env files are not a native .NET feature — they’re a convention used by:

  • Docker
  • Docker Compose
  • Some local dev tools

Example:

ApiBaseUrl=https://localhost:7071

EnableNewFeature=true

When loaded, these become environment variables, which .NET already understands.

If your tool loads .env into environment variables:

They override appsettings
They bind into AppConfig
No code changes required

The configuration pipeline doesn’t care where the value came from.


Azure App Settings (Production Overrides)

Azure App Service settings map directly to environment variables.

That means:

  • No appsettings edits for prod
  • No secrets in source control
  • No redeploys to change values

Your Blazor app still just consumes:

IOptions<AppConfig>

This is where the pattern really shines:

The app never changes — only the configuration source does.


The Golden Rule

Your application code should never know:

  • Where configuration comes from
  • Which environment it is running in
  • Whether a value came from JSON, secrets, .env, or Azure

It should only know:

“I depend on this configuration contract.”


Why Strongly-Typed Config Makes This Safe

Without a config contract:

  • Missing values fail silently
  • Typos become runtime bugs
  • Environment overrides are invisible

With a contract + validation:

  • Startup fails fast
  • Configuration drift is obvious
  • Overrides are intentional

That’s the difference between configuration and configuration chaos.


Bottom Line

Using AppConfig doesn’t replace:

  • appsettings.json
  • User Secrets
  • .env files
  • Azure App Settings

It unifies them.

Same contract.
Same injection.
Same behavior.

Only the environment decides the values.

And that’s exactly how configuration should work.


Feature Flags Done Right

Instead of this:

if (Configuration["EnableNewFeature"] == "true")

{

    // ...

}

Do this:

@if (AppConfig.EnableNewFeature)

{

    <NewFeature />

}

Clear.
Predictable.
Testable.


Bonus: Making Configuration Visible at Runtime

One of the most underrated improvements:

Create a read-only configuration diagnostics page.

public record ConfigSnapshot(

    string ApiBaseUrl,

    bool EnableNewFeature

);

Expose it:

  • Only in non-prod
  • Or behind admin authorization

This saves hours of “why is this behaving differently?” debugging.


When This Pattern Really Pays Off

This approach shines when:

  • You have multiple Blazor apps
  • You deploy to Azure App Services
  • You use Key Vault
  • You support feature flags
  • You care about testability
  • You want predictable deployments

In other words: real systems.


Final Thought

Configuration should not be:

  • A pile of strings
  • A runtime guessing game
  • A tribal knowledge problem

It should be:

Explicit
Validated
Environment-aware
Boring

And a boring configuration is exactly what you want.


 

[source code]

Comments

Popular posts from this blog

Customizing PWA Manifest and Icons for a Polished User Experience 🚀

Yes, Blazor Server can scale!

Offline-First Strategy with Blazor PWAs: A Complete Guide 🚀