How to manage Configuration in ASP.Net Core

Configuration management is something we cannot do without. It is a must-have feature. In this blog post, I am going to discuss how to manage and access configuration in ASP.Net Core applications. I will use ASP.Net Core 3.1 for demonstrating the example here.

Configuration management in ASP.Net Core has improved significantly compared to how it was managed in the legacy .Net Framework.

Firstly, it is saved as JSON instead of XML compared to the legacy .Net Framework. This makes reading and managing the configuration file extremely simple and user friendly.

Creating a new ASP.Net Core Application

To start this example, first of all, I will create a new ASP.NET Core 3.1 Web API Application.

Firstly, to do that, I will open up Visual Studio 2019. Once Visual Studio opens up, I will select the menu File -> New -> Project. This will open the Create a new Project project popup window.

Secondly, in the Create a new Project popup window, I will select ASP.NET Core Web Application from the project template and click on the Next button.

Thirdly, on the next page, I will provide the name of the application as Config.Demo and click on the Create button.

Finally, on the final page, I will select the API template option. And I will keep other values default (ASP.Net Core 3.1) and click on the Create button.

The configuration file

By default, the Visual Studio project template will create an appsettings.json and appsettings.Development.json file when a new project is created.

These are the files where we save configurations based on the environment, where appsettings.json is the default configuration file. The environment name is usually available as an environment variable, with key “ASPNETCORE_ENVIRONMENT”.

CreateDefaultBuilder

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace Config.Demo
{
    public static 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>();
                });
    }
}

Inside of the Program class, inside of the CreateHostBuilder function, we make a call to Host.CreateDefaultBuilder. If we peek into the definition of the CreateDefaultBuilder you can see it does the following:

configuration
  • Firstly, It sets the ContentRootPath of the IHostEnvironment to the current directory
  • Secondly, It goes and figures out the appsettings.json configuration file
  • And then also it loads the appsettings.{environment}.json config
  • Thirdly, it goes and it loads environment variables
  • Fourthly, it loads anything passed to the arguments

Accessing configurations from Appsettings

For reading configurations from appsettings.development.json, firstly, let us add a couple of entries to the appsetting.development.json.

Now, for the first entry, I will add a key Version, which will have a decimal value associated with it.

Next, for the second entry, I will add a key Settings, which will have a complex object associated with it.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Version": "1.0",
  "Settings": {
    "Database": "SQL"
  }
}




Now we will go to the Startup class, and try to access these two configuration items from the ConfigureServices method and print to the console as output.

To do that, I will access these values inside of the ConfigureServices method of the Startup class. And there are two ways to access the values from the configuration file.

  • Firstly using Configuration.GetValue
  • Secondly using the indexer on Configuration property
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Config.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            var version = Configuration.GetValue<string>("Version");
            var db = Configuration["Settings:Database"];

            Console.WriteLine(version);
            Console.WriteLine(db);
        }

        // 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();
            });
        }
    }
}

Now if I run the application, I will see the Version and Database are printed in the console.

And as you can see from the code, we can access multiple layers of configuration values by separating the keys with :, for example, here we are using Settings:Database to access the value of Database which is at the next level of Settings.

Accessing configurations from Controller

Now that we are able to access configuration inside of the Startup class, we can add the configurations to the dependency injection container and access it from the controller.

To do that I will update the WeatherForecastController class and inject string version to the constructor. And I will update the implementation of the Get method to return this version.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace Config.Demo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private readonly ILogger<WeatherForecastController> _logger;
        private readonly string version;

        public WeatherForecastController(
            ILogger<WeatherForecastController> logger,
            string version)
        {
            _logger = logger;
            this.version = version;
        }

        [HttpGet]
        public string Get()
        {
            return version;
        }
    }
}

Once the controller is ready, I will go to the Startup class to add the Version configuration to the dependency injection. To do that we can write the following code in the ConfigureServices method:

services.AddSingleton(version);

This will add the Version as a sting to the dependency injection container.

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Config.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            var version = Configuration.GetValue<string>("Version");
            var db = Configuration["Settings:Database"];

            services.AddSingleton(version);

            Console.WriteLine(version);
            Console.WriteLine(db);
        }

        // 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();
            });
        }
    }
}

Now, if I run the application and access /weatherforecast, I will see that version 1.0 is returned to the output.

accessing config from controller
accessing config from controller

Access complex configuration object

Now let us add another configuration section in the asspettings.development.json configuration file. And I will name this configuration as Custom, and it will be a multilevel configuration item. And Custom have ConfigValue as child level configuration.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Version": "1.0",
  "Settings": {
    "Database": "SQL"
  },
  "Custom": {
    "ConfigValue": "Custom configuration"
  }
}

Once the configuration item is added, firstly, I will create a class called Custom. And inside the class, I will have a single property ConfigValue of type string.

namespace Config.Demo
{
    public class Custom
    {
        public string ConfigValue { get; set; }
    }
}

Secondly, I will go to the Startup class and add this configuration value to the dependency injection container. To do that, I will use the Bind function of the IConfigurationSection instance, which is returned from the Configuration.GetSection method.

var custom = new Custom();
Configuration.GetSection("Custom").Bind(custom);

Thirdly, I can add this Custom object to the dependency injection container using services.AddSingleton method.

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Config.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            var version = Configuration.GetValue<string>("Version");
            var db = Configuration["Settings:Database"];

            services.AddSingleton(version);

            var custom = new Custom();
            Configuration.GetSection("Custom").Bind(custom);
            services.AddSingleton(custom);

            Console.WriteLine(version);
            Console.WriteLine(db);
        }

        // 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();
            });
        }
    }
}

