In this article, we’re going to cover how to set up and manage multiple environments in ASP.NET Core. If we expect our project to become large and complex, it is a good idea for us to separate our environments. For example, we don’t want to develop software on the same server that hosts our live application. Similarly, we don’t want to run tests on our live application. These bad practices could cause our users to see debugging pages, dummy data, altered data, etc. To help us avoid these issues, we’ll look at how to set up multiple environments to manage what kind of error page is displayed for an ASP.NET Core application.

To download the source code, visit our Multiple Environments repository.

Let’s dive in.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Creating a Project

Before we continue, let’s create a new MVC project using the ASP.NET Core CLI. This command will create a project with all the basic project and frontend files we are going to use in this example:

dotnet new mvc

Our project should now have the following project structure:

The default project structure of a new ASP.NET Core MVC Project - Environments

Now that we’ve created the project, let’s go through some of the key files that we are going to visit in this article.

The Program.cs file is the starting point of the ASP.NET Core application. We are going to modify this file later, but for now, let’s make sure that this is the code that was generated:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

The Program class uses the Startup.cs file to configure the application. To configure the application, let’s modify the Configure method inside Startup.cs.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseDeveloperExceptionPage();

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

The UseDeveloperExceptionPage method configures the application to call the debugging page whenever an error occurs.

To test this feature out we are going to modify the Privacy action inside the HomeController class:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    public IActionResult Index()
    {
        return View();
    }

    public IActionResult Privacy()
    {
        throw new Exception();
    }
}

Whenever we attempt to go to the privacy page, our controller will simulate an error by throwing an exception.

Before we do a test run, let’s modify the launchSettings.json file inside the Properties directory. We are going to remove all the environmentVariables key-value pairs under all the profiles. Later, we will talk about what those lines of code do, but for now, let’s remove them:

{
  "iisSettings": {
    "windowsAuthentication": false, 
    "anonymousAuthentication": true, 
    "iisExpress": {
      "applicationUrl": "http://localhost:34078",
      "sslPort": 44321
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true
    },
    "MultipleEnvsExample": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000"
    }
  }
}

Let’s run it to make sure it works:

dotnet run

As soon as we navigate to https://localhost:5001/Home/Privacy, we are going to see the debugging page:

Error page with debugging information - ASP.NET Core Environments

Of course, this is a great resource when we need to know what went wrong. However, we don’t want our users to see this page.

So, how do we deal with this problem?

We are going to solve this problem by using multiple environments.

The Environments

A hosting environment consists of OS-level definitions that the application uses to determine how it’s going to run. Let’s take a look at the three most common environments:

  • Development – We can use this environment to write code, commit code, fix bugs, etc. For example, we want our error page to give us some debugging information to help us write code.
  • Production – We can use this environment to publish our application to our end users. For example, instead of debugging information, we want our error to display generic error information to alert our user that something went wrong.
  • Staging – We can use this environment to test our application using automated or manual tests. Staging environments are often very similar to the production environment. For example, if a test fails while requesting a page, we want the tester to see the same generic error information that a user would see. Because it a staging environment, we may also want an additional message displaying the name of the failed test.

The Environment Variables

The way we are going to control which environment we want to use is through the environment variables. All modern operating systems offer a way to define environment variables.

So, what exactly is an environment variable?

An environment variable is a variable whose value is set outside of the application, hence the term “environment.” ASP.NET Core relies on several environment variables, but the one we are going to look at in this article is ASPNETCORE_ENVIRONMENT. ASP.NET Core reads this environment variable at startup and stores the value to allow us to write code that is dependent on this value.

Now, let’s talk about case sensitivity:

Environment variable values are not case sensitive on Windows and macOS but are case sensitive on Linux. This means that on Linux the value of Development would not be the same as development. It is good practice to keep all references to environment values consistent throughout the project. For the sake of this project, we are going to use the following values: Development and Production.

Setting the Environment Variable

There are multiple ways of setting a value to the environment variable that may vary by the operating system. For the sake of this project, we are going to work on a UNIX-based operating system.

Setting the Environment Variable with UNIX

We are going to open a terminal window and run:

export ASPNETCORE_ENVIRONMENT=Development

To see if it worked, we are going to run:

echo $ASPNETCORE_ENVIRONMENT

When we run the command we should see the value stored in that variable:

Development

When we run the application we are going to see that the application is running on the development environment:

Hosting environment: Development - ASP.NET Core Environments

