Site icon Ryadel

Advanced configuration strategies for ASP.NET Core projects

How to fix the "No executable found matching command dotnet-ef" error in Visual Studio with .NET Core

When setting up the configuration of an ASP.NET Core project, the most common mistake is starting with something simple and, a few months later, finding values scattered everywhere: some in appsettings.json, some in environment variables, some in Docker Compose, perhaps with a few secrets managed manually and duplicated connections in multiple places.

The problem is not just cosmetic: a messy configuration setup makes deployment harder, increases the risk of inconsistencies across environments, and complicates the work of both developers and those responsible for CI/CD, containers, and production.

A much more solid approach, which I personally use in almost all of my projects, is to build configuration around the standard Microsoft.Extensions.Configuration pipeline, defining a clear and predictable provider chain where each layer can override the previous one.

Configuration chain

In a well-structured scenario, configuration can be composed in this order:

The rule is simple: providers added later override those loaded earlier.

This model works well because it naturally separates the different responsibility layers:

  • appsettings.json contains the structure and any non-sensitive default values
  • .env contains the standard values for local development or the Docker stack
  • .env.local handles host machine differences when the app runs outside containers
  • process environment variables cover shell settings, CI/CD pipelines, and runtime overrides
  • Azure Key Vault handles production secrets
  • an optional final composer builds derived values only when they are missing

It is a model I often recommend because it remains readable even as the project grows. As long as the hierarchy is clear, understanding “where a value comes from” stops being a nightmare.

appsettings.json

appsettings.json should describe the shape of the configuration, not store sensitive data. In other words, it is perfectly fine to use it to define sections, typed options, and harmless defaults, while real endpoints, passwords, API keys, and environment-specific hosts should come from later layers.

For example:

This approach keeps the file versionable, portable, and suitable for sharing in the repository without exposing data that should not live there.

JSON, environment variables, and Azure Key Vault conventions

One of the most important aspects is adopting a consistent naming convention across all providers.

In the ASP.NET Core world, the logical key stays the same, but the separator changes depending on the layer:

  • in appsettings.json you use :
  • in environment variables you use __
  • in Azure Key Vault you normally use --

Example:

From the application’s point of view, all these forms point to the same logical key.

This is one of the strongest aspects of the .NET configuration system: application code does not need to know whether a value comes from JSON, an environment variable, or a remote secret. It simply keeps reading Database:Host.

Double underscore or double dash?

The choice is not arbitrary:

  • __ is the convention used by environment variables to represent hierarchical separators
  • -- is the convention commonly used in Azure Key Vault to map the same hierarchy

Defining this rule from the start avoids a lot of ambiguity, especially in teams where development, DevOps, and operations work on the same parameters in different contexts.

The role of .env and .env.local files

.env files are very useful for standardizing local development, especially when using Docker and Docker Compose.

The approach I use is to create two separate files:

  • .env for shared local stack or containerized values
  • .env.local for host-specific overrides

The rationale behind this separation depends on the real-world usage scenarios common to most development teams: typically, databases, queues, or support services run inside Docker, while the application itself is started directly from the IDE or with dotnet run. In these cases, hosts, published ports, and a few local endpoints change. Putting those differences in .env.local avoids cluttering the main file with personal exceptions.

A practical example could look like this:

The key point is that these files should not introduce an alternative logic: they should simply feed the normal environment variable pipeline, so that only one mental model is needed.

When to load the .env.local file

In general, it makes sense to load .env.local only outside containers. If the app is already running inside Docker, that local file should not come into play.

This distinction is important because it prevents an override meant for the developer’s machine from contaminating a containerized environment where the correct hosts are, for example, Docker service names rather than localhost.

Environment variables as an override layer

Environment variables remain the most versatile layer in the whole architecture. They work well locally, in Docker, in CI/CD pipelines, in PaaS services, and in many orchestrators.

In practice, it makes sense to treat them as the standard channel for:

  • temporary overrides
  • deployment parameters
  • environment-specific configuration
  • secrets passed by the runtime or hosting platform

This avoids the proliferation of scenario-specific files. The logic remains the same: same logical key, different provider.

Composite and derived values

A very useful pattern, though often underestimated, is that of composite values. Instead of asking operators to provide both the individual components and the final aggregated value, some keys can be calculated at startup from simpler elements.

