We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Exploring ASP.NET Core Fundamentals - Creating a Strongly typed Configuration class

ASP.NET Core  • Posted 7 months ago

Configuration in ASP.NET Core projects is accessed by means of an injectable service IConfiguration, which is an amalgamation of configurations from various sources - such as json, environmental variables and so on. It is a dictionary of sections and values, with the key being a string name of the section or the setting. Using the IConfiguration as it is for accessing settings in an application can be sometimes tedious, particularly when wanting to access a value which might be of a type other than a string.

In such cases, we might want to add a wrapper over this IConfiguration service which can help us in transforming the configuration values according to our expectations. This can also mean that we wouldn't need to go for additional typecasting for the values from the IConfiguration dictionary. In this article, let's see how we can add a layer of wrapping over the IConfiguration service and use that wrapper for values instead of the IConfiguration using a strongly typed settings class.

What is IConfiguration and How it Works

Generally speaking, the app configuration is maintained inside a JSON file called appsettings.json and configurations for different environments can be varied by maintaining separate appsettings files for each - say Development, Staging or Production. The configuration inside appsettings is maintained as a key value pair objects similar to a JSON document structure, in which there can exist collection of a specific set of settings, called as sections.

When the app bootstraps, the configuration from the appsettings json file and configurations from other sources like the environment variables, in-memory objects or any other custom providers are clubbed together and are presented for use in the application in the form of an interface IConfiguration. The IConfiguration object is a dictionary of key-value pairs with support for utility methods, like GetSection() or GetConnectionString() which pickup a specific section or a specific key value from the configuration source.

#Building Configuration from various sources#

var builder = new ConfigurationBuilder()
    	.SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", 
				optional: true, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", 
				optional: true)
        .AddEnvironmentVariables();

IConfiguration configuration = builder.Build();

Any required value of a key can be directly accessed from the IConfiguration in the form of a string index; example IConfiguration["myKey"] returns the value of a specific key "myKey". The key value parameter is of type string and the return value is of type object, which needs to be typecasted into required types. The IConfiguration service is an injectable, and hence can be injected into any class or controller by means of Dependency Injection.

Advantages and Possible Limitations:

Introducing a strong-typed Configuration wrapper over the native IConfiguration can provide us benefits such as:

  • Intellisense
  • Handling of types
  • Runtime errors and type checking
  • Better maintainability and structuring

However, It can also bring in a dependency for the application over the new Configuration layer, which might result in modifying the Configuration class each time a new key is added to the appsettings. Although this is can be a bit of overhead, it is particularly useful in keeping track of what all keys the application is actually using among all those available in the appsettings file.

Wrapping the Configuration:

In order to create the strongly-typed Configuration layer over the IConfiguration, we follow the below steps:

  1. Define a template class which holds the blueprint of the appsettings
  2. Inject the IConfiguration provider into the template constructor
  3. Bind the configuration with the template class sections
  4. Define the template as a service and inject wherever required

Defining the Template class:

let's assume our appsettings json looks like below:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "ConnectionStrings": {
    "DefaultConnection": "..."
  },
  "SmtpConfig": {
    "FromAddress": "...",
    "ServerAddress": "...",
    "Port": "...",
    "IsUseSsl": "...",
    "Username": "...",
    "Password": "..."
  },
  "CustomMessages": {
	"Error": {
		"Status":500,
		"Message":"Oops! An error has Occured!"
	},
	...
  },
  "IsVerificationEnabled": false,
  "ResetPasswordExpiryHours": 1,
  "AllowedHosts": "*",
  "dateTimeFormat": "d/M/yy hh:mm tt"
}

Now we can define a template class that reflects the property names and which can hold values for us from the configuration.

public class MyConfiguration {
	public string LogLevel { get; set; }
	public string ConnectionString { get; set; }
	public bool IsVerificationEnabled { get; set; }
	public int ResetPasswordExpiryHours { get; set; }
	public string AllowedHosts { get; set; }
	public string DateTimeFormat { get; set; }
	public SmtpConfig SmtpConfig { get; set; }
	public ICustomMessage Error { get; set; }
	public ICustomMessage Success { get; set; }
}

