In software development, application configuration allows applications to change the way they work without changing the applications themselves.
While the configuration sources are mostly the same across all platforms and frameworks—files, application memory, or databases—the way we define and access configuration is platform and framework-specific.
So, it comes as no surprise that ASP.NET Core offers specific ways for developers to add flexible configuration to web applications.
Using Files for Configuration
appsettings.json
Like ASP.NET on .NET Framework, ASP.NET Core starts with some conventions for a configuration that can be expanded as the application's needs change and grow.
Classic ASP.NET applications used `.config` files written in XML, starting with the `web.config`, as the primary source for configuration. While some of this configuration, in ASP.NET Core, has moved into code (ex. middleware instead of XML registered HttpHandlers and HttpModules), most of the configuration still comes from files, with the main difference being the file syntax.
JSON files, specifically the `appsettings.json`, have replaced the XML based `web.config` file.
Using IConfiguration
Any key/value pairs added to this `appsettings.json` file can be accessed anywhere in the application with the `IConfiguration` service using dependency injection.
Let's have a look at a concrete example!
Take an `appsettings.json` file in our ASP.NET Core application with the following:
{ "greetings": { "morning": "Good morning", "afternoon": "Good afternoon", "night": "Good night" }, "greetingTimes": { "morning": "6:00", "afternoon": "12:00", "night": "18:00" } }
We can access a configuration value in a Controller using the `IConfiguration` service:
public class HomeController : Controller { private readonly IConfiguration config; public HomeController(IConfiguration config) { this.config = config; } public ActionResult Index(string name) { string morningGreeting = config["greetings:morning"]; string personalizedGreeting = $"{morningGreeting}, {name}"; // ... } }
Notice the `greetings:morning` syntax, which allows us to access nested configuration values.
We could also have grabbed the entire `greetings` section of configuration:
IConfigurationSection greetings = config.GetSection("greetings"); string nightGreeting = greetings["night"];
There's another method on the `IConfiguration` service, `.GetValue<T>()`, that allows us to convert our configuration value to another type:
IConfigurationSection greetings = config.GetSection("greetings"); IConfigurationSection times = config.GetSection("greetingTimes"); TimeSpan morning = times.GetValue<TimeSpan>("morning"); TimeSpan afternoon = times.GetValue<TimeSpan>("afternoon"); TimeSpan night = times.GetValue<TimeSpan>("night"); TimeSpan now = DateTime.Now.TimeOfDay; if (now < morning || now >= night) { return greetings["night"]; } else if (now < afternoon) { return greetings["morning"]; } else if (now < night) { return greetings["afternoon"]; }
These simple configuration examples will work for some applications, but ASP.NET Core supports complex applications too!
Configuration Providers
If we have an ASP.NET .NET Framework application we are moving to ASP.NET Core, we might have lots of configuration coming from sources other than `.json` files.
Fortunately, ASP.NET Core offers several other configuration providers out of the box:
- Azure Key Vault
- Azure App Configuration
- Command-line
- Environment variables
- Files (XML, JSON, INI)
- Key-per-file
- Application memory
- User secrets (for local development)
That's a pretty exhaustive list! But, our application might need to read configuration from a source not covered by the providers above. For these scenarios, there's Custom Configuration Provider.
Maybe our application's configuration is stored in a cloud offering that doesn't have a configuration provider, or perhaps, and much more likely, we store configuration values in a database.
For either of these scenarios, we can write a configuration provider that knows how to populate an `IDictionary<string,string>` with our configuration values.
This configuration provider is then used by a custom configuration source (also created by us), added as a source for our applications configuration, during application startup.
Options Pattern
As our applications grow in both internal features and external integrations, we will inevitably need more configuration:
- database connection strings
- web service API keys
- email SMTP credentials
- filesystem paths
However, each part of our application should only have access to the configuration it needs, not every value for all features from all configuration sources.
Thankfully, ASP.NET Core provides a helpful abstraction with the Options Pattern.
Options allow developers to define simple C# classes that hold configuration, and the process for populating those classes from the application's `IConfiguration`. This means that once we adopt the Options pattern, we can avoid passing around `IConfiguration` as a dependency and guarantee that our configuration is validated and strongly typed!
Let's take a look at using Options with our previous greeting example.
First, we define a C# class that will hold our specific configuration values (for convenience, I'm using C# 9.0 record types):
public record GreetingConfiguration( GreetingSettings Morning, GreetingSettings Afternoon, GreetingSettings Night ) { } public record GreetingSettings(string Greeting, TimeSpan Time) {}
Next, in our application startup, we define `GreetingConfiguration` as being an Option and how to create it:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddOptions<GreetingConfiguration>() .Configure(greetingConfig => { IConfigurationSection greetings = config.GetSection("greetings"); IConfigurationSection times = config.GetSection("greetingTimes"); TimeSpan morning = times.GetValue<TimeSpan>("morning"); TimeSpan afternoon = times.GetValue<TimeSpan>("afternoon"); TimeSpan night = times.GetValue<TimeSpan>("night"); return new GreetingConfiguration( new GreetingSettings(greetings["morning"], morning), new GreetingSettings(greetings["afternoon"], afternoon), new GreetingSettings(greetings["night"], night), ); }); // ... } }
Now we can inject our specific `GreetingConfiguration` wherever we need it in our application using the `IOptions<T>` interface:
public class GreetingController : Controller { private readonly GreetingConfiguration greetings; public GreetingController(IOptions<GreetingConfiguration> greetings) { this.greetings = greetings.Value; } public ActionResult Index() { // C# 9.0 record destructuring! ((string morningGreeting, TimeSpan morningTime), _, _) = greetings; // ... } }
Other interesting use-cases are enabled with IOptionsSnapshot, which re-creates the Options for every request to ensure we have the latest values. This would be helpful if our configuration came from a database or web service.
We can also use IOptionsMonitor, which can re-create our Options any time the data source changes.
Kentico Xperience Configuration
Some of the configuration examples we looked at are simple (using `IConfiguration`) while others were much more complex (a custom `IConfigurationProvider` or `IOptionsMonitor`).
So, when using Kentico Xperience 13.0, a data-driven application that sources configuration from both the database and file system, how do we initialize and access our configuration values?
Thankfully, it's pretty easy!
First, we make sure Kentico Xperience is integrated into our application by registering it at application startup:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddKentico(); // ... } }
Since Xperience follows ASP.NET Core conventions and looks for configuration in the `appsettings.json`, all we need to do is reference the configuration keys and supply our custom values. All the configuration that Xperience needs to start running is automatically wired up!
But what about database configuration?
Xperience makes that easy as well, with the `ISettingsService` interface, which is automatically available in our application, through Dependency Injection, with our single `services.AddKentico();` call.
With that done, we can use `ISettingsService` directly (or as the data source for Options using the Options Pattern shown above).
public class HomeController : Controller { private readonly ISettingsService settings public HomeController(ISettingsService settings) { this.settings = settings; } public ActionResult Index() { string greeting = settings["SiteGreeting"]; // ... } }
As we can see, Kentico Xperience supplies the building blocks we need to start building our applications quickly and manage complexity as their requirements grow.