CLI tool
tonerdo/dotnet-env avatar
tonerdo/dotnet-env

dotnet-env: loading .env files into .NET environment variables

A .NET library to load environment variables from .env files

564 stars52 forksC#MIT

At a glance

What is it?
dotnet-env reads .env files and puts their contents into the process environment, with the three behaviours that matter spelled out as flags: whether to set variables at all, whether to overwrite existing ones, and whether to search parent directories. It also plugs into ConfigurationBuilder and supports the shell-style interpolation you would expect from a .env file.
Who is it for?
dotnet-env fits a .NET service that wants a .env file in development while production configuration arrives as real environment variables, and it fits a team that wants the shell syntax people already know, since export keywords, quoting and ${VAR:-default} interpolation are all supported.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 180 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A .env file is a configuration file that pretends to be the environment

dotnet-env is described as a .NET library to load environment variables from .env files, supporting .NET Core and .NET Framework from 4.6 onwards. The licence is MIT and the package is published on NuGet as DotNetEnv.

The problem it solves is the gap between how a developer configures an application and how a hosting platform configures it. On a laptop you want a file you can edit and commit to a repository you do not commit, and on a platform you want environment variables set by the deployment. Most .NET frameworks solve this by adding a configuration provider layer, and this library takes the narrower route: it parses a .env file and writes its contents into the process environment, so the rest of your code reads them with System.Environment.GetEnvironmentVariable and nothing else changes.

That narrowness is the design, and it has consequences. The library is not a configuration system, it is a loader with a .env parser attached. If your settings are nested, typed and bound to sections, a .NET configuration provider is the better tool, and this project has one of those too, described later.

The repository is on the master branch, the most recent release is v3.2.0 from 2026-04-26 after v3.1.1 in August 2024, and the last push was on 2026-04-26. The tree is small, with src and test directories, a DotNetEnv.sln, a global.json, an AppVeyor configuration and a file called .env_much_higher, which is a fixture for the path traversal tests. The AppVeyor badge in the README still points at the older rogusdev organisation, a leftover from the repository's earlier home.

Two ways to install it

The package is on NuGet, and the README gives the two commands you need depending on your tooling.

In Visual Studio's package manager console:

powershell
PM> Install-Package DotNetEnv

And from the .NET Core command line:

bash
dotnet add package DotNetEnv

That is the entire installation. There is no configuration file to wire up and no startup hook to register, which is the point of the library.

The reference to .NET Framework 4.6 in the description is worth a moment's attention. It means the library still compiles for the older .NET Framework line, which is what makes it usable in the kind of large studio or enterprise codebase that has not moved. The interpolation caveat noted later in the README, about netstandard2.0 and empty environment variables, is the practical edge of that compatibility.

For a library whose whole job is one call at startup, the question worth asking is whether the alternative of a dozen lines of your own parsing code is worth the dependency. The case for the dependency is the parser: the .env format has enough edge cases, as the grammar section of the README shows, that a maintained parser is worth more than the call site costs.

Loading a file, a stream, a string, or several files in order

The Load method comes in a few shapes, and the default one has a behaviour worth knowing.

Load() will automatically look for a .env file in the current directory by default, or any higher parent or ancestor directory if given the option flag via TraversePath():

csharp
DotNetEnv.Env.Load();
DotNetEnv.Env.TraversePath().Load();

You can also specify the path directly, and as above with TraversePath() it will start looking there and then look in higher directories from there if not found:

csharp
DotNetEnv.Env.Load("./path/to/.env");

Then there are the content variants. You can load the file as a Stream, or load a string of contents, or load several files in sequence:

csharp
using (var stream = File.OpenRead("./path/to/.env"))
{
    DotNetEnv.Env.Load(stream);
}

DotNetEnv.Env.LoadContents("OK=GOOD\nTEST=\"more stuff\"");

// will use values in later files over values in earlier files
// NOTE: NoClobber will reverse this, it will use the first value encountered!
DotNetEnv.Env.LoadMulti(new[] {
    ".env",
    ".env2",
});

