Skip to main content
ANVISoftware Solutions
Lesson 8 of 12Intermediate16 min

Environments

By the end of this lesson

Vary behaviour between development, staging and production correctly.

The defaults that make development pleasant are wrong in production, and the settings that make production safe make development miserable. Detailed error pages, a permissive local database, fake outbound integrations, verbose logging: all useful on your machine, all unacceptable on a live service.

The platform resolves that with one string. At start-up the host reads an environment name from an environment variable, exposes it through IHostEnvironment, and uses it to choose which appsettings file layers on top of the base one. Your own code reads the same value when behaviour needs to differ.

This is the intended mechanism, not a workaround. Branching on the environment name is how the platform expects one build to behave differently in different places, and inventing your own flag for the same job means two things to keep in step.

One detail is worth committing to memory: when the variable is not set at all, the environment is Production. The safe default applies when nobody has said anything, which is the right way round.

The names and the variables:

Development
Your machine, and nowhere else. User secrets are read, detailed errors are acceptable, and the container validates its own registrations more strictly. Tooling special-cases this name — no other name gets those behaviours.
Staging
A deployed environment configured as closely to production as you can afford, used to test a release before it goes live. Nothing is special-cased for this name; it exists so appsettings.Staging.json and your own branches have somewhere to hang.
Production
The live service, and the default when the variable is unset. Everything is at its strictest: no detailed errors, real integrations, secrets from a secret store.
DOTNET_ENVIRONMENT
The variable the generic host reads. This is the one that applies to console applications, workers and message consumers.
ASPNETCORE_ENVIRONMENT
The variable a web application reads, and it takes precedence if both are set. Setting only DOTNET_ENVIRONMENT for a web application is a common and quiet failure.
Custom names
Permitted. IsEnvironment compares any name you like, and appsettings.QaEast.json will load if the environment is QaEast. Only Development carries built-in behaviour, so a custom name is purely yours to give meaning to.
Setting the environment name in the places it actually gets set
Shell
# One run, current shell (Linux, macOS)
DOTNET_ENVIRONMENT=Staging dotnet run

# Current session (Windows PowerShell)
$env:DOTNET_ENVIRONMENT = "Staging"
dotnet run

# A container, at the point it starts
docker run -e ASPNETCORE_ENVIRONMENT=Production anvi/employees-api:1.4.0

# Confirm what a running container was actually given
docker exec employees-api printenv ASPNETCORE_ENVIRONMENT

# Which environment did the application decide it was in?
dotnet run -- --Environment=Staging
  • There is no single canonical place to set this. It is an environment variable, so it comes from wherever that machine gets environment variables: your shell, a container definition, an orchestrator manifest, a service configuration.
  • On your machine it usually comes from launchSettings.json, which the dotnet run and editor launch profiles read. That file is a development convenience and is never used by a published application, so a value that works locally tells you nothing about a deployment.
  • Checking the value on a running instance rather than assuming it is the single most useful habit in this lesson. Print it in a start-up log line so it is visible in the log rather than something you have to go and ask.
Branching where the decision belongs — at composition time
C#
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

if (builder.Environment.IsDevelopment())
{
    // No database and no real email on a laptop
    builder.Services.AddSingleton<IEmployeeStore, InMemoryEmployeeStore>();
    builder.Services.AddSingleton<INotificationSender, LoggingNotificationSender>();
}
else
{
    string connection = builder.Configuration.GetConnectionString("Employees")
        ?? throw new InvalidOperationException(
            "Connection string 'Employees' is required outside Development.");

    builder.Services.AddSingleton<IEmployeeStore>(_ => new PostgresEmployeeStore(connection));
    builder.Services.AddSingleton<INotificationSender, SmtpNotificationSender>();
}

using IHost host = builder.Build();
await host.RunAsync();
  • The branch happens once, at start-up, while services are being registered. Everything downstream receives an IEmployeeStore and never asks which environment it is in — which keeps the environment check in one file instead of scattered through the application.
  • The missing connection string throws rather than falling back to something local. Failing at start-up with a clear message beats starting successfully and writing production data somewhere unintended.
  • IsDevelopment compares the name ignoring case, so DEVELOPMENT and development both match. It does not do anything clever with near misses — Dev matches nothing.
  • There is a risk in this shape worth naming: the else branch is the one that runs in production and the if branch is the one you test all day. Keep the untested side small, and make sure staging exercises the same path production will.

What should actually differ, and what should not:

 DevelopmentProduction
Error detail returned to the callerFull, including stack traces — you are the only callerA generic message and an identifier that ties back to a log entry. Never a stack trace
Where secrets come fromUser secrets in your profileA managed secret store, surfaced as configuration
Log levelDebug, because you are reading the output as it happensInformation or Warning, because volume costs money and hides signal
Outbound integrationsFakes and local doubles, so nothing real is emailed or chargedThe real services
DataSeeded sample employees you can delete freelyReal records, with backups and retention rules
Business rules and validationIdenticalIdentical — if a rule differs by environment, you are testing a different application

Summary

  • The environment name is one string read at start-up, exposed through IHostEnvironment, and used to layer appsettings.{Environment}.json — the name comparison ignores case, but file names on Linux do not
  • Unset means Production, which is the safe direction to fail in
  • Generic hosts read DOTNET_ENVIRONMENT; web applications read ASPNETCORE_ENVIRONMENT and it wins if both are set
  • Only Development carries built-in behaviour; Staging, Production and custom names get meaning from your own code and files
  • Branch once at registration time rather than scattering checks, and never return detailed errors outside Development

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Prove which file is being layered

Add an appsettings.Staging.json to the employees project that changes one value, and log that value plus the environment name at start-up.

Run it three times: with no environment variable, with the variable set to Staging, and with the variable set to staging in lower case. Then rename the file to lower case and repeat on Linux or in a container if you can.

Show solution

With no variable you get Production and the base value, because that is the default. With Staging or staging you get the overridden value, because the environment name comparison ignores case.

The file name is a different matter. The configuration source is built from the name, and the file system decides whether it matches. On Windows a lower-case file name works; on Linux it does not. That asymmetry is behind a whole category of it-works-locally bug reports, and it is worth seeing once with your own eyes.

Think about it

Why default to Production?

When the environment variable is absent, the host chooses Production rather than Development.

Argue for that choice. Then describe what would go wrong on a new developer's first day if the default were Development, and what would go wrong on a server.

Show solution

The rule is that an unstated configuration should fail safe. Every protective behaviour hangs off not being in Development, so an unset variable turning on the strictest mode is the outcome you want when something has gone wrong in deployment.

If Development were the default, a server with a missing or misspelled variable would serve detailed errors, look for user secrets and relax container validation. That failure is silent and public, and it is the worst possible direction to fail in.

The cost lands on the developer instead: a new checkout with no launch profile behaves as Production, so local secrets are ignored and errors are terse. That is a five-minute confusion with a clear fix, discovered by someone who can fix it. Putting the cost there rather than on a live service is the trade being made.

Saved in this browser only.