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?

:backhand_index_pointing_right: 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:

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

Blazor doesn’t need more configuration magic.
It needs a 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:

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.

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:

// 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:

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:

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:

:check_mark: They override appsettings
:check_mark: They bind into AppConfig
:check_mark: No code changes required

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

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.

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

Azure App Settings (Production Overrides)

Azure App Service settings map directly to environment variables.

That means:

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:

It should only know:

“I depend on this configuration contract.”

Why Strongly-Typed Config Makes This Safe

Without a config contract:

With a contract + validation:

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:

It should be:

And a boring configuration is exactly what you want.

[source code]