Skip to content

Configuration Objects

Allon Guralnek edited this page Jul 17, 2017 · 3 revisions

To access the configuration from code, you use configuration objects - strongly typed objects that are populated with values from corresponding entries. A Config object is mapped from some root in the configuration tree. All values of configuration entries under this root are mapped into fields and properties on this object. By default, it is mapped from a top-level node with same name as object's class name, but it is possible to map it from another tree root by adding a [ConfigurationRoot] attribute to the config object (see below).

An configuration object must implement the marker interface IConfigObject and can optionally be decorated by a [ConfigurationRoot] attribute. Each public property is mapped to a configuration entry and must have a public getter and setter. You can optionally specifiy a default value which will be used if no value was specified in the configuration. The object mapping and population is done with Json.NET, so the type of the property can be any type that it can handle, including complex types. Also, you can use [JsonProperty] (or any other Json.NET attribute) to control the mapping of the object.

Below is an example of a configuration object that has a mix of primitive and complex properties.

public class ProfileImageConfig : IConfigObject
{
   public bool PreserveAspectRatio { get; set; } = true;
   public string MissingImageText { get; set; } = "No Image";
   public ImageSize MaximumImageSize { get; set; } = new ImageSize { Height = 200, Width = 200 };
   public ImageSize MinimumImageSize { get; set; } = new ImageSize { Height = 32, Width = 32 };
}

public class ImageSize
{
   public int Height { get; set; }
   public int Width { get; set; }
}

Given the above classes, the following XML configuration will populate some of it's fields:

<configuration>
    <ProfileImageConfig PreserveAspectRatio="false">
        <MinimumImageSize Height="48" />
        <MaximumImageSize Height="120" Width="100" />
    </ProfileImageConfig>
</configuration>

And will result in the following final values in the object:

ProfileImageConfig values
Rendered using LINQPad's world-famous .Dump() extension method.

Getting a Configuration Object in Code

Even though you can simply add your configuration object as constructor argument and dependency injection will provide you with an populated instance, you won't get any changes that are made to the configuration after you've recieved the object instance. Therefore, it is recommended that you request a Func<T> factory of the configuration object, and use it for each flow that requires the configuration.

For example, if you have a class that renders profile images, it could look like the following:

public class ProfileImageRenderer
{
   Func<ProfileImageConfig> GetConfig { get; set; }

   public ProfileImageRenderer(Func<ProfileImageConfig> getConfig)
   {
       GetConfig = getConfig;
   }
   
   public async Task<byte[]> Render(long userId)
   {
       var config = GetConfig();
       
       var originalImage = await GetUploadedProfileImage(userId);
       
       if (originalImage.Size.Width > config.MaximumImageSize.Width)
       {
           /// TODO: Reduce image size
       }
   }
}

Using a Func<T> to return the configuration object has always provides the most recent configuration, but is optimized for the case when nothing changes and will return the same object in that case. While the configuration may change during execution of the Render method, the retrieved object won't be changed thus providing internal consistency withing the object (known as object tearing). The next time the Func<T> is called, it will return a new instance of the object with the updated configuration. Note that because we reuse the same object for multiple calls to Func<T> in which the configuration hasn't changed, you should never mutate the returned object because that will affect all other parts of the code which use the configuration.

Dictionary Properties

Sometimes you need to store a collection of complex objects in your configuration, which is where dictionary properties on configuration objects can be of great help. When you define a property that has with a type of IDictionary<TKey, TValue> where TKey is string, all configuration entries under this entry with the property's name will be mapped into this dictionary as key-value pairs.

Example:

public class DatabaseConfig : IConfigObject
{
   public int MaxAllowedConnections { get; set; } = 100;
   public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromMinutes(2);
   public Dictionary<string, DatabaseConnection> Connections { get; set; }    
}

public class DatabaseConnection
{
    public string HostName { get; set; }
    public int Port { get; set; } = 7777;
    public string Username { get; set; }
    public string Password { get; set; }
    public string DatabaseName { get; set; }
}

In the following configuration, ConnectionTimeout is changed from the default 2 minutes to 30 seconds, and two connections are defined:

<configuration>
    <DatabaseConfig ConnectionTimeout="00:00:30">
        <Connections>
        	<Storefront HostName="db1" Username="MyAppUser" Password="MyAppPassword" />
        	<Audit HostName="auditdb" Username="MyAppUser" Password="MyAppPassword" DatabaseName="Audit" />
        </Connections>
    </DatabaseConfig>
</configuration>

ConfigurationRootAttribute

Configuration objects can be created from any arbitrary point in XML file, right now three strategies are supported, the default one is to map configuration Class to node under XML root with same name this the strategy that was used in the example in the previous section.

In addition we have two more that are controlled via ConfigurationRootAttribute that can be placed on the class definition of configuration object.

It accepts value of the following enum:

  public enum RootStrategy
  {
    AppendClassNameToPath,
    ReplaceClassNameWithPath,
  }

ReplaceClassNameWithPath - is used if you want to point to any arbitrary point in XML tree. AppendClassNameToPath - is used when you want to use class name with arbitrary prefix in the tree.

So if XML in example above would be wrapped with additional element like this:

<configuration>
  <Databases>
    <DatabaseConfig ConnectionTimeout="00:00:30">
      <Connections>
        <Storefront HostName="db1" Username="MyAppUser" Password="MyAppPassword" />
        <Audit HostName="auditdb" Username="MyAppUser" Password="MyAppPassword" DatabaseName="Audit" />
      </Connections>
    </DatabaseConfig>
  </Databases>
</configuration>

You can use attribute to map DatabaseConfig to the correct point in XML tree in two ways:

[ConfigurationRoot("Databases.DatabaseConfig", RootStrategy.ReplaceClassNameWithPath)]
public class DatabaseConfig : IConfigObject { }

[ConfigurationRoot("Databases", RootStrategy.AppendClassNameToPath)]
public class DatabaseConfig : IConfigObject { }

Validation Attributes

TODO: Explain how and when the validation happens, link to article the explains the validation system, talk about it being recursive, talk about what happens when validation fails (on startup, during config update).

Encryption

Microdot supports encrypted values on configuration files. The encrypted text should be decorated with $enc(), for example:

<ConnectionString>$enc(==Tohoioso_oIoso_oAono_oEonocoroyopotoeodo_oToeoxoto==)</ConnectionString>

Microdot does not contain default encryption implementation. In order to implement your encryption method, you need to set value of two static fields. For example:

public void SetEncryptor()
{
  ConfigItem.ConfigDecryptor = x => x.Replace("o","").Replace("_"," ");
  ConfigItem.IsValidEncryptedStringFormat = x => x.StartsWith("==") && x.EndsWith("==");
}