ASP.NET Core Configuration Recommended Practices

Published on Monday, September 24, 2018

ASP.NET Core and ASP.NET Core Configuration build upon some fundamental and important principles to maintain its vision. Having worked with some of those principles for years and more recently working with ASP.NET Core Configuration and guiding other team members, I've come up with a recommended practices view of the principles through the eyes of ASP.NET implementation examples. It has helped me to have a deconstructed view of ASP.NET Core (and many other things) with those principles in mind, I hope it helps you too.

Some of the principles:

  • Separation of Concerns
  • Loose coupling
  • Interface Segregation
  • Single Responsibility
  • Dependency Inversion
  • Don't Repeat Yourself
  • Explicit Dependencies

1. Compartmentalization: Sections Are Your Friends

1.1 Do separate independent configuration groups by sections

One big chunk of configuration (essentially a big chunk of key-value pairs) as the configuration for all the individual components of your application has maintainability issues. It Works™, but that means that every component that is individually configurable within your application is more tightly-coupled to all the others because there is no explicit abstraction between their settings data. Something more maintainable is to separate the component configurations from one another by a section in the configuration. An appsettings.json example:

{
  "Logging" {
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  }, 
  "ConnectionStrings": {
    "BloggingDatabase": "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"
  },}

...where the sections are "Logging" and "ConnectionStrings", and subsections "Debug" and "LogLevel".

ASP.NET does a lot to allow you to be explicit and to support Dependency Inversion, and section separation of configuration will make that and the following recommendations easier.

1.2 Do not couple all your classes to IConfiguration

In the same vein as 1.1, using a single IConfiguration instance all over the place is the same as all your components being coupled to each others' configuration. IConfiguration is loosely coupled, but maintain loose coupling at the class/component level too.

See also Use of C# built-in types in constructor parameters

2. Configuration Isn't Just appsettings

Out of the box, ASP.NET Supports appsettings.json, environment-specific appsettings.json, command-line arguments, environment variables, ini files, XML config files, in-memory data, and any custom source/provider you need.

Viewing the application configuration as being manifested by appsettings JSON files is very limiting. Worse case it becomes an expectation and the ability to change configuration depends on loading appsettings:

	var builder = new ConfigurationBuilder()
		.SetBasePath(Directory.GetCurrentDirectory())
		.AddJsonFile("appsettings.json");

	Configuration = builder.Build();

As we'll see, ASP.NET configuration is very powerful and flexible, think of ASP.NET Configuration independently of appsettings and that appsettings is just one part of it.

2.1 Do think of ASP.NET configuration as a means to a Configuration Management end

ASP.NET Configuration is powerful; powerful enough to be the application's pane of glass into Configuration Management. ASP.NET Configuration provides a unified view of the independent variables that affect the success/outcome of application functionality. (think of the application as a function and configuration is the container of "inputs" that affect dependent variables to be observed or measured).

2.2 Do not couple code to source of configuration data

ASP.NET Core configuration is powerful and flexible, there is no need for controller, model, or domain classes to know anything about details like appsettings.json, environment variables, etc. Let ASP.NET Configuration handle all of that heavy lifting so that controller, model, and domain depend only upon abstractions, including these configuration abstractions.

2.3 Consider using the Options Pattern for grouping settings

One of the goals of a Dependency Injection container is to act as a registry--an opt-in mechanism to resolve instances from types and to relieve construction logic from dependents. Settings, options, and configuration are genuinely unique; they're not object-oriented but data containers or shapes. As data containers there's an expectation of "default" values and "missing" values and often "missing" means falling back to "defaults". But, in an opt-in registry situation, what does that mean?

That's what IOptions does for you, you don't have to know that "defaults" need to be registered or to force the responsibility of knowing what "defaults' mean on a boostrapper. Using IOptions<T> in your constructors means you don't have to worry about that. Using IOptions<T> is an indirect opt-in of T, the container will go ahead and instantiate it if one hasn't already been registered with specific values (it doesn't do that with any other types). This means the constructor of the options class takes on the responsibility of what "defaults" mean--which is more maintainable.

The Pattern is using IOptions<T> in constructors to inject settings via an instance of T, and that any configuration is loaded via services.Configure<TOptions>(Configuration.GetSection("TOptions")). But, a key part of that pattern are the defaults. So, the shapes you use as TOptions types should set default values. For example:

public class MyOptions
{
	public MyOptions()
	{
		// Set default value.
		Option1 = "value1_from_ctor";
	}

	public string Option1 { get; set; }
	public int Option2 { get; set; } = 5;
}

For more detail, see Options Pattern

2.4 Do understand configuration source default precedence

When using the ASP.NET Core WebHostBuilder, the order of precedence of configuration sources is:

  1. appsettings.json
  2. appsettings.env.json
  3. [User Secrets]
  4. Environment Variables
  5. Command Line

To be specific, the order of precedence mirrors the order the providers are added to the configuration builder. The above is really just how WebHostBuilder implements them.

