Configuration System

Quino includes an IConfigurationDataSettings object with a property ConfigurationData of type IKeyValueNode<string>. The IKeyValueNode represents a named value with a list of n children. With this, Quino provides an arbitrary hierarchy of named values to an application.

Loading Configuration Data

Quino also has a mechanism for filling this hierarchy with data. The IConfigurationDataInitializer in the startup uses the IConfigurationDataLoader to load data from external files into the hierarchy.

The hierarchy supports merging, so the configuration for an application generally consists of multiple files, each of whose contents is merged into the existing hierarchy, in the order that they are loaded.

The default implementation of the IConfigurationLoader pulls content from the following files, for a given base filename of data-configuration.xml,

  • data-configuration.common.xml (searches up the folder hierarchy)
  • data-configuration.xml (current folder)
  • data-configuration.local.xml (current folder)
  • data-configuration.xml (user's local app data folder)
  • data-configuration.local.xml (user's local app-data folder)

The user's local application-data folder is calculated as follows:

%LOCALAPPDATA%\{CompanyName}\{ProductName}

Here, {CompanyName} and {ProductName} are taken from the IApplicationDescription.CompanyName and IApplicationDescription.Title properties, respectively.

The default implementation of the ITextKeyValueNodeReader uses an XML format. An application can replace this implementation with TOML or JSON, but these loaders are not provided with Quino. See the XML implementation for how to go about supporting other configuration formats.

KeyValueNodes

For APIs that support a path,

  • The call applies to the current node if the path is empty.
  • A path is separated by forward-slashes, e.g. "service/settings/intervals/heartbeat".

The IKeyValueNode<string> has the following API:

  • GetValue<T>(): Gets the value of the current node with a specific type T. Returns default(T) if there is no value.
  • GetValue<T>(string path): Gets the value of the node at the given path relative to the current node. Returns default(T) if there is no value.
  • TryGetValue<T>(out T value, string path): Gets a value indicating whether a value exists for the current node
  • GetNodes(string path): Gets the list of nodes that match the given path.

The Settings Pattern

Adding nodes to the configuration files is obviously not enough -- an application needs to read and use those values at runtime.

It is not recommended to inject the IConfigurationDataSettings and read values directly out of the nodes. Instead, Quino recommends using a pattern that couples a service with settings for the implementation of that service. A service like IFileLogger with a concrete implementation of FileLogger injects the IFileLogSettings.

public interface IFileLogger
{  
}

public interface IFileLoggerSettings
{
  string FilenameTemplate { get; set; }
}

public class FileLoggerSettings : IFileLoggerSettings
{
  public string FilenameTemplate { get; set; } = "{Title}.log";
}

public class FileLogger : IFileLogger
{
  public FileLogger([NotNull] IFileLoggerSettings settings)
  {
  }
}

Both of these services must be registered with the IOC, of course.

application
  .UseRegisterSingle<IFileLogger, FileLogger>()
  .UseRegisterSingle<IFileLoggerSettings, FileLoggerSettings>()

With this pattern, an application is using typed values everywhere, except at the boundary where data is loaded from the IConfigurationDataSettings into the settings objects themselves. The next section explains how to transfer this data.

Configuring Settings Objects

The recoomended way of loading settings is to make your settings inherit from IConfigurableSettings, implementing the Load(IKeyValueNode<string>) method. In this method, you will load your properties from the provided node (or from sub-nodes, if your data is more complex).

The FileLoggerSettings are extended below.

public interface IFileLoggerSettings : IConfigurableSettings
{
  string FilenameTemplate { get; set; }
}

public class FileLoggerSettings : IFileLoggerSettings
{
  public string FilenameTemplate { get; set; } = "{Title}.log";

  public void Load(IKeyValueNode<string> node)
  {
    FilenameTemplate = node.GetValue("FilenameTemplate", FilenameTemplate);
  }
}

The standard practice is to call GetValue(), passing in the current value as the default value. If configuration data does not exist, then the value is unchanged.

Configuring at Startup

Finally, most applications will want to load configuration for settings at application startup.

An application defines an initializer interface that can be registered with the IOC, say IFileLogSettingsInitializer. For settings that are IConfigurableSettings, the implementation can use ConfigurableSettingsInitializerBase.

public interface IFileLogSettingsInitializer : IApplicationActionService
{
}

public class FileLogSettingsInitializer : ConfigurableSettingsInitializerBase<IFileLogSettings>
{
  public FileLogSettingsInitializer([NotNull] IFileLogSettings settings, [NotNull] IConfigurableDataSettings configurableDataSettings, [NotNull] ILogger logger)
    : base("logging/file", settings)
  { }
}

Finally, the application must register the new interface and schedule the initializer,

application
  .UseRegisterSingle<IFileLogSettingsInitializer, FileLogSettingsInitializer>()
  .AddOrReplaceStartupAction<IFileLogSettingsInitializer>()
Back to top Generated by DocFX