However, it’s important to note that the default environment in ASP.NET Core is Development. So, now we are going to change it to Production:

export ASPNETCORE_ENVIRONMENT=Production

dotnet run

When we run the application we are going to see that application is running on the production environment:

Hosting environment: Production - ASP.NET Core Environments

Now we know how to set the environment variable through the OS!

Using CLI Arguments to Set the Environment Variable

Another way to set the environment variable is by feeding the value directly to the dotnet run command as an argument.

To set this up, let’s modify Program.cs:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureHostConfiguration(configHost =>
        {
            configHost.AddCommandLine(args);
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

In the CreateHostBuilder method, we are setting up the builder’s configuration using ConfigureHostConfiguration. We are configuring the configuration with the AddCommandLine method to allow it to read configuration values from the command line.

By default, the host builder will add the ASPNETCORE_ prefix to the argument key that is provided. For example, if we run dotnet run --environment Development then the application will set ASPNETCORE_ENVIRONMENT to Development. When we try it out we are going to see that the application will override the variable’s previous value (Production) with Development:

Hosting environment: Development - ASP.NET Core Environments

Using Launch Settings to Set the Environment Variable

Now that we know how to set the environment variable through the OS and through CLI arguments, let’s take a look at another way. In this section, we are going to set it through the launchSettings.json file. This method will override every other configuration.

This is a useful way of setting environments because it allows us to organize multiple profiles with their respective project settings and environment variables.

Now, we are going to define profiles that can be used when we run our application. Let’s begin by modifying the profiles section of launchSettings.json:

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:34078",
      "sslPort": 44321
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true
    },
    "Development": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "Production": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Production"
      }
    }
  }
}

Within the Development profile, we are defining the ASPNETCORE_ENVIRONMENT with a value of Development. When we execute the dotnet run command, the value in the launch profile will overwrite the environment variable we define through the terminal. By default, the command will launch the first profile it finds with a commandName value of Project.

So we are expecting the application to run in the Development environment when we execute the dotnet run command:

Hosting environment: Development - ASP.NET Core Environments

To run the application in the production environment, we are going to add an argument --launch-profile Production.

Now, let’s try to run the application in the production environment:

dotnet run --launch-profile Production

Hosting environment: Production - ASP.NET Core Environments

On Visual Studio for Mac, we are going to see that modifying the launchSettings.json file will give us the option to choose between our launch profiles:

Options to run in Production or Development - ASP.NET Core Environments

Additionally, we can define the value of the environment by navigating to the “Project” menu and selecting “MultipeEnvsExample Options”:

Project > MultipleEnvsExample Options - ASP.NET Core Environments

Then, under the “Configurations” menu, we can choose the profile we want to configure:

Run > Configuration > Production > Environment Variables - ASP.NET Core Environments

Now, we know how to define and organize multiple environments!

So far, so good!

Accessing the Environment in the Startup Class

Now, we are going to see how to use our environments to manipulate our application. Currently, our application displays a debugging page whenever there is an error. This is great while we’re working in the development environment.

But, what about the production environment?

For the production environment, we want to display a custom error page. To do this, we are going to explore two methods: dependency injection and method conventions in the Startup class.

But first, we are going to create our custom error page.

Let’s create the ErrorViewModel class under the Models directory if we don’t already have one:

public class ErrorViewModel
{
    public string RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}

We are defining the ErrorViewModel with a property of type string and a property of type boolean. We are going to use this model in our error page.

Let’s create the Error.cshtml file under the Views/Shared directory if we don’t already have one:

