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 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:
-
appsettings.json
-
appsettings.{Environment}.json
-
User Secrets (Development only)
-
Environment variables
-
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:
They override appsettings
They bind into AppConfig
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.
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.
