DEV Community

nausaf
nausaf

Posted on • Edited on

Use .env files for .NET Core development secrets and configuration with VS Code

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:

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>
Enter fullscreen mode Exit fullscreen mode

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"
      }
    },
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>"
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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 folder C:\Users\<my windows login name>.

  2. 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, the dotenv command in the .envrc file above is telling direnv to go and load the contents of the specified .env file as environment variables.

  3. On the shell in your project's folder, run:

    direnv allow
    
  4. 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 your csproj 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)