@model ErrorViewModel
@{
    ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (Model.ShowRequestId)
{
    <p>
        <strong>Request ID:</strong> <code>@Model.RequestId</code>
    </p>
}

When the error view is requested, we are going to see a generic message and the id associated with that request.

Now, let’s create an action method that returns the error view by adding a new action to the HomeController.cs:

public IActionResult Error()
{
    return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

After we start our application and navigate to https://localhost5001/Home/Error and we should see our error page:

Home/Error - Error: An error occurred while processing your request - ASP.NET Core Environments

By Dependency Injection in the Startup Class

However, we want our error page to display when an error occurs in the production environment. The simplest way to do this by taking advantage of dependency injection in the Startup class.

Let’s go ahead and modify Startup.cs:

public void Configure(IApplicationBuilder app, IHostEnvironment env)
{
    if (env.IsEnvironment("Development"))
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }
    // more code below
}

Using the DI we supply an argument of type IHostEnvironment into the Configure method. Then we use a simple if-else structure to determine which page is displayed by using the IsEnvironment method. This method checks which environment we are using. If it is set to Development then we use the developer exception page. For any other environment, we use an exception handler pointing to the custom error page.

After starting the application in the production environment and clicking on Privacy, an error is simulated and we see our custom error page:

Home/Privacy - Error: An error occurred while processing your request. - ASP.NET Core Environments

It’s also possible to use the DI in the Startup class itself rather than just in the Configure method for a wider-scoped IHostEnvironment variable:

private readonly IHostEnvironment _env;
public Startup(IConfiguration configuration, IHostEnvironment env)
{
    Configuration = configuration;
    _env = env;
}

Now, it’s accessible throughout the entire class!

By Startup Method Conventions

As our project gets more complicated our if-else structure will get bloated and difficult to maintain. To avoid this, we are going to use the Startup method conventions.

How does this convention work?

There are two key methods in the Startup class: Configure and ConfigureServices. However, in the Startup class, these methods support environment-specific versions. For example, in the development environment, ASP.NET Core would look for the ConfigureDevelopment and the ConfigureDevelopmentServices methods before the default Configure and ConfigureServices methods.

Let’s try to implement this convention by rewriting Startup.cs:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void SharedConfigureServices(IServiceCollection services){
        services.AddControllersWithViews();
    }
    public void ConfigureDevelopmentServices(IServiceCollection services)
    {
        SharedConfigureServices(services);
    }
    public void ConfigureServices(IServiceCollection services)
    {
        SharedConfigureServices(services);
    }

    public void SharedConfigure(IApplicationBuilder app)
    {
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
    public void ConfigureDevelopment(IApplicationBuilder app)
    {
        app.UseDeveloperExceptionPage();
        SharedConfigure(app);
    }

    public void Configure(IApplicationBuilder app)
    {
       app.UseExceptionHandler("/Home/Error");
       app.UseHsts();
       SharedConfigure(app);
    }
}

There’s a lot of changes that were made, but we are going to work our way through them slowly.

First, we define the SharedConfigureServices method. This method contains the common configuration related to the application’s services across all environments. The application calls the ConfigureDevelopmentServices method when we are in the development environment. In any other environment, the application calls the ConfigureServices method.

Next, we define the SharedConfigure method. This method contains the common configurations related to the application’s request pipeline across all environments. The application calls the ConfigureDevelopment method when it is set to development. In any other environment, the application calls the Configure method. The default Configure method configures the application with the custom error page. The ConfigureDevelopment method configures the application with the debugging page.

When we run the application in the production environment and navigate to the privacy page, we are going to see our custom error page:

Home/Privacy: Error: An error has occurred while processing your request. - ASP.NET Core Environments

When we run the application in the development environment and navigate to the privacy page, we are going to see the debugging page:

Error page with debugging information - ASP.NET Core Environments

Accessing the Environment with the Tag Helper

There’s one more way to use our environments. Using them in the Startup class is great for making project-wide changes.

But what if we only want to make small changes?

Now, we come to the tag helper.

This is a great way to take advantage of our environment to make small changes to our code. For example, we want to display a little extra information on the error page when the environment is set to Testing.

To do this, let’s modify Error.cshtml by adding this code to the bottom of the file:

<environment names="Testing,">
<h3>Testing Mode</h3>
<p>
    Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
    <strong>The Development environment shouldn't be enabled for deployed applications.</strong>
    It can result in displaying sensitive information from exceptions to end-users.
    For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
    and restarting the app.
</p>
</environment>

The environment tag checks if the environment variable is set to one of the values in the names attribute. When we are in the testing environment all the content within the tag is going to be rendered.

To test this, let’s go ahead and add a testing profile to launchSetting.json:

"Testing": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Testing"
      }
}

And that’s it!

After we start our application with the testing profile and navigate to the privacy page, we should see our custom error page with the additional content unique to the testing environment:

Home/Privacy - Error: Testing Mode - ASP.NET Core Environments

Conclusion

We have successfully defined, organized, and implemented environments in our ASP.NET Core application. These are useful tools when you start deploying your application to testing, staging, or production servers. Remember, you can define as many environments as needed and name them whatever you think is most convenient.

In summary, we have covered how to use:

  • Environments
  • Environment variables
  • CLI arguments to read environments at runtime
  • Environments via the Startup class
  • Environments via tag helpers
Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!