Environments

One build, different behaviour on a laptop and in production, chosen by a single environment name read at startup.

Syntaxif (app.Environment.IsDevelopment()) { /* development only */ }

The same build of an app runs on a laptop and on a server, and it should not behave identically. Locally you want detailed error pages. In production you want a short error response and no stack traces sent to strangers. The environment is one string that says which situation the app is in.

Where the name comes from

.NET reads it from two operating system environment variables: DOTNET_ENVIRONMENT and ASPNETCORE_ENVIRONMENT. If neither is set, the environment is Production. That default is deliberate: an app that is misconfigured should be the safe one, not the chatty one. Three names have meaning to the framework: Development, Staging and Production. Any other string is allowed. The name is fixed when the app starts and cannot change while it runs.

Reading it in code

builder.Environment and app.Environment expose it. The property EnvironmentName is the raw string. IsDevelopment(), IsStaging() and IsProduction() are shorthand for comparing that string to the matching name, ignoring case; IsEnvironment("Testing") does the same comparison for any name you pass.

Keep the difference deliberate

The environment also picks a configuration file: appsettings.Development.json loads on top of appsettings.json. Prefer changing a setting there over writing if (env.IsDevelopment()). Every such if is a branch that production runs and your laptop never does.

Running the example below and calling curl http://localhost:5000/ returns {"environment":"Production","isDevelopment":false} with neither variable set, and {"environment":"Development","isDevelopment":true} under dotnet run -e Development.

Example

Example · csharp
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

// app.Environment is the same information as builder.Environment.
// Outside Development, replace an unhandled exception with a plain response.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/error");
}

app.MapGet("/", () => new
{
    environment = app.Environment.EnvironmentName,
    isDevelopment = app.Environment.IsDevelopment()
});

app.MapGet("/error", () => Results.Problem("Something went wrong."));

app.Run();

When to use it

  • Show full exception detail on a developer machine while a deployed instance returns a plain error response.
  • Run the app locally as Staging to reproduce a bug that only appears after deploy.
  • Load a different database connection string per environment from appsettings.{Environment}.json instead of branching in code.

More examples

Choosing a different implementation per environment

Use this when the difference is a whole implementation rather than a value: no real email from a developer machine, no real card charges from staging. Register the swap in one place at startup and log which one was chosen. The alternative, an if (env.IsDevelopment()) inside the sender itself, hides the behaviour in the class that claims to send mail.

Example · csharp
var builder = WebApplication.CreateBuilder(args);

// Decide once, at startup, where the decision is easy to see.
if (builder.Environment.IsDevelopment())
{
    builder.Services.AddSingleton<IEmailSender, LoggingEmailSender>();
}
else
{
    builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
}

var app = builder.Build();

// Log the choice so nobody has to guess which one is running.
app.Logger.LogInformation(
    "Environment {Environment}", app.Environment.EnvironmentName);

app.MapGet("/", () => "ok");
app.Run();

public interface IEmailSender
{
    void Send(string to, string body);
}

public sealed class LoggingEmailSender(ILogger<LoggingEmailSender> logger) : IEmailSender
{
    public void Send(string to, string body) =>
        logger.LogInformation("Would send mail to {To}", to);
}

public sealed class SmtpEmailSender : IEmailSender
{
    public void Send(string to, string body)
    {
        // Real SMTP send goes here.
    }
}

Setting the environment when you run

Reach for these when you need to reproduce production behaviour locally, which is the usual way to find a bug that only appears after deploy. Note --no-launch-profile: without it, dotnet run applies Properties/launchSettings.json, whose environmentVariables override the ones you exported, and you end up testing something other than what you set.

Example · bash
# Neither variable set, no launch profile: the app runs as Production.
dotnet run --no-launch-profile

# For one run, using the dotnet run option.
dotnet run -e Staging

# As a real environment variable, which is what a container does.
export ASPNETCORE_ENVIRONMENT=Staging
dotnet run --no-launch-profile

A launch profile for local work

launchSettings.json is a local development file. It is not published with the app, so it never affects a deployed service. Select a profile with dotnet run -lp as-staging. Without -lp, the CLI uses the first profile whose commandName is Project, which is why adding a profile at the top of the file can quietly change what dotnet run does for everyone on the team.

Example · json
// Properties/launchSettings.json
{
  "profiles": {
    "https": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:7205",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "as-staging": {
      "commandName": "Project",
      "applicationUrl": "https://localhost:7205",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Staging"
      }
    }
  }
}

Discussion

  • Be the first to comment on this lesson.