A code review of an ASP.NET Core launchSettings.json: how launch profiles are used by Visual Studio, Rider, VS Code and dotnet run, and fixing committed secrets, the wrong environment and unsafe URLs.
Properties/launchSettings.json defines launch profiles for local development: how to start the app, which URLs, and which environment variables. Visual Studio, Rider, VS Code with C# Dev Kit and dotnet run all read it, but it is not published. In this file I would flag: secrets are committed (a production connection string and API key), so they must be rotated now and moved to user secrets; the environment is Production, which switches on production behaviour and turns off user secrets, and combined with the production connection string means a developer's F5 talks to the live database; and it binds to 0.0.0.0 over plain HTTP, exposing the app to the network. I would replace it with HTTPS and HTTP profiles on localhost using Development, and add a separate production-like profile only if someone needs to test production behaviour against non-production resources.
"
Properties/launchSettings.jsonholds local launch profiles: how to start the app, which URLs and which environment variables. Visual Studio, Rider, VS Code with C# Dev Kit anddotnet runall read it, anddotnet runtakes the first Project profile unless you pass--launch-profile, but it isn't published, so production and CI configuration come from app settings, environment variables and Key Vault. When I review it, I look for committed secrets, which I treat as compromised and rotate before cleaning history; the environment name, because Production switches off user secrets and developer pages, while the production connection string is what actually reaches production data; and 0.0.0.0 over plain HTTP, which I replace with localhost HTTPS and HTTP profiles on Development.For example, on [our orders API], a production connection string ended up in a committed launch profile and a developer accidentally ran a migration against production. I led the clean-up and wanted to make it impossible to repeat. I had the credentials rotated straight away, worked with [the DBA] to roll back the migration, cleaned the history, moved local configuration to user secrets and a local SQL Server container, and replaced the profile with Development-only localhost profiles. I also pushed for managed identity, Key Vault and push protection. No data was lost [confirm in your own story], and since then [no] secrets have reached the repo.
The mistake I see is just deleting the line, which leaves the secret in history and in every clone. I review the file for what it enables, not just what it contains: if a profile can reach production, the real fix is least privilege, so developer machines can't reach production data at all, and that's part of keeping the whole team environment secret-free."
Properties/launchSettings.json is a development-time file created by the ASP.NET Core templates. Each profile says how to launch the project:
commandName: Project (run the app itself with Kestrel), IISExpress, Executable or Docker.applicationUrl: the URLs Kestrel listens on.environmentVariables: set for that launch only, most importantly ASPNETCORE_ENVIRONMENT.launchBrowser / launchUrl: whether to open a browser.dotnet run uses the first profile with commandName: Project by default; --launch-profile <name> picks another and --no-launch-profile ignores the file.dotnet publish does not include it, and the deployed app never reads it. Production configuration comes from appsettings.json, environment variables and Azure Key Vault.| # | Finding | Severity | Fix |
|---|---|---|---|
| 1 | Production SQL password and live payment gateway key are committed | Critical | Rotate both now, remove from git history, report per the incident process; use dotnet user-secrets locally and Key Vault in Azure |
| 2 | ASPNETCORE_ENVIRONMENT is Production |
High | Use Development so developer exception pages, user secrets and local config load, and production behaviour is not triggered locally |
| 3 | Connection string targets the production server | High | Developers should not be able to reach production data from a launch profile; point at a local SQL Server container |
| 4 | http://0.0.0.0:5000 binds every interface over HTTP |
Medium | Use https://localhost:7181;http://localhost:5181 |
| 5 | Single profile, no $schema |
Low | Add https and http profiles and the schema for editor validation |
Git keeps history, forks and clones keep copies, and scanners find secrets in public and internal repositories quickly. Treat the secret as compromised: rotate, audit access logs, then clean history. If personal information could have been accessed, assess it under the Notifiable Data Breaches scheme of the Privacy Act 1988.
A developer copied a production connection string into a launch profile to reproduce a bug, and later ran an EF Core migration from the IDE against production by accident. We rotated the credential, removed it from history, moved local configuration to user secrets and a local SQL container, and removed production network access from developer machines.
The interviewer is checking whether you know what this common file actually does, and whether you spot and respond correctly to a secrets leak.
Green flags
Red flags
Production environment or the 0.0.0.0 binding.A launch profile is a set of instructions that a tool reads before it starts your app. The app itself never opens launchSettings.json. The tool turns the profile into an ordinary process start: a command line, a working directory and a set of environment variables.
Properties/launchSettings.json
|
v
tool reads the selected profile
(Visual Studio, Rider, VS Code C# Dev Kit, dotnet run, dotnet watch)
|
+-- applicationUrl --> ASPNETCORE_URLS for the process
+-- environmentVariables --> process environment variables
+-- commandLineArgs --> args passed to Main
+-- workingDirectory --> current directory of the process
+-- launchBrowser/launchUrl --> tool opens a browser (app unaware)
|
v
your app starts and reads configuration as normal
(it has no idea a launch profile existed)
That is why the file has no effect after dotnet publish: nothing in the deployed environment reads it.
| Setting | Meaning | Notes |
|---|---|---|
commandName |
How to launch | Project (run the app with Kestrel), IISExpress, IIS, Executable (run another program), Docker (Visual Studio container tools) |
applicationUrl |
URLs to listen on, separated by ; |
Becomes ASPNETCORE_URLS. Use localhost, not 0.0.0.0 or * |
environmentVariables |
Variables set for this launch only | ASPNETCORE_ENVIRONMENT, feature flags, logging levels. Never secrets |
commandLineArgs |
Arguments passed to the app | Read through args and the command-line configuration provider |
workingDirectory |
Starting folder | Affects relative paths such as appsettings.json lookup in console apps |
launchBrowser / launchUrl |
Open a browser after start | Set to false for APIs so the IDE doesn't open tabs |
dotnetRunMessages |
Show "Building..." messages from dotnet run |
Cosmetic |
inspectUri |
Browser debugging endpoint | Used for Blazor WebAssembly debugging |
iisSettings (top level) |
IIS Express site settings | Only for IISExpress profiles; Windows only |
$schema |
JSON schema reference | Gives completion and validation in editors |
| Tool | How it picks a profile |
|---|---|
| Visual Studio | The run dropdown lists every profile. The "Debug launch profiles" UI edits the same file |
| Rider | Imports the profiles as run configurations automatically |
| VS Code with C# Dev Kit | Launching a project offers its profiles. A launch.json entry of type dotnet can name one |
dotnet run |
The first profile with commandName: Project, unless --launch-profile <name> (or -lp) is given. --no-launch-profile ignores the file |
dotnet watch |
Same rules as dotnet run |
dotnet test |
Does not use launch profiles |
dotnet publish and the deployed app |
Ignore it completely |
In .NET 10, file-based apps (dotnet run app.cs) can have launch profiles too, in a file named after the app such as app.run.json, next to the .cs file.
launch profile --sets--> process environment variables
|
WebApplication.CreateBuilder(args) loads, later wins:
1. appsettings.json
2. appsettings.{Environment}.json
3. user secrets (only when Environment = Development)
4. environment variables <-- launch profile values land here
5. command-line arguments <-- commandLineArgs land here
Two consequences follow from this order:
appsettings.Development.json, silently.ASPNETCORE_ENVIRONMENT=Production stops user secrets loading at all, and loads appsettings.Production.json instead. People then paste secrets into the profile to make it work, which is how the file in this question came to exist.The double underscore in a variable name such as ConnectionStrings__OrdersDb maps to the : separator, so it becomes ConnectionStrings:OrdersDb.
| Behaviour | Development | Production |
|---|---|---|
appsettings.{Environment}.json loaded |
appsettings.Development.json |
appsettings.Production.json |
| User secrets | Loaded | Ignored |
| Developer exception page | On in the default templates | Off |
app.Environment.IsDevelopment() branches |
True | False |
| OpenAPI/Swagger UI in the templates | Mapped | Usually not mapped |
| Scope and service validation on build | On | Off |
So the environment name changes real behaviour, but it isn't what reaches production data. The connection string is. Fixing only the environment name leaves the live database one F5 away.
dotnet user-secrets init adds a <UserSecretsId> GUID to the project file.%APPDATA%\Microsoft\UserSecrets\<id>\secrets.json on Windows, ~/.microsoft/usersecrets/<id>/secrets.json on macOS and Linux.dotnet user-secrets list shows what is set; dotnet user-secrets clear removes everything for the project.Order matters. Rotating first limits the damage; cleaning history first only hides the evidence.
git filter-repo or BFG, force-push, and ask everyone to re-clone. Forks and existing clones still hold the old values, which is why step 1 comes first. repo (committed) developer machine (not committed)
----------------- ---------------------------------
launchSettings.json user secrets: local connection string,
https / http profiles sandbox API keys
ASPNETCORE_ENVIRONMENT=Development
localhost URLs only local SQL Server container
appsettings.json (no secrets)
appsettings.Development.json
(non-secret local defaults)
Azure (deployed)
----------------
App Service settings + Key Vault references, managed identity
If someone needs production-like behaviour locally, add a clearly named profile such as staging-like with ASPNETCORE_ENVIRONMENT=Staging that points at non-production resources.
dotnet run uses the first Project profile.app.Environment.EnvironmentName and the bound URLs at start-up.((IConfigurationRoot)builder.Configuration).GetDebugView() shows each key and its source; never call it in production because it prints values.appsettings the same way the profile does.--no-launch-profile to rule the file out.0.0.0.0 or * exposes the app to every network the laptop joins. Combined with HTTP and the Development exception page, it can leak stack traces and data on shared Wi-Fi.launchSettings.json in code review and run a secret scanner on every pull request.Launch profiles are stable and nothing in the .NET 11 previews changes the advice here. Check the release notes for dotnet run changes when .NET 11 ships.
dotnet new webapi -n Orders.Api and open Properties/launchSettings.json.dotnet run, then dotnet run --launch-profile https, and compare the URLs printed.dotnet user-secrets init, set ConnectionStrings:OrdersDb, and log whether it is present (not its value).environmentVariables and confirm the profile value wins.Production and confirm the user secret disappears.dotnet run --no-launch-profile and note that the app now uses default URLs and the Production environment.The file under review (do not copy):
{
"profiles": {
"Orders.Api": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://0.0.0.0:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production",
"ConnectionStrings__OrdersDb": "Server=orders-prod.database.windows.net;Database=Orders;User Id=orders_app;Password=Winter2026!;",
"PaymentGateway__ApiKey": "live_4f9a2c..."
}
}
}
}
The corrected Properties/launchSettings.json:
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"launchBrowser": false,
"applicationUrl": "https://localhost:7181;http://localhost:5181",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"http": {
"commandName": "Project",
"launchBrowser": false,
"applicationUrl": "http://localhost:5181",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Local secrets instead (values stay in your user profile, outside the repo):
dotnet user-secrets init --project src/Orders.Api
dotnet user-secrets set "ConnectionStrings:OrdersDb" "Server=localhost,1433;Database=Orders;User Id=sa;Password=$LOCAL_SQL_PASSWORD;TrustServerCertificate=True" --project src/Orders.Api
dotnet run --project src/Orders.Api --launch-profile https
Reading it safely in Program.cs:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("OrdersDb")
?? throw new InvalidOperationException("Connection string 'OrdersDb' is not configured.");
var app = builder.Build();
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
app.Run();
Mistake: thinking launchSettings.json configures production.
The file isn't published and the deployed app never reads it, so settings that only live there are missing in CI and production.
Instead: put real configuration in appsettings.json, environment-specific files, App Service settings and Key Vault. Treat the launch profile as a local convenience only.
Mistake: putting secrets in a profile "because it's only local".
The file is committed with the project, so every clone, fork, backup and CI log that prints the repo holds the secret.
Instead: store local values with dotnet user-secrets and use sandbox credentials. Keep production secrets in Key Vault, reached through managed identity.
Mistake: deleting a leaked secret without rotating it.
The value is still in git history, forks and every existing clone, and scanners may already have found it.
Instead: rotate first, audit its use, assess privacy impact, then clean history with git filter-repo or BFG.
Mistake: not knowing configuration precedence.
Profile values become environment variables, which override appsettings.json, appsettings.Development.json and user secrets. A connection string in the profile silently wins over the safe local one.
Instead: keep connection strings out of profiles entirely. When behaviour is surprising, use GetDebugView() locally to see which provider supplied each key.
Mistake: using Production in the default profile.
It turns off user secrets, loads appsettings.Production.json and disables the developer exception page, so developers start pasting production values in to make it work.
Instead: default profiles use Development. If someone must test production-like behaviour, add a separate, clearly named profile that uses Staging and non-production resources.
Mistake: fixing the environment name and thinking the risk is gone. The environment name changes behaviour, but the production connection string is what reaches production data. Instead: remove the production connection string and remove the network path from developer machines to production databases.
Mistake: binding to 0.0.0.0 or * over HTTP.
The app is reachable by anyone on the same network, often with the Development exception page on.
Instead: use https://localhost:<port>;http://localhost:<port> and trust the dev certificate with dotnet dev-certs https --trust.
Mistake: believing user secrets are encrypted. They are plain JSON in the user profile. Anything on the machine running as that user can read them. Instead: use them for development values only, and never for production credentials.
Mistake: expecting dotnet test or CI to use the profile.
dotnet test ignores launch profiles, so tests that pass in the IDE can fail in the pipeline.
Instead: set the environment and configuration in the test itself, for example WebApplicationFactory<Program> with builder.UseEnvironment("Testing").
Mistake: assuming dotnet run uses the profile you see in the IDE.
It uses the first Project profile in the file unless told otherwise, which may not be the one selected in Visual Studio.
Instead: order the profiles deliberately and pass --launch-profile <name> in scripts and documentation.
Mistake: having no team rules for the file. It changes with every developer's experiment and drifts into unsafe settings. Instead: review it like code: Development or clearly named non-production profiles only, localhost URLs, no secrets, and secret scanning with push protection on every pull request.
Review the file for what it enables, not just what it contains. A launch profile that can reach production means production credentials and network access exist on developer machines, which is the real problem; removing the line only hides it. The senior fix is structural: least privilege (developers cannot reach production data), managed identity and Key Vault in Azure, secret scanning with push protection, and an incident process that rotates first and investigates second. That aligns with ACSC Essential Eight thinking on restricting administrative privileges and with Privacy Act obligations if customer data could be exposed.
Keep the boundary clear with CI/CD too. Pipelines and automated tests should never depend on launch profiles: dotnet test does not use them, and integration tests set their own environment explicitly, for example with WebApplicationFactory<Program> and builder.UseEnvironment("Testing") plus test-specific configuration. Deployed environments get configuration from the pipeline's variable groups, App Service settings or Key Vault references. If a value only exists in a launch profile, the app will behave differently in CI and production, which is itself a review finding.
The .NET CLI is the dotnet command that ships with the SDK. It runs apps and drives the SDK's build system, which is MSBuild underneath. dotnet new creates a project, solution or file from a template. dotnet build restores NuGet packages and compiles the project into bin/Debug/net10.0. dotnet run builds if needed and then runs the app, and anything after -- is passed to the app as arguments. dotnet clean deletes the outputs of a previous build. The same commands work on every OS and in CI, and IDEs call the same SDK underneath.
I standardise below the IDE rather than forcing one IDE. The CLI and CI are the source of truth. I pin the SDK with global.json, put style and analyser severities in .editorconfig, centralise build properties in Directory.Build.props and versions with Central Package Management, and commit shared IDE files such as .vsconfig, .vscode/extensions.json and launchSettings.json while ignoring personal state. I offer a Dev Container or a setup script for onboarding, keep secrets in user secrets and Key Vault, and CI runs the same dotnet format, build and test commands developers run locally.
How to tell it (STAR)
**Situation:** On [our orders API], a production connection string ended up in a committed launch profile and a developer accidentally ran a migration against production. **Task:** I led the clean-up and wanted to make it impossible to repeat. **Action:** I had the credentials rotated straight away, worked with [the DBA] to roll back the migration, cleaned the history, moved local config to user secrets and a local SQL container, and pushed for managed identity, Key Vault and push protection. **Result:** No data was lost [confirm in your own story], and since then no secrets have reached the repo.
Last updated 1 Nov 2026.