You can override all that if you want, but don't cause yourself that pain: accept the defaults of WebHostBuilder.

2.5 Do know how to override settings

Configuration supports a variety of providers. Some inherently support structured, complex data; some don't. JSON is the quintessential example of structured configuration for ASP.NET:

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

And command-line arguments are the quintessential example of unstructured:

MyApplication.exe --Verbosity true

But, configuration providers translate whatever structure they support into the flat structure that ASP.NET Core configuration supports. To do that, providers simply use a colon : as hierarchy level separators. So, the Default key name from the above JSON has a fully-qualified configuration key name of Logging:LogLevel:Default. Which means you can override what is in appsettings with key/value pair from anyware, e.g. via the command line: MyApplication --Logging:LogLevel:Default Error

And to override appsettings values with environment variable values, use a double underscore __ as a hierarchy delimiter instead of a colon hierarchy level delimiter:

SET Logging__LogLevel__Default=Error

3. ASP.NET Core bootstrapping is all about the Builder Pattern and the Dependency Injection technique

3.1 Do understand the bounds of builders and utilize them correctly

To maintain the level of loose coupling that has lead to ASP.NET Core advancements, builders are essential. Builders provide the ability to describe (declaratively) what is needed to compose things from dependencies, and avoid coding how (imperatively) to compose things with dependencies.

The flow of things is often useful when getting into advanced situations:

Building the web host starts in Program.Main, building configuration spawns from building the web host, configuration is built before spawning Startup from building the web host, Startup is created before configuring services, configuring services completes before application builder, application builder completes before controllers are instantiated, some services may be instantiated after the application builder completes.

That flow results in some expectations:

  • Use the application builder to amend what a built application means.
  • Don't expect to use services with IConfigurationBuilder
  • Use the configuration builder only to describe what building configuration means.
  • Don't expect to do any configuration building when services start configuring.
  • Use the services collection to register instances or types for dependencies instead of within the dependencies.
  • Add dependent service information after adding the information for services they depend upon.
  • If service instances are required before service configuration is complete, instantiate them in ConfigureServices and use them before adding them to the services collection.

Dependency inversion means that the order of operations start is opposite to the order of dependency:

  • host→configuration→services→application
  • application⇢services⇢configuration⇢host

3.2 Avoid constructing any instance that aren't used through Services Collection

Needing to construct an instance before you've added all dependent services to the services collection is an indication of a potential dependency problem (not inverted, or circular). So, avoid doing that without a specific reason (besides to Get It To Work™).

Metrics

Smells

Some less-measurable things that you can watch for that will help recognize refactoring candidates...

Use of C# built-in types in constructor parameters

The C# built-in types are the building-blocks for all other complex types. Parametric Polymorphism and Ad hoc Polymorphism are the language features that allow the compiler and runtimes to differentiate which thing to use while supporting looser coupling. (e.g. Generics and Function Overloading). Dependency Inversion depends heavily on Dependency Injection and Dependency Injection depends heavily on that type inference to achieve that level of loose coupling.

So, the re-usability of a type is at odds with how reliably another type (dependent on that first type for instantiation input) can be used with Dependency Injection.

For example, I may have a controller that needs a URL, and that URL could be a string parameter:

public class NaiveController
{
	public NaiveController(string oAuthUrl)
	{
		//...
	}
}

and I could use the services collection to inject that URL when the controller is instantiated:

services.AddSingleton("http://api.example.com/fugassi");

...and the ASP.NET container can infer that when instantiating a NaiveController instance. But, that means you can only have that one string object as a service. If you had another controller that needed a URL, the container wouldn't be able to figure out which string instance to use (if it supported more than one, unnamed).

So, when you see the use of built-in types, or very ubiquitous types as inputs to controllers and services, consider refactoring to the Options Pattern or refactoring to a Service. i.e. it's not just that URL is stored in a built-in type, the Uri type could have been used instead, and the problem would be the same. Refactored to Options Pattern:

public class NaiveController
{
	public NaiveController(IOptions<NaiveControllerOptions> options)
	{
		var url = options.Value.OAuthUrl;
		//...
	}
}

Refactoring to a service means that functionality is encapsulated within a service. In the case of the URL example, rather than creating an options type to store that string value, the controller should actually be dependent on another service that would use that URL value but provide behavior. Refactoring to a service example:

public class NaiveController
{
	public NaiveController(IOAuthProvider oAuthProvider)
	{
		//...
	}
}
//...
	services.AddSingleton<IOAuthProvider>(new TwitterOAuthProvider(Configuration.OAuthUrl/*...*/));

This is an easy example to understand, but lack of testability of a controller the uses a URL directly might have reared its head before noticing this smell. ¯\_(ツ)_/¯

See also Introduce Parameter Object refactoring.

Use of IConfiguration outside of Startup or bootstrap

See 1.2. Use of IConfiguration outside of Startup or bootstrap means you're coupling many classes to IConfiguraiton.

References

comments powered by Disqus