Table of Content
This blog post will share our experiences at Enlab Software about managing the configuration of environments in .NET core /ASP.NET core applications.
What Is “Environment” in Software Development?
An environment in software development is the collection of hardware, software, and tools needed for building and running the software. Commonly, a popular setup can include development, staging, and production environments.
Using multiple environments ensures that your software is properly tested before deployment and release to users. Therefore, you should remember to always keep your app creation flow organized to maximize the benefit of the Environment.
How Environments Affect the Coding Process
The Environments manage the setting for each environment (app configurations, 3rd party packages, service configurations, or a feature that needs to be enabled/disabled on each environment, etc.). Without a thorough configuration, the application may not function well in each environment and cause developers to rewrite codes.
Therefore, it is crucial to check and update your environment configuration correspondingly before you deploy it to the environment. Fortunately, the .NET core does support detecting the environment and loading the correct configuration by codes.
Configure .NET Core Environments
How .NET Core Support Environment Configurations
.NET core uses environment variables to indicate in which environment the application is running and to allow the app to be configured appropriately. They provide a static class Environment in the system namespace to access the environment variables.
Example: I used the GetEnvironmentVariable method to get the value of the “windir” environment variable as below:
using System;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
var environment = Environment.GetEnvironmentVariable("windir");
Console.WriteLine("windir path: " + environment);
Console.ReadKey();
}
}
}
The result:
You will see the value of the “windir” environment variable via “C:\WINDOWS”.
When using Visual Studio, the environment variable can be specified during the development process in your project’s debug profiles. Below is my example:
How about configuration?
Configuration in .NET core is performed using one or more configuration providers, which will read configuration data from key-value pairs using a variety of configuration sources, such as:
- File configuration (INI, JSON, and XML files)
- Command-line arguments
- Environment variables
- Key per file
- Directory files
- In-memory
Built-In Environments in .NET Core/ASP.NET Core
Example: In the ASP.NET core application, the “ASPNETCORE_ENVIRONMENT” variable and file configuration provider (appsettings.json file) is used by default.
When checking the ASP.NET core project template, you should see that the “ASPNETCORE_ENVIRONMENT” variable with the value “Development” is set by default.
Those are three values that are used by convention: Development, Staging, and Production.
Furthermore, we have two default files with appsettings files (appsettings.json and appsettings.Development.json).
Besides, in the .NET core, you can use the IHostingEnvironment service (Changed to IWebHostEnvironment from .NET core 3) to work with environments and the IConfiguration to work with the appsettings files.
Those services are already provided by the ASP.NET hosting layer and can be injected via Dependency Injection. You can see this in the following ASP.NET project template:
See the source code below:
namespace WebApplication
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
To get the configuration in appsettings files, you can use the IConfiguration service instance as follows:
- AllowedHosts configuration in appsettings.json and appsettings.Development.json files
{
"AllowedHosts": "*"
}
{
"AllowedHosts": "http://localhost:5000"
}
- Access via square brackets or the GetValue method.
See the source code below (for the red highlighted part):
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
var allowedHosts = configuration["AllowedHosts"];
var allowedHosts1 = configuration.GetValue<string>("AllowedHosts");
}
}
As can be seen from above, the AllowedHosts config appears in both appsettings.json and appsettings.Development.json files. However, the value in runtime is in appsettings.Development.json because the app is running in the “Development” environment. Subsequently, asp.net core will replace the value in the appsettings.json with the value in appsettings.{Environment}.json (with the Environment being the value of “ASPNETCORE_ENVIRONMENT” environment variable)
By changing the “ASPNETCORE_ENVIRONMENT” variable value, you can change the configuration value to the appsettings file accordingly.
- If you are using Visual Studio:
You can also run this PowerShell command:
$Env:ASPNETCORE_ENVIRONMENT = "Your environment name"
It is possible to add more appsettings.{Environment}.json for your Environment if needed. See my implementation for the “Staging” environment as follows:
Best Practice to Store and Load Environment Settings
Several Ways to Manage the Environment Settings in .net Core (with Code Example)
Let's try something advanced! Suppose that your application needs to send emails using SMTP, and there are different configurations for each environment. What you can do is store the SMTP configuration in the appsettings files as below:
appsettings.Staging.json file:
{
"ConnectionString": "Server=192.168.2.231;Integrated Security=True;Database=Staging_DB;",
"EmailSettings": {
"SMTPLogin": "staging-email@gmail.com",
"SMTPPassWord": "my-password",
"SMTPPort": "587",
"SMTPHostname": "smtp.gmail.com"
}
}
{
"ConnectionString": "Server=localhost;Integrated Security=True;Database=Dev_DB;",
"EmailSettings": {
"SMTPEmail": "dev-email@gmail.com",
"SMTPPassWord": "my-password",
"SMTPPort": "587",
"SMTPHostname": "smtp.gmail.com"
}
}
Now, how can we retrieve the configuration value at runtime?
As I mentioned above, you can use the IConfiguration service since it allows a way to retrieve configuration values, such as:
Basically, the appsettings.json file accepts any configuration in a JSON format and supports nested objects (such as EmailSettings above). To get the value directly, you can use the path that is separated by a colon (:):
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public WeatherForecastController(IConfiguration configuration)
{
var smtpEmail = configuration.GetValue<string>("EmailSettings:SMTPEmail");
}
}
- Use IConfigurationSection with GetSection Method, Followed by GetValue.
GetSection will return an IConfigurationSection instance that represents a section of application configuration values (in this case, it’s a JSON object in the appsettings file). As can be seen from above, it has returned the “EmailSettings” configuration.
Microsoft team also introduced the Options pattern, which allows us to have strongly typed options. Once configured, they can inject the options into your services. Here is how you can use it:
- Step 1: Define a strongly typed class to hold your configuration (EmailSettings in this case).
namespace WebApplication.Models
{
public class EmailSettings
{
public string SMTPEmail { get; set; }
public string SMTPPassword { get; set; }
public int SMTPPort { get; set; }
public string SMTPHostname { get; set; }
}
}
- Step 2: Register it with IServiceCollection.
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));
}
}
- Final step: Inject the options into your services using the IOptions<T> interface, with T being your defined class:
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using WebApplication.Models;
using Microsoft.Extensions.Options;
namespace WebApplication.Controllers.old1
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
public WeatherForecastController(IOptions<EmailSettings> options)
{
var emailSettings = options.Value;
}
}
}
Below is my result after debugging:
Our Best Configuration Tips
From the above examples, using a defined class stands out as the most effective for us. Besides, it is highly recommended in MS documentation. Reflecting on my experience working with .NET core, I would like to share some configuration tips and trips that can be useful for you:
- Isolate your configuration by separating classes: the scenarios (classes) that depend on configuration settings are only affected by the configuration settings they use.
- Make your defined class setters private: As the configurations are the read-only values and do not change during the application runtime, the private setters make sure that the configuration values can't be changed for any reason.
- Use static properties for the configurations that you know will remain unchanged; otherwise, you will need to reset the app.
- Static properties with private setters.
namespace WebApplication.Models
{
public class EmailSettings
{
public static string SMTPEmail { get; private set; }
public static string SMTPPassword { get; private set; }
public static int SMTPPort { get; private set; }
public static string SMTPHostname { get; private set; }
}
}
- After that, use the GET method with the option “BindNonPublicProperties = true” to set the values, and you’’ be able to access your configuration values everywhere.
public void ConfigureServices(IServiceCollection services)
{
Configuration.GetSection("EmailSettings").Get<EmailSettings>(options => options.BindNonPublicProperties = true);
var smtpEmail = EmailSettings.SMTPEmail;
services.AddControllers();
}
- Register your configuration class instead of using the Options pattern: Options pattern is a good choice when your configuration is unstable and should be recomputed on every request or you want to manage the change options for notifications. However, it is less useful in my opinion because you won’t need it in most cases. If you must use it, please add the comment below:
- Revert the static properties on the EmailSettings class but keep the private setters.
namespace WebApplication.Models
{
public class EmailSettings
{
public string SMTPEmail { get; private set; }
public string SMTPPassword { get; private set; }
public int SMTPPort { get; private set; }
public string SMTPHostname { get; private set; }
}
}
- Register EmailSettings instead of IOptions<EmailSetting>.
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var emailConfigSection = Configuration.GetSection("EmailSettings");
services.AddControllers();
services.AddSingleton<EmailSettings>(
emailConfigSection.Get<EmailSettings>(options => options.BindNonPublicProperties = true)
);
}
}
- If you want to apply changes without restarting the app, you need to change the registration code to use AddScoped instead of AddSingleton:
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var emailConfigSection = Configuration.GetSection("EmailSettings");
services.AddControllers();
// Register for EmailSettings class
services.AddScoped<EmailSettings>(sp =>
{
return emailConfigSection.Get<EmailSettings>(options => options.BindNonPublicProperties = true);
});
}
}
We hope our sharing can help you explore better the .NET core Environment configuration as it has many perks that are yet to be discovered. In our next blog about this topic, we will analyze “how to secure sensitive data in configuration” and provide you with hands-on examples. Stay tuned!
References
- Rick Anderson and Kirk Larkin, Configuration in asp.net Core, docs.microsoft.com, 2020.
- Steve Smith, Working with Multiple Environments, aspnetcore.readthedocs.io.
- Steve Gordon, Using Configuration and Options in .net Core and asp.net Core Apps, pluralsight.com.