Fourthly, I will go to the WeatherForecastController class and inject the Custom object to the controller. And then I will update the Get method to return the value of ConfigValue property of the Custom class as output.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace Config.Demo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private readonly ILogger<WeatherForecastController> _logger;
        private readonly string version;
        private readonly Custom custom;

        public WeatherForecastController(
            ILogger<WeatherForecastController> logger,
            string version,
            Custom custom)
        {
            _logger = logger;
            this.version = version;
            this.custom = custom;
        }

        [HttpGet]
        public string Get()
        {
            return custom.ConfigValue;
        }
    }
}

Finally, if I run the application, I will see the value of ConfigValue, which is “Custom configuration” returned from the /weatherforecast API.

The more simplified way to add configuration value to the dependency injection container is to use the Configure method of the IServiceCollection method, instead of using the Bind feature. But in that case, the dependency injection container will inject a generic IOptions object for the type.

To demonstrate that, firstly, I will update the implementation of the Startup class’s ConfigureServices method to use the following code to configure the Custom configuration section to the dependency injection container.

services.Configure<Custom>(Configuration.GetSection("Custom"));

Updated code for the Startup class is as follows:

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Config.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            var version = Configuration.GetValue<string>("Version");
            var db = Configuration["Settings:Database"];

            services.AddSingleton(version);

            services.Configure<Custom>(Configuration.GetSection("Custom"));

            Console.WriteLine(version);
            Console.WriteLine(db);
        }

        // 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();
            });
        }
    }
}

Secondly, I will change the implementation of the WeatherForecastController class, to take IOptions of Custom instead of just Custom class as a dependency.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Config.Demo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private readonly ILogger<WeatherForecastController> _logger;
        private readonly string version;
        private readonly Custom custom;

        public WeatherForecastController(
            ILogger<WeatherForecastController> logger,
            string version,
            IOptions<Custom> custom)
        {
            _logger = logger;
            this.version = version;
            this.custom = custom.Value;
        }

        [HttpGet]
        public string Get()
        {
            return custom.ConfigValue;
        }
    }
}

Now, if I run the application, I will see the exact same output as before.

Accessing Environment variables

This is one feature that I absolutely love about .Net Core, is that we can access even environment variables through the same IConfiguration object. Which is really helpful.

To demonstrate this, firstly, I will add a new environment variable named TEST_ENV in the environment variables, and I will set its value to “Test_Environment_Variable”.

environment variable

Secondly, I will go to the Startup class and access this environment variable using Configuration["TEST_ENV"] and print it out to the console.

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Config.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            var version = Configuration.GetValue<string>("Version");
            var db = Configuration["Settings:Database"];

            services.AddSingleton(version);

            services.Configure<Custom>(Configuration.GetSection("Custom"));

            Console.WriteLine(version);
            Console.WriteLine(db);

            Console.WriteLine(Configuration["TEST_ENV"]);
        }

        // 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();
            });
        }
    }
}

Now if I run the application, I can see the environment variable output in the console.

configuration environment variable output

Accessing Custom Configuration File

We can also add custom JSON configuration files to our projects and access them using the IConfiguration object. To demonstrate that, firstly, let us add a new configuration file named CustomConfig.json to the project. And this configuration file will have a single configuration section CustomConfig.

{
  "CustomConfig": "Added custom config"
}

Secondly, we will update the Program class’s CreateHostBuilder method to add the extension method ConfigureAppConfiguration. And to the IConfigurationBuilder delegate, we will call the AddJsonFile extension method to add the custom configuration file.

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

namespace Config.Demo
{
    public static 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>()
                    .ConfigureAppConfiguration(c => 
                        c.AddJsonFile("CustomConfig.json"));
                });
    }
}

Now I will go and update the Startup class’s ConfigureServices method to access the newly added custom configuration value.

Console.WriteLine(Configuration["CustomConfig"]);

Now if I run the application, I will see the value of CustomConfig node is printed in the console output.

Overriding configuration with arguments

We can update the configurations from the arguments passed to the application. To demonstrate this, let us say, we want to override the Version number of the appsettings.development.config from the arguments passed to the application.

We can set up the arguments in the Project Properties window in the Debug tab as shown below.

Once we do that, we can update the CreateHostBuilder method of Program class to replace the configuration value from the arguments passed.

To do that firstly, I will create a new Dictionary instance, where I will create a mapping of the argument keys with the configuration keys.

var replacement = new Dictionary<string, string>
                    {
                        {"-v", "Version" }
                    };

Secondly, I will call the AddCommandLine extension method on the IConfigurationBuilder instance to replace the configurations with command line arguments.

using System.Collections.Generic;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

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

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    var replacement = new Dictionary<string, string>
                    {
                        {"-v", "Version" }
                    };

                    webBuilder.UseStartup<Startup>()
                    .ConfigureAppConfiguration(c => 
                        c.AddJsonFile("CustomConfig.json")
                        .AddCommandLine(args, replacement));
                });
    }
}

Now if we run our application, we will see that the value of Version in the console output from the Startup class where we are accessing it through Configuration.GetValue will print out as 2.0 now.

configuration replacement with arguments

Conclusion

As you can see, configuration management in ASP.NET Core is extremely streamlined which makes using configurations a breeze in ASP.NET Core applications.

I have captured the entire process in the YouTube video here.

The source code for this blog post is available in my GitHub repo here.