public interface ICustomMessage {
	public int Status { get; set; }
	public string Message { get; set; }
}

public class ErrorMessage : ICustomMessage {}

public class SuccessMessage : ICustomMessage {}

public class SmtpConfig {
	public string FromAddress { get; set; }
	public string ServerAddress { get; set; }
	public int Port { get; set; }
	public bool IsUseSsl { get; set; }
	public string Username { get; set; }
	public string Password { get; set; }
}

We can observe that we have defined a separate template type for the section SmtpConfig since it is a collection of simple configurations and strong typing this section can further enhance usability.

Next, we inject the IConfiguration service into the MyConfiguration template for binding the values to their respective properties.

public class MyConfiguration {

	private readonly IConfiguration _config;
	
	public MyConfiguration(IConfiguration configuration) 
	{
		_config = configuration;
	}

	public string LogLevel 
	{
		get 
		{
			return _config["Logging:LogLevel:Default"].ToString();
		}
	}

	public string ConnectionString {
		get {
			return _config.GetConnectionString("DefaultConnection");
		}
	}

	public bool IsVerificationEnabled {
		get {
			return bool.Parse(_configuration["IsVerificationEnabled"]);
		}
	}

	public int ResetPasswordExpiryHours {
		get {
			return int.Parse(_configuration["ResetPasswordExpiryHours"]);
		}
	}

	public string AllowedHosts {
		get {
			return _configuration["AllowedHosts"].ToString();
		}
	}

	public string DateTimeFormat => {
		get {
			return _configuration["DateTimeFormat"].ToString();
		}
	}

	public SmtpConfig SmtpConfig { get; set; }
	public ICustomMessage Error { get; set; }
	public ICustomMessage Success { get; set; }
}

Binding Sections onto the Configuration class:

In order to bind an entire section onto the created Configuration type, we make use of the IConfiguration.Bind() method. This simplifies our job of transforming the section into an object without having to process key by key. In our example, The SmtpConfig section of configurations which will be binded onto an object of type SmtpConfig.

public class MyConfiguration 
{
	private readonly IConfiguration _config;
	private readonly SmtpConfig _smtpConfig;
	private readonly ICustomMessage _error;
	private readonly ICustomMessage _success;
	
	public MyConfiguration(IConfiguration configuration) 
	{
		_config = configuration;
		_smtpConfig = new SmtpConfig();
		_error = new ErrorMessage();
		_success = new SuccessMessage();
		_config.Bind("SmtpConfig", _smtpConfig); 
		_config.Bind("CustomMessages:Error", _error); 
		_config.Bind("CustomMessages:Success", _success); 
	}

	public SmtpConfig SmtpConfig => _smtpConfig;
	public ICustomMessage Success => _success;
	public ICustomMessage Error => _error;
	...
}

In the above example, we can observe that sections under a specific section can also be strongly typed, if they follow a common template using an interface and a parent-child hierarchy. The sub sections in a configuration are accessed by using the colon operator which defines the levels; for example CustomMessages:Error picks up the Error section under the CustomMessages section. If the message needs to be accessed individually, we use CustomMessages:Error:Message.

Finally, we can define the MyConfiguration as a service under the Startup class which provides the instance whenever required under the Dependency Injection fashion.

public class Startup 
{
	public void ConfigureServices(IServiceCollection services) 
	{
		services.AddSingleton(MyConfiguration);
		...
	}	
}

And in any random consumer class Consumer:

public class Consumer 
{
	MyConfiguration _configuration;

	public Consumer(MyConfiguration configuration) {
		_configuration = configuration;
	}

	public void SomeLogic() {
		// the type of variable smtp will be SmtpConfig and 
    // the value will be the binded value of SmtpConfig from Configuration.
		var smtp = MyConfiguration.SmtpConfig;
		...
		
		// the boolean type of MyConfiguration.IsVerificationEnabled 
    // can be accessed directly which works on the Configuration
		if(_configuration.IsVerificationEnabled) {
			....
		}
	}	 
}

This way, we can wrap IConfiguration into a strongly-typed Configuration Manager class which can help us transform the settings read from the appsettings or IConfiguration into productive means.