That last comment is the important line for anyone layering configuration. LoadMulti takes later files over earlier ones, and NoClobber reverses the rule so the first value encountered wins. That is how you build a configuration chain in the conventional order, with a base file and then an environment-specific file overriding it, or the reverse if you want the local file to win.

The Stream overload is the one to reach for in a container or a service that receives its configuration from somewhere else, since it means the file never has to exist on disk.

Reading values back out, typed and with defaults

Once loaded, the variables are ordinary environment variables, which the README shows through the standard class:

csharp
System.Environment.GetEnvironmentVariable("IP");

There are also helper methods for the common types:

csharp
DotNetEnv.Env.GetString("A_STRING");
DotNetEnv.Env.GetBool("A_BOOL");
DotNetEnv.Env.GetInt("AN_INT");
DotNetEnv.Env.GetDouble("A_DOUBLE");

The typed getters matter more than they look. Reading a boolean or an integer out of the environment means parsing it yourself at every call site, and every call site gets it slightly wrong. Centralising the conversion means the question of what happens when the variable is missing is answered in one place.

And that question is answered. The helper methods also have an optional second argument which specifies what value to return if the variable is not found:

csharp
DotNetEnv.Env.GetString("THIS_DOES_NOT_EXIST", "Variable not found");

The alternative, and the one the library recommends, is to read the raw values through System.Environment and deal with the null yourself, which keeps the library out of your type conversion logic. Which of the two you should use depends on how much you value having one place that knows about the environment's shape.

Three flags decide what Load actually does

Everything about the load behaviour is controlled by a LoadOptions object with three parameters, and the README is clear that all of them default to true.

The first is setEnvVars, which must be true in order to actually update environment variables. Setting it to false allows consumers of the library to process the .env file but use it for things other than updating env vars, as a generic configuration file. The Load methods all return an IEnumerable<KeyValuePair<string,string>> for this, with an extension method ToDotEnvDictionary() to get a dictionary with the last value for each key:

csharp
var kvps = DotNetEnv.Env.Load(
    options: new DotNetEnv.Env.LoadOptions(
        setEnvVars: false
    )
)

// or the recommended, cleaner (fluent) approach:
var dict = DotNetEnv.Env.NoEnvVars().Load().ToDotEnvDictionary();

The second is clobberExistingVars, where true means always set env vars and false would leave existing env vars alone. The third is exactPathOnly, where true requires .env to be in the current directory if not specified, or to match the exact path passed in, and false would traverse the parent directories above the current or given path to find the nearest .env file. That option only applies to Env.Load that takes a string path.

The constructor form is workable, but the README says the recommended approach is a fluent syntax for turning flags off, such as:

csharp
DotNetEnv.Env.NoEnvVars().NoClobber().TraversePath().Load();

One subtlety is easy to miss. With CreateDictionaryOption you can change the behavior of ToDotEnvDictionary to take either the First value or to throw on duplicates, and with the TakeFirst options you can simulate NoClobber behaviour. So the dictionary built from a multi-file load can be told to keep the first value, which is the opposite of what the environment itself ends up with under the default settings.

That default of true for clobbering is the one to think about. In a container platform that injects a value like a connection string as a real environment variable, a .env file present in the image will overwrite it unless you ask otherwise.

It also works as a ConfigurationBuilder source

For a project that uses the standard .NET configuration stack, the README says integrating with the usual ConfigurationBuilder is simple:

csharp
var configuration = new ConfigurationBuilder()
    .AddDotNetEnv(".env", LoadOptions.TraversePath()) // Simply add the DotNetEnv configuration source!
    .Build();

One detail makes this usable rather than merely possible. The configuration provider will map __ as : to allow sections, which is the convention that lets a flat .env file populate a nested configuration object. Without that mapping, every setting would land in one undifferentiated section and binding to a class with nested properties would fail.

So there are two ways to use the library, and they suit different situations. The environment variable route writes into the process environment and lets any code read the value, which is what you want when you want the .env file to behave exactly like platform-injected configuration. The configuration provider route keeps the values in the configuration system, which is what you want when the rest of your application reads IConfiguration and the .env file is just another source in the chain.

