Auto fill settings objects with .config values

Lately I've been playing around with API's. Most of them need settings like AppId and AppSecret. I found myself doing the same work over and over again: creating a settings class, filling the class with information and using it. So I came up with a way to leverage reflection to fill my setting classes with .config values.

Example class

A CloudMQTT setting class could have the following properties:

public class MqttSettings
{
    [Required]
    public string BrokerHostName { get; set; }

    [Required]
    public uint Port { get; set; }

    [Required]
    public bool UseSecureConnection { get; set; }

    [Required]
    public string UserName { get; set; }

    [Required]
    public string Password { get; set; }

    [Required]
    public string Topic { get; set; }
}

So let's build something to automate the process! I would like it to be created and filled with the following line of C#:

var settings = AppSettingsProvider.Create<MqttClientSettings>();

1. Reading a setting from the configuration

So let's start with the basics, reading a setting from the config. I've created a static class with the basic ability to read a setting by its name. To access the ConfigurationManager you'll need to include a reference to the System.Configuration.

public static class AppSettingsProvider
{
    /// <summary>
    /// Gets the value of the setting from the configuration.
    /// </summary>
    /// <param name="key">The name of the setting.</param>
    /// <param name="required">If <c>true</c> an exception will be thrown if the setting isn't present.</param>
    /// <returns>The value.</returns>
    public static string GetValue(string key, bool required = true)
    {
        if (key == null)
        {
            throw new ArgumentNullException(nameof(key));
        }

        string value = ConfigurationManager.AppSettings[key];

        if (required && String.IsNullOrWhiteSpace(value))
        {
            throw new Exception("Required configuration value for '" + key + "' is missing. Add it to config.");
        }

        return value;
    }
}

Some settings will be required. The system should throw an error if a configuration value is missing. The exception will inform the end-user about what setting is missing in the configuration file.

Note: I'm using a String.IsNullOrWhiteSpace check to see if a settings has been filled. You might want to see if this fits your needs. A value with only spaces or an empty string might be something you want to support.

2. Get a value by convention

To make the system easier to work, a convention for configuration-names is needed. I'm using {namespace}.{class-name}.{field-name}. My config file will look like this:

<add key="KeesTalksTech.Apis.Mqtt.MqttClientSettings.BrokerHostName" 
     value="m88.cloudmqtt.com" />

<add key="KeesTalksTech.Apis.Mqtt.MqttClientSettings.Port"
     value="21337" />

<add key="KeesTalksTech.Apis.Mqtt.MqttClientSettings.UseSecureConnection"
     value="true" />

<add key="KeesTalksTech.Apis.Mqtt.MqttClientSettings.UserName"
     value="6hhmsd33daas" />

<add key="KeesTalksTech.Apis.Mqtt.MqttClientSettings.Password"
     value="-tdERffsd3#4" />

To make things more convenient, I have to add a method that resolves the configuration name and value for a given object and field:

/// <summary>
/// Gets the value for the field of an object from the configuration. 
/// The classname of the object is used to construct the setting name.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="field">The name of the field.</param>
/// <param name="required">If <c>true</c> an exception will be thrown if the setting isn't present.</param>
/// <returns>The value.</returns>
public static string GetValue(object obj, string field, bool required = true)
{
    if (obj == null)
    {
        throw new ArgumentNullException(nameof(obj));
    }

    if (field == null)
    {
        throw new ArgumentNullException(nameof(field));
    }

    string settingName = obj.GetType().FullName + "." + field;

    //fix inner classes
    settingName = settingName.Replace("+", ".");

    return GetValue(settingName, required);
}

This solution supports inner classes. Generic classes aren't supported (as the GetType().FullName returns a weird looking name).

3. Fill settings object

Any object can be filled by configuration values, so let's create a method that takes an object and inspect its properties using reflection. All instance properties that are public and settable will qualify. I'm using the standard System.ComponentModel.DataAnnotations.RequiredAttribute to determine if the field is required. To convert the string value to the right destination type I'm using a Convert.ChangeType.

/// <summary>
/// Fills the public settable instance properties with the corresponding configuration values. 
/// Properties decorated with [Required] will throw an error if they aren't set by the configuration.
/// </summary>
/// <param name="obj">The object. Required.</param>
public static void Fill(object obj)
{
    if (obj == null)
    {
        throw new ArgumentNullException(nameof(obj));
    }

    var properties = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.Instance);

    foreach (var property in properties)
    {
        var required = property.GetCustomAttribute<RequiredAttribute>() != null;
        var value = GetValue(obj, property.Name, required);

        if (!String.IsNullOrEmpty(value))
        {
            object convertedValue = Convert.ChangeType(value, property.PropertyType);
            property.SetValue(obj, convertedValue);
        }
    }
}

Note: I'm rejecting (not binding) values that are null or "". You might need something different in your project.

4. Convenience: create a settings object

I love generics, so I added one more convenience method:

/// <summary>
/// Creates the object and fills it with values from the configuration. 
/// The public settable instance properties with the corresponding 
/// configuration values. Properties decorated with [Required] will
/// throw an error if they aren't set by the configuration.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>An instance with filled properties.</returns>
public static T Create<T>() where T : new()
{
    var config = new T();
    Fill(config);
    return config;
}

Fill 'er up!

So let's see how this works together in a factory pattern. Line 10 loads creates a filled settings object:

using KeesTalksTech.Apis.Mqtt.M2MQTT;
using KeesTalksTech.Utilities;

public static class MqttClientFactory
{
    public static IMqttClient CreateClient()
    {
        var settings = AppSettingsProvider.Create<MqttClientSettings>();
        return CreateClient(settings);
    }

    public static IMqttClient CreateClient(MqttClientSettings settings)
    {
        return new M2MqttAdapter(settings);
    }
}

With a solution like this, loading settings becomes way easier. As your APIs increase, your settings in your config will remain manageable and readable.

GitHub

Want to use this solution? Great. You can find it here on GitHub: AppSettingsProvider.

NuGet

Or just NuGet it into your project:

  • Install-Package KeesTalksTech.Utilities.Settings -Version 0.3.2
  • dotnet add package KeesTalksTech.Utilities.Settings --version 0.3.2
  • <PackageReference Include="KeesTalksTech.Utilities.Settings" Version="0.3.2" />

expand_less