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 typeT
. Returnsdefault(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. Returnsdefault(T)
if there is no value.TryGetValue<T>(out T value, string path)
: Gets a value indicating whether a value exists for the current nodeGetNodes(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>()