Summary
A developer reported an issue where a .NET 10 application on macOS failed to switch to the Development environment, despite explicitly passing environment variables via the CLI. While the same command worked perfectly on Linux, the macOS environment ignored the DOTNET_ENVIRONMENT flag, defaulting instead to Production. The investigation revealed a misunderstanding of how shell environments interact with the dotnet CLI and a confusion between CLI arguments and Environment Variables.
Root Cause
The failure stems from a fundamental distinction between Command Line Arguments and Environment Variables in Unix-based shells (like Zsh or Bash on macOS):
- Argument Misinterpretation: Passing
--environment DOTNET_ENVIRONMENT=Developmenttells the .NET CLI to pass a string argument to the application’sMainmethod. It does not set a system-level environment variable. - Shell Scoping: On macOS/Linux, prefixing a command with
VAR=value(e.g.,DOTNET_ENVIRONMENT=Development dotnet run) sets the variable only for the child process created by that command. - The CLI Trap: The
--environmentflag is not a nativedotnet runargument for setting the global application environment; it is often misinterpreted as a way to inject theDOTNET_ENVIRONMENTkey. - Version Regression/Shift: The user noted that behavior changed after updating to .NET 9/10. This is likely due to changes in how the Host Builder parses arguments or how the shell handles specific flag sequences during the transition between runtime versions.
Why This Happens in Real Systems
In production-grade systems, this happens due to Environment Parity gaps:
- OS Differences: Windows (CMD/PowerShell) handles environment variable injection differently than macOS (Zsh). Developers often write scripts on one OS that fail on another because of shell syntax nuances.
- Configuration Precedence: .NET follows a strict hierarchy: Command Line Args > Environment Variables > appsettings.json. If a developer thinks they are setting an environment variable but are actually just passing a string argument, the Host Builder ignores it and falls back to the default (Production).
- CI/CD Discrepancies: A script that works on a developer’s local machine might fail in a GitHub Action or Jenkins runner if the runner uses a different shell (e.g.,
shvsbash) or if the variable is not exported correctly.
Real-World Impact
- Security Risks: If a system fails to switch to
Development, it may attempt to connect to Production databases or use Production API keys while the developer is testing, leading to catastrophic data corruption. - Debugging Blindness: Critical diagnostic tools (like Detailed Error Pages or Swagger UI) are typically gated behind the
Developmentenvironment. If the environment stays inProduction, the developer sees generic 500 errors instead of actionable stack traces. - Wasted Engineering Hours: Senior engineers often spend hours debugging “ghost bugs” that are actually just configuration mismatches.
Example or Code
# INCORRECT: This passes a literal string argument to the app, it doesn't set the environment
dotnet run --environment DOTNET_ENVIRONMENT=Development
# INCORRECT: This sets the variable for the shell, but may be swallowed if the CLI parses it incorrectly
DOTNET_ENVIRONMENT=Development dotnet run
# CORRECT: Use the standard way to set environment variables in macOS/Linux shells
export DOTNET_ENVIRONMENT=Development
dotnet run
# CORRECT: Inline execution (most reliable for one-off commands)
DOTNET_ENVIRONMENT=Development dotnet run --project ./Path/To/Project.csproj
How Senior Engineers Fix It
- Standardize Local Tooling: Instead of relying on manual CLI flags, senior engineers use
launchSettings.json. This file is cross-platform and ensures thatdotnet runpicks up the correct profile regardless of the host OS. - Explicit Shell Scripting: When writing automation, we use
exportor explicitly define the environment within the.envfiles used by tools likedirenv. - Validation Logic: We implement Startup Guards. If the application detects it is running in an unexpected environment (e.g.,
Developmentbut pointing to aProdconnection string), the application should fail fast and refuse to boot. - Infrastructure as Code (IaC): We ensure that environment variables are treated as first-class citizens in our deployment manifests (Kubernetes ConfigMaps or Terraform), rather than relying on command-line arguments.
Why Juniors Miss It
- Confusing Arguments with Variables: Juniors often assume that any text following a
--flag is automatically treated as a system setting, failing to realize thatdotnettreats them as application-level parameters. - Implicit vs. Explicit: They rely on the “magic” of the IDE (like Visual Studio or Rider) to set environments. When they move to the Terminal, they don’t realize the IDE was doing the heavy lifting of setting the environment variables behind the scenes.
- Lack of OS Depth: Many developers are “Language Experts” but not “System Experts.” They understand C# syntax perfectly but do not understand how Unix Process Spawning or Shell Scoping works.