The Problem with .NET Core Development Configuration
Most of the ecosystems I use - Node.js, Docker/Compose, Terraform - expect configuration key/value pairs to be provided as environment variables. This is in keeping with the philosophy of 12-factor apps.
This means you can either use .env
files (e.g. with Next.js, Docker and Docker Compose, or by using something like dotenv in Node.js projects) and/or set environment variables in the shell (either by explicitly calling export MY_VAR=<value>
or using something like direnv to do this automatically).
In the .NET world however, the convention for providing development-time configuration is to:
- use .NET User Secrets Manager for secrets.
- use
appSettings.*.json
configuration files for everything else.
To complicate matters further, there is also a scaffolded Properties/launchSettings.json
in a .NET Core project and, while originally meant for use in Visual Studio (the big dog), it gets loaded even when you launch your app through a Debug/Launch configuration in VS Code or on the command line. This too can provide config key/value pairs of config data.
All these sources of config data make .NET Core configuration stick out like a sore thumb in polyglot solutions where everything else just uses environment variables.
For example in a solution where, beside a .NET Core API, you have a Next.js frontend and a Docker Compose compose.yaml
to tie the two together for local testing, both Next.js and Docker compose can take in config data in .env
files. In such a file, configuration for the .NET Core API would look something like this:
ASPNETCORE_ENVIRONMENT=Development
ASPNETCORE_URLS=http://localhost:3022
ALLOWED_CORS_ORIGINS=http://localhost:3020
ConnectionStrings__EShop=<redacted>
Just exclude these files from Git by placing *.env
in your .gitignore
, and they won't get checked in. That's your secrets taken care of!
Yet, when launching a .NET Core API in a VS Code Debug/Launch configuration, it is common to use both appSettings.*.json
files and .NET User Secrets Manager, which is really discordant with how everything else is configured: via environment variables or .env
files.
Providing .env
files to VS Code launch configurations
My preferred technique is to store all configuration, secret and non-secret, for a .NET Core project in a VS Code launch configuration in an .env
file. I keep this next to launch.json
in the .vscode
folder:
I provide the path of such an .env
file in envFile
attribute in the launch configuration. For example, consider the compound launch configuration in launch.json
given below. Note the envFile
attributes:
{
"version": "0.2.0",
"compounds": [
{
"name": "Frontend/Backend",
"configurations": [
"Next.js: debug in full stack",
".NET Core: debug in full stack"
],
"stopAll": true
}
],
"configurations": [
{
"name": ".NET Core: debug in full stack",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-backend",
"program": "${workspaceFolder}/flowmazonbackend/flowmazonapi/bin/Debug/net9.0/flowmazonapi.dll",
"cwd": "${workspaceFolder}/flowmazonbackend/flowmazonapi",
"stopAtEntry": false,
"envFile": "${workspaceFolder}/.vscode/flowmazonapi.env",
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
},
"requireExactSource": false
},
{
"name": "Next.js: debug in full stack",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/flowmazonfrontend",
"program": "${workspaceFolder}/flowmazonfrontend/node_modules/next/dist/bin/next",
"args": ["dev", "--port", "3020"],
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.vscode/flowmazonfrontend.env",
"serverReadyAction": {
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome",
"webRoot": "${workspaceFolder}/flowmazonfrontend"
}
},
}
flowmazonapi.env
configures the .NET Core minimal API and looks like this:
ASPNETCORE_ENVIRONMENT=Development
ASPNETCORE_URLS=http://localhost:3022
ALLOWED_CORS_ORIGINS=http://localhost:3020
ConnectionStrings__FlowmazonDB=<redacted>
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-eu-west-2.grafana.net/otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=vscode_launch
OTEL_EXPORTER_OTLP_HEADERS=<redacted>
VS Code takes all the key value pairs in the specified .env
file and sets them as environment variables. In ASP.NET Core, these are read by the environment variable config source in the Host. This is the last and therefore the highest priority config source in ASP.NET host's default sequence, and so override anything else that provides values for the same keys.
So having an .env
file to configure a .NET Core project in a VS Code launch configuration means it works exactly like appSettings.*.json
and .NET Core User Secrets Manager would. For example, the ConnectionStrings__FlowmazonDB=<redacted>
line in the .env
file shown above equates to the following in appSettings.json
:
{
"ConnectionStrings": {
"FlowmazonDB": "<connection string redacted>"
}
}
But using .env
files in VS Code launch configurations instead of the conventional sources of .NET config data works out better because:
- it is consistent with other ecosystems in the solution
- you get the same experience as when you configure .NET Core apps with Docker Compose.
-
it allows you to get away from the many sources of dev config data for .NET Core projects which are stored all over the place.
In particular, you can keep all config data for all projects (both .NET and non-.NET projects) in a launch configuration right next to your
launch.json
in.vscode
folder.And if you have multiple launch configurations that need to be configured differently, you can define separate
.env
files for each of them.
Providing an .env
file to local dotnet
Even when you have one or more VS Code launch configuration to run a .NET Core project, you would likely still need to run the project in its folder on the shell, e.g. to generate or apply EF Core migrations from the DbContext in an API project.
My solution for that is to use direnv to load the .env
file for the API from .vscode
folder, as environment variables within the .NET Core project's own folder on the terminal.
You can set up direnv for this purpose like this:
-
Set up direnv using the two steps given here:
-
Install direnv using your operating system's package manager.
On Windows you could do this by installing WinGet if you don't have it already, then running the command
winget install direnv
in the shell. -
Hook direnv into you shell.
In Bash on Windows, I had to add line
eval "$(direnv hook bash)"
at the end of the file~/.bashrc
where~
is my user profile folderC:\Users\<my windows login name>
.
-
-
Create a file named
.envrc
in the .NET Core project's folder with the following content:
dotenv <relative path to folder containing env file>/<env file name>.env
Normally you would have statements like
export MY_VAR=<value
in the.envrc
file to create environment variables. However, thedotenv
command in the.envrc
file above is telling direnv to go and load the contents of the specified.env
file as environment variables. -
On the shell in your project's folder, run:
direnv allow
-
In general, an
.envrc
file for direnv should not be checked in. This is because it can directly contain environment variables and their values, some of which may be secrets. Even though this not the case with the.envrc
above, as matter of good practice, I would add the following line to the.gitignore
file in solution root:
.envrc
Now whenever you cd
into your .NET Core project's folder on the shell, direnv would run automatically, run the .envrc
file in the folder, and load your .env
file as environment variables.
This means you can run commands like dotnet run
or dotnet ef migrations add
or dotnet ef database update
which require these configuration settings to run.
Also, when you move out of the folder (e.g. cd ..
), the environment variables get unloaded by direnv. (Of course if you close that instance of the terminal, then too the loaded environment variables disappear).
Note
The
*.env.template
files shown in the screenshot above are pre-filled versions of the respective.env
files with secrets redacted (much like the snippets shown above). I do check these in and, together with setup instruction in the project's wiki, they make it easy to recreate the configuration. The*.env
files on the other hand obviously DO NOT get checked in.If you want to use this technique, please make sure to delete the
<UserSecretsId>a-guid-here</UserSecretsId>
element that would be present in yourcsproj
if you have previously used .NET User Secrets Manager with the project. While.env
would be the highest-precedence source anyway, deleting<UserSecretsId>
would avoid surprises down the line such as when you realise that you had forgotten to set certain secrets in.env
and they had been coming from User Secret Manager all along.
Top comments (0)