Choosing the provider route also sidesteps the clobbering question entirely, since the configuration system has its own precedence rules and you decide where the .env source sits in the chain.

The .env grammar, and the one place it differs from the shell

The file format section is the longest part of the README, and for good reason, because the parser has to accept what shells and deployment tools produce.

The base rule is strict. All lines must be valid assignments or empty lines, with optional comments. A minimal valid assignment is KEY=value. There can optionally be one of a few export or equivalent keywords at the beginning, there can be a comment at the end, values can be quoted to include whitespace, and interpolated references can be included, in unquoted values as well as double quoted ones, with optional braces in both cases:

sh
export KEY="extra $ENVVAR value" # comment
set KEY2=extra${ENVVAR}value # comment

The export keywords it accepts are export for bash, set for windows cmd, SET for windows cmd and set -x for fish. That set is what allows the .env file itself to be source-d, as the README puts it, with . .env.

Braced interpolation is where it goes furthest. ${ENVVAR:-default} and ${ENVVAR-default} give a default value, ${ENVVAR:?error} and ${ENVVAR?error} exit with an error if the variable is unset, and ${ENVVAR:+alternative} and ${ENVVAR+alternative} give an alternative value when the variable is set. Interpolation can be nested, with forms like ${VARIABLE:-${FOO:-default}}.

There is one documented divergence from the shell, and it comes from the platform rather than the design. The README notes that using the colon syntax before a default value, error or alternative value typically indicates that the interpolated variable is allowed to be empty, that netstandard2.0 does not support this and treats empty environment variables as unset, and that this library currently does not support the if set and non-empty behaviour, although the syntax itself is supported.

So on a runtime where an empty environment variable is indistinguishable from an unset one, ${VAR:-default} and ${VAR-default} behave the same way here even though a shell would treat them differently. That is a small surprise waiting for anyone who relies on the distinction, and the README flags it explicitly rather than leaving you to discover it.

Editorial conclusion

dotnet-env fits a .NET service that wants a .env file in development while production configuration arrives as real environment variables, and it fits a team that wants the shell syntax people already know, since export keywords, quoting and ${VAR:-default} interpolation are all supported. It is the wrong choice if your configuration is genuinely structured rather than flat key-value pairs, because the library maps a file onto environment variables rather than onto a configuration tree, and the wrong choice if you need to keep variables out of the process environment, since the setEnvVars flag exists but the library's whole model assumes the environment as the destination. Verify first which of the three load flags you want, because all of them default to true and the clobbering behaviour in particular can silently replace a value your platform injected.

Frequently asked questions

What does the dotnet-env library do?

It loads environment variables from .env files into the process environment, so the values can then be read through System.Environment.GetEnvironmentVariable or through its typed helpers. It supports .NET Core and .NET Framework 4.6 and newer, and is published on NuGet as DotNetEnv under the MIT licence.

How do I install dotnet-env?

With dotnet add package DotNetEnv from the .NET Core command line, or with Install-Package DotNetEnv in the Visual Studio package manager console. The package is published on NuGet as DotNetEnv.

How do I stop a .env file from overwriting environment variables that already exist?

Use the clobberExistingVars option set to false, or the fluent DotNetEnv.Env.NoClobber().Load(). The README notes that all LoadOptions parameters default to true, so clobbering existing variables is the default behaviour.

Does dotnet-env support ${VAR:-default} style interpolation?

Yes, including defaults, errors and alternatives in both braced and unbraced forms, and nested interpolation. The README notes one caveat: netstandard2.0 treats empty environment variables as unset, so the library does not support the if set and non-empty behaviour of the colon syntax, although the syntax itself is supported.

Can I use dotnet-env with ConfigurationBuilder?

Yes. The README shows adding AddDotNetEnv(".env", LoadOptions.TraversePath()) to a ConfigurationBuilder, and says the configuration provider will map __ as : to allow sections, so a flat .env file can populate nested configuration.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. tonerdo/dotnet-env on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tonerdo-dotnet-env.svg)](https://hysenlabs.com/projects/tonerdo-dotnet-env)

Was this useful?