The classic example is the connection string:

which can be built from:

  • Database:Host
  • Database:Port
  • Database:Name
  • Database:Username
  • Database:Password

The same reasoning can be applied to authority URLs, full endpoints, or other configurations derived from atomic components.

There is only one correct rule, though: compose the value only if the final key is still empty. If someone has already provided the full value, it must be respected.

This detail makes the difference between a useful solution and an intrusive one: the composer must be an intelligent fallback, not an arbitrary override.

If applied correctly, the approach described above brings the following advantages:

  • it reduces duplication
  • it avoids inconsistencies between components and the final value
  • it simplifies local setup
  • it still leaves room to specify the full value when needed

Local environment configuration

When the project uses Docker Compose, the .env file can become the central point for feeding both compose file interpolation and the variables passed to application containers.

This makes it possible to manage in one place:

  • the Compose project name
  • the .NET environment
  • public ports
  • application binding
  • support service parameters

For example:

There is also a useful distinction to make here. Not all variables present in Docker Compose necessarily have to follow the naming conventions of ASP.NET Core option classes. Some images require upstream-defined names, often uppercase and with non-negotiable syntax. In those cases, it makes sense to accept the constraint while keeping the rest of the configuration consistent.

Azure Key Vault for production secrets

In production, moving secrets to Azure Key Vault is a natural choice for many ASP.NET Core projects hosted in Azure or integrated with the Microsoft ecosystem.

The registration order matters a lot: the Key Vault provider should be added after environment variables and before any derived value composition.

A typical example:

This way remote secrets can replace previous values, while composite values can still use whatever is available at the end of the chain.

Keys stored in the vault should follow the double-dash convention:

The real advantage is that the application code does not change. Only the provider feeding those same logical keys changes.

Configuration sections

A well-designed configuration is not just a matter of providers, but also of internal structure. In general, it makes sense to split parameters into coherent sections, each dedicated to a specific responsibility.

Some common examples:

  • Database for host, port, database name, and credentials
  • Authentication or Identity for OIDC, OAuth, or federated login parameters
  • Storage for object storage and file services
  • Messaging for brokers, queues, and asynchronous systems
  • Cache for Redis or equivalent systems
  • Email for SMTP or transactional providers
  • Logging and Telemetry for observability and diagnostics
  • Security for keys, encryption, policies, and sensitive settings

It is best to avoid monolithic sections or overly generic names. When everything ends up inside a container such as Settings or App, the result is almost always a loss of clarity.

Common mistakes to avoid

Regardless of the configuration model you choose to adopt, it is important to avoid some very common mistakes which, at first, may look like harmless shortcuts but tend over time to turn into operational issues, inconsistencies between environments, and maintenance headaches.

  • Putting secrets in appsettings.json. It is still extremely common. It works, sure, but it immediately creates security, versioning, and portability issues.
  • Duplicating the same data in multiple formats. If a full connection string coexists with host, port, database, username, and password, you need to define clearly which one is authoritative. Without a precise rule, sooner or later those values will drift out of sync.
  • Mixing application configuration and infrastructure details. Some parameters belong to the application, others to the container, and still others to third-party images. They need to be kept separate, even if they live in the same ecosystem.
  • Using localhost everywhere. localhost works fine on the host machine, but almost never inside a container. If local and containerized layers are not clearly separated, problems show up immediately.
  • Ignoring provider order. The ASP.NET Core configuration pipeline is simple, but only as long as the order is intentional. If a provider is added in the wrong place, overrides stop behaving as expected.

Conclusions

Configuring an ASP.NET Core project properly does not just mean filling appsettings files with parameters, but designing a pipeline that is coherent, readable, and sustainable over time. The combination of appsettings.json, .env files, local overrides, environment variables, external secrets, and derived values is a very effective model because it separates responsibilities, reduces duplication, and keeps application code independent from the actual source of the data.

It is a solution that scales well from small projects to more structured scenarios involving Docker, CI/CD, and cloud environments. Most importantly, it avoids something that, in day-to-day operations, weighs much more than it may seem: having to remember exceptions, implicit conventions, and shortcuts accumulated over time. If configuration is designed well from the beginning, the rest of the project tends to age better too.

Exit mobile version