Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Configuration sources fetch data from storage (local files or remote APIs) and convert it to settings nodes, abstracting away actual data formats such as JSON or YAML.
They are not meant to be consumed directly and should be used in conjunction with a configuration provider (see assign sources to types and obtain settings from provider scenarios).
Sources are also responsible for data change detection. They expose a reactive interface with subscription support:
IObservable<(ISettingsNode settings, Exception error)> Observe();On each update, triggered either periodically or by an internal event, the source emits a pair: (settings, null) on success or (null, error) on failure. It's not required to deduplicate settings or errors at this level, although it's never wrong to do so.
Sources must never block indefinitely while waiting for data and should rather publish null settings after a short initial timeout.
Sources must be thread-safe and should be designed to support multiple concurrent observers. It is also expected that every new observer would immediately receive a notification with current state upon subscription.
Here are some of the often used source implementations:
It's also possible to .
Scoping is the operation of navigating a tree by accessing child nodes of via names in a case-insensitive manner. A sequence of names resembling a path in the object structure, such as ["property1", "property2"] is called a scope.
Scoping does not work on and nodes (always results in null).
Scoping is used to map object fields/properties to nodes of settings tree during . It also allows to — create a source that returns a subtree of settings returned by the original source.
{A: 1} scoped to a is just a value of 1.
{A: 1} scoped to b is null.
{A: {B: [1, 2]}} scoped to A is {B: [1, 2]}
{A: {B: [1, 2]}} scoped to [A, B] is [1, 2]
{A: {B: [1, 2]}} scoped to [A, B, C] is null.
Location: main sources module.
CommandLineSource converts CLI arguments into settings nodes.
It supports 7 syntax options for key-value parameters:
--key=value
--key value
-key=value
-key value
/key=value
/key value
key=value
Keys with dots (such as a.b.c) are treated as hierarchical and get split into segments:
Multiple occurrences of the same key are merged into arrays.
This source is static and never issues data updates.
Standalone keys may optionally be supplied with a default value.
Standalone values may optionally be grouped under default key.
public static void Main(string[] args)
{
var source = new CommandLineSource(args);
}--throttling.capacity=50 --> { "throttling": { "capacity": "50" } }var source = new CommandLineSource(args, "main", "true");
--enable --> { "enable": "true" }
100 200 --> { "main": ["100", "200"] }Location: main sources module.
EnvironmentVariablesSource converts process environment variables into settings nodes.
var source = new EnvironmentVariablesSource();Keys with dots, colons and double underscores are treated as hierarchical and get split into segments:
throttling.capacity=50 --> { "throttling": { "capacity": "50" } }
throttling:capacity=50 --> { "throttling": { "capacity": "50" } }
throttling__capacity=50 --> { "throttling": { "capacity": "50" } }This source is static and never issues data updates.
Requires: logging module.
See error handling for cases where this logging is useful.
See logging documentation for the log interface required here.
var providerSettings = new ConfigurationProviderSettings();
providerSettings = providerSettings.WithErrorLogging(log);
var provider = new ConfigurationProvider(providerSettings);Requires: main module.
Settings can be obtained on-demand with configuration provider's Get method.
With prior assignment of sources:
var settings = provider.Get<MySettings>();With sources passed on per-call basis:
Always returns the most recent version of settings (updates may happen in background):
Call Get on every settings access for "hot" configuration;
Call Get once and cache the result for "cold" configuration;
First call for a type may block or due to source latency, data unavailability or incorrect data format. This behavior persists until a data update remedies the error;
Once warmed up, subsequent calls never block or throw errors and are extremely cheap due to . Future errors are not propagated to the calling code, but . Get calls return the last seen correct settings object;
var settings = provider.Get<MySettings>(new JsonFileSource("settings.json"));Requires: main module.
Settings models can be printed in JSON or YAML formats to generate configuration file examples.
var printSettings = new PrintSettings
{
Format = PrintFormat.Json
};
var defaultSettings = new MySettings();
var printed = ConfigurationPrinter.Print(defaultSettings);
Console.Out.WriteLine(printed);Requires: main module.
One can subscribe to settings updates for a type with configuration provider's Observe method.
With prior assignment of sources:
With sources passed on per-call basis:
Temporary subscriptions should be disposed of as they might hold on to resources in sources:
OnError and OnCompleted notifications are never produced. Only successful settings updates are propagated to the observers. This means that there will be no notifications if initial attempt to provide settings and no further data updates are published by the .
The only way to notice errors when using Observe is to .
It's recommended to obtain initial settings instance with Get method before subscribing to updates with Observe. This practice ensures correct error propagation, warms up the and eliminates situations where the settings are not ready yet on access attempt.
provider.Observe<MySettings>().Subscribe(newSettings => {});provider.Observe<MySettings>(new JsonFileSource("settings1.json"))
.Subscribe(newSettings => {});var subscription = provider.Observe<MySettings>().Subscribe(OnNewSettings);
using (subscription)
{
// ...
}The subscription is not guaranteed to immediately produce a notification. The first notification may be delayed due to data not having been fetched from the source yet. However, once a valid settings instance has been observed, all new observers receive a notification with current actual settings upon subscription. This notification is published on a background thread, so don't count on it being delivered after Subscribe call completes.
private volatile MySettings settings;
public Task InitializeAsync(IConfigurationProvider provider)
{
OnSettingsUpdated(settings = provider.Get<MySettings>());
provider.Observe<MySettings>()
.Subscribe(newSettings => OnSettingsUpdated(settings = newSettings));
return Task.CompletedTask;
}
private void OnSettingsUpdated(MySettings settings)
{
// ...
}Requires: abstractions module.
By default all settings are optional; that is, absence of relevant data in the source results in default values during binding and does not produce errors.
Some parts of configuration may be crucial to the application to the point that it's pointless to start without them being initialized. These fields and properties should be annotated with [Required] attribute:
class MySettings
{
[Required]
public string DbConnectionString { get; }
[Required]
public MySecrets Secrets { get; }
}
class MySecrets
{
public string ApiKey { get; }
public string Login { get; }
public string Password { get; }
}Default behavior can be inverted in the scope of a type with [RequiredByDefault] attribute. Individual fields and properties can then be made optional with [Optional] attribute:
provider.Get<MySettings>(); // will throw if DbConnectionString or Secrets are not initialized [RequiredByDefault]
class MySettings
{
[Optional]
public TimeSpan Timeout { get; } = TimeSpan.FromSeconds(30);
}Requires: sources module.
This operation allows to modify source contents by applying a custom delegate. Most often this involves rewriting values stored in value nodes:
class MyTransformer : ValueNodeTransformer
{
public MyTransformer()
: base(node => new ValueNode(node.Name, node.Value?.Replace(..., ...)))
{
}
}var transformedSource = source.Transform(new MyTransformer());Vostok.Configuration is a set of libraries offering configuration tools to .NET applications. It handles fetching configuration data from files or APIs, parsing and converting it to user-defined model classes.
Configuration data sources should be composable regardless of storage formats.
It should be easy to combine settings from JSON files, environment variables and custom APIs. This is achieved with concept and universal abstraction.
"Hot" configuration updates should be easy to leverage.
and methods to obtain settings;
Support for and settings;
Support for custom settings ;
See / settings scenarios, and sections for details.
Hot configuration implies handling changes in settings without application restart. This is enabled by explicitly reactive source design and settings provider methods.
Rich object models should be supported for user convenience.
Binding process supports a wide range of primitives, collections, classes, interfaces and employs a number of conventions for arbitrary types (anything with a TryParse method works).
It should be possible to extend the library with arbitrary data sources and object models.
Two major extensions points are custom configuration sources and binders.
Wide selection of built-in sources;
Composable binders;
Sources can be combined, navigated and transformed;
Integration with Microsoft configuration system;
Value nodes are key-value pairs with optional keys. They cannot have child nodes and thus are always the leaves of settings node trees. Standalone values are rare: most of the time value nodes can be found inside objects or arrays.
Value nodes are typically mapped to primitive types during binding.
Property
Two value nodes are considered equal if their values match exactly and their names match up to differences in case.
Description
Name
Required if nested in an object node, optional otherwise.
Value
Useful payload. The value of a object field or array element. Can be null.
Children
Always returns an empty sequence.
this[name]
Always returns null.
Value("name", "value") --> "value"
Value("name", null) --> <null>are containers used to represent sequences, such as JSON arrays. Each array node contains an ordered list of child nodes. There is no limit to nesting: arrays can contain other arrays and objects. Elements of an array are not required to have names.
Array nodes are typically mapped to ordered collections during .
Name
Required if nested in an , optional otherwise.
Value
Always returns null. Only can have values.
Children
Returns an ordered sequence of child nodes.
ChildrenCount
Returns the number of elements in the Children sequence.
this[name]
Always returns null. Arrays cannot be navigated with .
Two array nodes are considered equal if their Children sequences are equal elementwise and their names match up to differences in case.
roperty
Description
Array(Value("1"), Value("2)) --> ["1", "2"]
Array(Object(), Object()) --> [{}, {}]
Array(Array(Value("1")), Array(Value("2"))) --> [["1"], ["2"]]Here's the simplest way to experience Vostok.Configuration for the first time:
Install main, abstractions and sources.json modules:
Install-Package Vostok.Configuration
Install-Package Vostok.Configuration.Abstractions
Install-Package Vostok.Configuration.Sources.JsonDefine a settings model class:
class MySettings
{
public string Key1 { get; }
public string Key2 { get; }
}Create a JSON file with settings:
Obtain a model instance with a from a file and print it:
Great job! Here are some possible next steps:
Learn the most basic concepts: , , , ;
Explore available and ;
Go over the .
Settings nodes are the intermediate representation of configuration data and one of the core concepts of the library. Their main purpose is to abstract away various configuration formats (JSON, XML, etc.) so that can be implemented once without regard to the nature of being used.
A settings node is a tree with string keys and string values in its leaf nodes.
Fetches the newest version of settings of given type:
Allows to subscribe for updates of settings of given type:
Both Get and Observe methods have 2 variations:
The one without any parameters requires a prior assignment of a source to the requested type;
{
"key1": "value1",
"key2": "value2"
}var provider = new ConfigurationProvider();
provider.SetupSourceFor<MySettings>(new JsonFileSource("settings.json"));
var settings = provider.Get<MySettings>();
Console.Out.WriteLine(ConfigurationPrinter.Print(settings));var settings = provider.Get<MySettings>();provider.Observe<MySettings>.Subscribe(newSettings => {});Configuration sources produce settings nodes as their primary artifacts.
Binding is the process of converting a settings node to an arbitrary .NET object.
Settings nodes are implemented in the abstractions module.
Settings nodes are immutable objects.
Despite being a somewhat internal API used directly only in a handful of advanced scenarios, settings nodes are crucial for a solid understanding of how configuration data translates to objects in C# code via different node types and scoping.
Implementation of a custom configuration source;
Implementation of a custom model binder;
Transformation of configuration sources
There are three types of nodes:
Value nodes, used to hold data;
Array nodes, used to represent sequences;
Object nodes, used to represent objects with named properties.
Users are not expected to implement custom node types.
Null node instances represent absence of settings (e.g. a configuration file that does not exist).
All node types implement the ISettingsNode interface and have following properties:
Property
Type
Description
Name
string
Node name. Case-insensitive.
Value
string
Node value. Only present in .
Children
IEnumerable<ISettingsNode>
Sequence of child nodes in containers: and .
this[name]
All nodes also implement a merge operation.
All node types implement a JSON-like ToString() method. Its result may look like this for a sample object node with two nested value nodes:
This representation is extensively used on the rest of the pages.
Binding is the process of initializing a model (an instance of almost arbitrary type) with data from a obtained from a :
node {A: 1, B: 2} --> new CustomModel { A = 1, B = 2}
The resulting model is queried by the application code with a to access settings.
Binding is implemented by a set of , each of which knows how to convert a
This page describes how deal with errors arising from and .
It can be summarized in two simple rules:
If an error occurs and no correct settings instance has been observed for the requested type thus far, the error is propagated to the calling code, resulting in exceptions from Get method. A subsequent settings update with correct data automatically "heals" future Get calls.
are containers used to represent objects with named fields/properties. Each object node contains a map of child nodes with their names as keys. There is no limit to nesting: objects can contain other objects and arrays. Elements of an object are required to have non-null names.
Object nodes are typically mapped to arbitrary classes and structs during .
cache settings for each (type, source) pair where sources are compared by reference. Caching ensures a solid performance level: only the first Get call is somewhat expensive while all the subsequent ones are extremely cheap. The cache is automatically updated when the underlying issues new data.
{
"A": "1",
"B": "2"
}ISettingsNode
Special care should be taken when using Get and Observe methods with short-lived source instances passed on per-call basis. This could cause poor performance due to cache misses and even lead to cache overflow events. Overflow events may cause violations of error handling guarantees. Default cache capacity is 50 but can be tuned in provider settings:
This pitfall is easy to fall into as all of the source-related extensions (combine, scope, transform, etc) return decorators that are treated as distinct sources. The only viable solution is to cache these derivative sources.
Binders are composable: if there's a registered binder for Dictionary<T1, T2>, string and int, then it's possible to bind to Dictionary<string, int>. This is heavily used for collections.
A typical binding process starts with a class binder and proceeds downward by matching fields and properties with scoped node subtrees by names and invoking appropriate binders:
See all binders descriptions to learn more about this process.
Binding fails if there's at least one error on any level. Errors may arise from incorrect value formats for primitives, missing values for required fields and properties, mismatches of settings node types or missing binders for requested types.
In case of failure, a complete list of all errors is presented in resulting exception.
GitHub repository: vostok/configuration.sources.json
NuGet package: Vostok.Configuration.Sources.Json
Cement users should reference this module with the following command:
The second guarantee can be violated by cache overflow events. Read the caching section to find out how to avoid them.
var settings = new ConfigurationProviderSettings
{
MaxSourceCacheSize = 100_000
};
var provider = new ConfigurationProvider(settings);provider.Get<MySettings>(); // may block and incurs binding costs
provider.Get<MySettings>(); // instantly returns a cached object
// ... the source issues a data update ...
provider.Get<MySettings>(); // instantly returns the old cached object
// ... the cache is automatically updated in background...
provider.Get<MySettings>(); // instantly returns an updated cached objectNode:
{
Timeout1: "1 seconds",
Timeout2: "2 seconds"
Logging:
{
Enabled: "true",
Levels: ["Info", "Warn"]
}
}Model:
class AppSettings
{
public TimeSpan Timeout1 { get; }
public TimeSpan Timeout2 { get; }
public LoggingSettings Logging { get; }
class LoggingSettings
{
public bool Enabled { get; }
public LogLevel[] Levels { get; }
}
}Binding process:
Node --> AppSettings via ClassStructBinder
Node["timeout1"] --> Timeout1 via PrimitiveBinder
Node["timeout2"] --> Timeout2 via PrimitiveBinder
Node["logging"] --> Logging via ClassStructBinder
Node["logging"]["enabled"] --> Enabled via PrimitiveBinder
Node["logging"]["levels"] --> Levels via ArrayBinder
Node["logging"]["levels"][0] --> Levels[0] by EnumBinder
Node["logging"]["levels"][1] --> Levels[1] by EnumBinderInstall-Package Vostok.Configuration.Sources.Jsoncm ref add vostok.configuration.sources.json <path-to-project>// the source contains incorrect data
provider.Get<MySettings>(); // throws an exception
// the source updates with correct data
provider.Get<MySettings>(); // returns settings
// the source updates with incorrect data once again
provider.Get<MySettings>(); // returns settings from cacheProperty
Values
Description
ObjectMergeStyle
Deep, Shallow
Type of the merge procedure performed on . Deep is the default style.
ArrayMergeStyle
Replace, Concat, Union, PerElement
Type of the merge procedure performed on . Replace is the default style.
SettingsNodeMerger is a handy public helper used to merge arbitrary nodes that handles nulls:
If both nodes are null, the result is also null.
If one of the nodes is null, the non-null node wins:
left + null --> left
null + right --> right
If nodes are of different types, the right node always wins:
value + array --> array
array + object --> object
If nodes are of same type, special rules apply. They are described in the next sections.
Right node always wins: left value + right value --> right value.
Replace style (default) always preserves the right array:
left array + right array --> right array.
Concat style produces an array containing elements from both arrays. All elements from the left array, then all elements from the second one, preserving order inside arrays.
[1, 2] + [2, 3] --> [1, 2, 2, 3]
Union style produces an array containing unique items from both arrays. The order is the same to Concat style.
[1, 2, 3] + [2, 3, 4] --> [1, 2, 3, 4]
Per element style produces an array containing items obtained by merging corresponding items (by index) from both arrays. If merged arrays have different children count, the "tail" of the longer array is preserved as-is.
[1, 2, 6] + [4, 5] --> [4, 5, 6]
Deep style (default) produces an object with union of the children from both nodes, then merges children with same names recursively.
{A:1} + {B:2} --> {A:1, B:2}
{A: {C:1}, B: {D:2}} + {A: {E:3}, B: {F:4}} --> {A: {C:1, E:3}, B: {D:2, F:4}}
Shallow style compares children of both nodes by names. If the sets of names match, regardless of order, merges the pairs of matching children recursively. Elsewise, just prefers to return the right node.
{A:1} + {B:2} --> {B:2}
{A:1} + {A:2} --> {A:2}
Name
Required if nested in an , optional otherwise.
Value
Always returns null. Only can have values.
Children
Returns an unordered sequence of child nodes.
ChildrenCount
Returns the number of elements in the Children sequence.
this[name]
Returns a child node with given name or null if such a node does not exist.
Two object nodes are considered equal if their Children sequences are equivalent (contain equal elements but may present different order) and their names match up to differences in case.
roperty
Description
var result = SettingsNodeMerger.Merge(left, right, SettingsMergeOptions.Default);Object(Value("A", "1"), Value("B", "2")) --> { "A": "1", "B": "2" }object + value --> valueThis library contains a few basic implementations of configuration sources, such as ObjectSource, FileSource, ConstantSource, CommandLineSource, EnvironmentVariablesSource. It also hosts a set of extensions to manipulate sources (combine, scope, freeze, nest, transform, template) and helpers aiding in creation of custom sources.
GitHub repository:
NuGet package:
users should reference this module with the following command:
This library contains core interfaces (IConfigurationSource, IConfigurationProvider), intermediate data model (settings nodes) and attributes used to annotate user models, such as RequiredAttribute.
GitHub repository:
NuGet package:
users should reference this module with the following command:
This library contains a source that fetches settings from ClusterConfig. It also allows to apply custom parsers in order to support formats other than CC's native one.
GitHub repository:
NuGet package:
users should reference this module with the following command:
This library contains YAML-based configuration source implementations: file and string. It also exposes a configuration parser for integration with other sources, such as ClusterConfig.
GitHub repository:
NuGet package:
users should reference this module with the following command:
This library contains an implementation of configuration provider, binding, and a couple of custom configuration primitives (such as DataSize and DataRate).
GitHub repository:
NuGet package:
users should reference this module with the following command:
This library provides an integration with Vostok logging, allowing to log errors and settings updates.
GitHub repository:
NuGet package:
users should reference this module with the following command:
Install-Package Vostok.Configuration.Sources.ClusterConfigvar source = new JsonStringSource("<json content>");
​source.Push("<json content>") // update with new contentvar source = new JsonFileSource("settings/config.json");Install-Package Vostok.Configuration.Sourcescm ref add vostok.configuration.sources <path-to-project>Install-Package Vostok.Configuration.Abstractionscm ref add vostok.configuration.abstractions <path-to-project>cm ref add vostok.configuration.sources.cc <path-to-project>Install-Package Vostok.Configuration.Sources.Yamlcm ref add vostok.configuration.sources.yaml <path-to-project>Install-Package Vostok.Configurationcm ref add vostok.configuration <path-to-project>Install-Package Vostok.Configuration.Loggingcm ref add vostok.configuration.logging <path-to-project>NuGet package: Vostok.Configuration.Sources.Vault
Cement users should reference this module with the following command:
GitHub repository: vostok/configuration.microsoft
NuGet package: Vostok.Configuration.Microsoft
Cement users should reference this module with the following command:
Install-Package Vostok.Configuration.Microsoftcm ref add vostok.configuration.microsoft <path-to-project>GitHub repository: vostok/configuration.sources.xml
NuGet package: Vostok.Configuration.Sources.Xml
Cement users should reference this module with the following command:
Install-Package Vostok.Configuration.Sources.Vaultcm ref add vostok.configuration.sources.vault <path-to-project>Install-Package Vostok.Configuration.Sources.Xmlcm ref add vostok.configuration.sources.xml <path-to-project>Location: main sources module.
ObjectSource converts an arbitrary object (including anonymous types) to a settings node:
var source = new ObjectSource(new {A = 1, B = 2, C = new[] {1, 2, 3});Conversion process obeys the following priority list:
Objects with overridden ToString are converted to value nodes;
Dictionaries with primitive key types are converted to ;
Conversion is performed recursively for keys and values;
Objects that implement IEnumerable are converted to ;
Conversion is performed recursively for sequence elements;
Everything else is converted to ;
Conversion is performed recursively for public instance fields and properties;
Like , object source does not produce any updates by itself; conversion exceptions are exposed via (null, error) notifications.
Location: main sources module.
A constant source returns a preconfigured settings node and never issues any updates.
var source = new ConstantSource(new ValueNode("value"));A lazy constant source does the same but defers the node acquisition until first subscription:
Errors in the provided delegate are propagated via (null, error) notification. It's guaranteed to execute not more than once.
Constant sources are handy for unit testing and may serve as base classes for .
Binders convert settings nodes to objects. There are built-in binders for primitives, collections, classes and structs. One can also extend the library by implementing a custom binder.
This source is not documented yet. Come back for updates later.
This source is not documented yet. Come back for updates later.
var source = new LazyConstantSource(() => new ValueNode("value"));Primitive types are parsed from string values in value nodes.
Type
Allowed format examples
string
Literally any string.
and are custom new types. They also provide factory extensions and operators:
There's also support for types that implement Parse or TryParse method with standard signature:
This allows to use arbitrary types with string representation without resorting to .
or a container (array/object) node with a single value node child.
Default value for the type is used unless .
Explicitly specified null string value has the same effect.
Parsing errors arising from incorrect value formats lead to binder failure and result in exceptions, even for optional members (see for more context).
Location: Sources.Xml module.
XmlStringSource parses well-formed XML documents from in-memory strings and supports manual external updates:
var source = new XmlStringSource("<xml content>")
source.Push("<xml content>") // update with new contentXmlFileSource parses XML files and automatically watches for changes:
var source = new XmlFileSource("settings/config.xml");A file that does not exist simply leads to a null node, which implies default settings values unless something is explicitly required.
Location: Sources.Yaml module.
YamlStringSource parses well-formed YAML documents from in-memory strings and supports manual external updates:
var source = new YamlStringSource("<yaml content>")
source.Push("<yaml content>") // update with new contentYamlFileSource parses YAML files and automatically watches for changes:
var source = new YamlFileSource("settings/config.yml");A file that does not exist simply leads to a null node, which implies default settings values unless something is explicitly required.
Arrays and lists:
T[]
List<T>
IEnumerable<T> (backed by T[])
IReadOnlyList<T>(backed by T[])
IReadOnlyCollection<T>(backed by T[])
ICollection<T> (backed by List<T>)
IList<T>(backed by List<T>)
Dictionaries:
Dictionary<TKey, TValue>
IDictionary<TKey, TValue> (backed by Dictionary)
IReadOnlyDictionary<TKey, TValue> (backed by Dictionary
Sets:
HashSet<T>
ISet<T> (backed by HashSet)
Collections can have and other collections as elements.
Collections require binders defined for element types (both keys and values in case of dictionaries).
or node. Empty nodes are converted to empty collections.
An empty collection is returned unless .
Nulls are not valid as dictionary keys.
Any element binding failure results in complete collection binding failure.
A custom element comparer (such as StringComparer.OrdinalIgnoreCase for case-insensitive dictionary keys) can be achieved by wrapping the collection in a custom type and utilizing the .
char
Literally any single character.
bool
true, True, TRUE
byte, sbyte
Anything built-in TryParse method can grok.
short, ushort
Anything built-in TryParse method can grok.
int, uint
Anything built-in TryParse method can grok.
long, ulong
Anything built-in TryParse method can grok.
float, double, decimal
1.23, 1,23, 5,12e2
Guid
Anything built-in TryParse method can grok.
Uri
http://example.com, example.com/some, /part/of/path
TimeSpan
00:12:34, 2 seconds, 500 ms, 1.5 days, 10s, 0.5 minutes
DateTime(Offset)
2018-03-14 15:09:26.535, 20050809T181142+0330
IPAddress
127.0.0.1, 2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d
IPEndPoint
192.168.1.10:80
453453 (just bytes), 1 kb, 24.3 megabytes, 500 TB
500 (just bytes/sec), 200 kilobytes/second, 5 GB/sec, 80 mb/s
Encoding
utf-8, us-ascii
Enum types
Anything built-in TryParse method can grok.
Nullable structs
A valid value or null.
class PerServiceSettings : Dictionary<string, string>
{
public PerServiceSettings(Dictionary<string, string> dictionary)
: base(dictionary, StringComparer.OrdinalIgnoreCase)
{
}
}class PerServiceSettings
{
public PerServiceSettings(Dictionary<string, string> data)
{
Index = data;
InvertedIndex = Index.ToDictionary(
pair => pair.Value,
pair => pair.Key
);
}
public Dictionary<string, string> Index { get; }
public Dictionary<string, string> InvertedIndex { get; }
}Requires: abstractions module.
By default settings are subject to logging, which is generally not appropriate for secrets. This can be fixed either by disabling logging entirely on the configuration provider or by annotating fields and properties of the model with [Secret] attribute:
class MySettings
{
[Secret]
public MySecrets Secrets { get; } = new MySecrets();
public TimeSpan Timeout { get; } = TimeSpan.FromSeconds(30);
[Secret]
public string EncryptionKey { get; }
}
class MySecrets
{
public string ApiKey { get; }
public string Login { get; }
public string Password { get; }
}Requires: main module.
When using configuration providers, Get and Observe method overloads without parameters require an explicit association of the model type with a configuration source.
// This simply won't work (no source defined):
provider.Get<MySettings>();
provider.Observe<MySettings>();// This will work as expected:
provider.SetupSourceFor<MySettings>(new JsonFileSource("settings.json"));
provider.Get<MySettings>();
provider.Observe<MySettings>();A source can only be assigned to a type before any Get or Observe calls are made for that type:
provider.SetupSourceFor<MySettings>(source1); // succeeds
provider.SetupSourceFor<MySettings>(source2); // succeeds, overwrites
provider.Get<MySettings>();
provider.SetupSourceFor<MySettings>(source3); // fails
Requires: logging module.
See logging documentation for the log interface required here.
var providerSettings = new ConfigurationProviderSettings();
providerSettings = providerSettings.WithSettingsLogging(log);
var provider = new ConfigurationProvider(providerSettings);DataSize size = 50.Megabytes();
DataRate rate = size / 2.Seconds();class/struct T
{
public static T Parse(string input) { ... }
public static bool TryParse(string input, out T result) { ... }
}Requires: abstractions module.
Name aliases provide alternative keys to look for in settings nodes when performing binding.
They can be applied to fields and properties with a special attribute:
class MySettings
{
[Alias("LegacyTimeout")]
public TimeSpan Timeout { get; }
}
// The property can be initialized from either "timeout" or "legacytimeout" keyIt's allowed to assign multiple aliases to a single member:
[Alias("alias1")]
[Alias("alias2")]
public TimeSpan Timeout { get; }Aliases do not handle ambiguity. If the source returns data with more than one of the lookup keys assigned to a field or property, binding fails with an error.
Ambiguous data examples:
{ "timeout": "...", "alias1": "..." }
{ "alias1": "...", "alias2": "..." }The binder starts off by creating an instance of the bound type with default values of public fields and properties (the same way as Activator.CreateInstance does) and then recursively binds the value of every field and property to a corresponding subsection of the settings tree. See binding nodes to models page for a step-by-step illustration of this process.
Fields and properties don't have to be mutable: readonly fields and properties with private setters (or even without one) are fine.
Following members are ignored: indexers, constants, static and non-public fields and properties, computed properties without backing fields.
Nested classes and are supported without restrictions.
When binding a field or property value, the settings tree is to the member name. This imposes a requirement to synchronize naming in configuration data provided by and models in the application code. In order to bind a model property named Timeout from a section of JSON file, this section must contain a property with the same name (barring case).
allow to decouple property names from configuration data names.
By default all fields and properties are treated as optional: they are just left with default values if no data could be found in the settings tree (default values may come from field and property initializers). It's possible to though. Required members with no data in the settings tree cause the binding process to fail and produce an exception.
Model classes must either have a parameterless constructor, a constructor with a single parameter, or have an OmitConstructorsAttribute (which allows , therefore, no default parameters are present).
Binders are required for all public fields and properties.
Only are supported.
Requires: sources module.
Value substitution feature allows to replace placeholders in settings data. It's based on the transformation extensions and uses #{} syntax for placeholders.
var originalSource = new JsonFileSource("settings.json");
var substitutions = new []
{
new Substitution("param1", "value1"),
new Substitution("param2", () => "value2")
};
var templatedSource = originalSource.Substitute(substitutions);Data in originalSource:
{
"param1": "#{param1}",
"param2": "#{param2}",
"param3": "value3",
"param4": "#{param4}"
}Requires: sources module.
This operation allows to embed source's data into a nested object section:
var source = new ObjectSource(new {A = 1, B = 2});
var nestedSource = source.Nest("obj1", "obj2");Data in nestedSource:
{
"obj1":
{
"obj2":
{
"A": "1",
"B": "2"
}
}
}Model JSON data source
class Settings {
{
TimeSpan Timeout { get; } -------> "timeout": "2 seconds",
int Parallelism { get; } -------> "parallelism": 32
} }Data in templatedSource:
{
"param1": "value1",
"param2": "value2",
"param3": "value3",
"param4": "#{param4}"
}Requires: main module.
Configuration provider class has a property named Default that serves as a global static source of IConfigurationProvider instance.
It's intended use case is self-sufficient configuration in libraries: library authors may not want to force their users to provide an IConfigurationProvider instance each time they're using library classes. Instead, these classes could just obtain a log from the shared property:
ConfigurationProvider.Default.SetupSourceFor<MyLibrarySettings>(...);
var settings = ConfigurationProvider.Default.Get<MyLibrarySettings>();By default this property returns a singleton provider with default settings. It can be configured explicitly in the application:
var providerSettings = new ConfigurationProviderSettings { ... };
var provider = new ConfigurationProvider(providerSettings);
// Will return false if already explicitly configured:
ConfigurationProvider.TrySetDefault(provider);Requires: main module.
The basic recommended way to get most up-to-date settings in the presence of background updates is to use provider's Get method (see the relevant scenario) on each access attempt. However, it's also possible to obtain an inherently dynamic settings object whose properties are updated under the hood. This requires to use an interface as the settings model:
interface IMySettings
{
string Option1 { get; }
string Option2 { get; }
ISubConfig Section { get; }
}
// Get once, use as a singleton:
var hotSettings = provider.CreateHot<IMySettings>();
// Properties of the 'hotSettings' object are mutable.
// They will automatically return most up-to-date values.Note that in order to get a guaranteed consistent view of the settings without "tearing" (observing a mix of values from before and after update due to a race condition) when using a nested object (section), it's recommended to access a snapshot of this nested object saved in a variable:
var section = hotSettings.Section;
// access a consistent view of 'section` propertiesRequires: main sources module.
Multiple configuration sources can be combined into a single composite source whose data is produced by merging the settings trees provided by original sources.
var source1 = new JsonStringSource(...);
var source2 = new YamlStringSource(...);
var source3 = new XmlStringSource(...);
var combined = source1.CombineWith(source2, source3);
// combined == Merge(source1, source2, source3)
// combined.Data == Merge(Merge(source1.data, source2.data), source3.data)Order of the sources is important: settings from sources that come later in the list have greater priority, hence the rightmost source should be the most specific/significant. In other words, merging is performed in a left-to-right fashion.
Updates are pushed to subscribers each time one of the component sources generates new settings.
Requires: , (constraints).
Validation feature allows to associate a custom user-made validator with a settings type. Validation occurs during and results in binding errors for incorrect settings.
There are also built-in validation constraints you can use to create a custom validator. Just inherit your validator class from ConstraintsValidator and override a method returning constraints to be checked:
Requires: .
This extension point covers scenarios where are not sufficient to handle required types.
Custom binders should be used as a last resort when and are not powerful enough.
Creating a binder for a type is just a matter of implementing an interface:
Custom binders can be applied to specific fields and properties or whole types:
NotNullConstraint for arbitrary reference types;
NotNullOrEmptyConstraint for strings;
NotNullOrWhitespaceConstraint for strings;
RangeConstraint, LessConstraint, LessOrEqualConstraint, GreaterConstraint and GreaterOrEqualConstraint for any types that implement IComparable;
UniqueConstraint to check that a set of field/properties only contains unique values;
class MyComplexTypeBinder : ISettingsBinder<MyComplexType>
{
public MyComplexType Bind([CanBeNull] ISettingsNode node)
{
...
}
}class MySettings
{
[BindBy(typeof(MyComplexTypeBinder))]
public MyComplexType ComplexProperty { get; }
}var source = new JsonFileSource("settings.json");
var frozenSource = source.Freeze();
// frozenSource will not issue updates after first successful onevar source = new JsonFileSource("settings.json");
ISettingsNode contents = source.Get();
Console.Out.WriteLine(contents);var baseSource = new JsonFileSource("settings.json");
var scopedSource = baseSource.ScopeTo("secrets");
var scopedTooFarSource = baseSource.ScopeTo("timeouts", "unknown-section");Data in baseSource:
{
"Timeouts":
{
"DbTimeout": "20 seconds"
},
"Secrets":
{
"ApiKey": "xxxx-xxxx-xxxx"
}
}For sources based on local files. Includes automatic reload on file updates.
[ValidateBy(typeof(MySettingsValidator))]
class MySettings
{
int CacheCapacity { get; }
}class MySettingsValidator: ISettingsValidator<MySettings>
{
// Returns validation errors. Empty enumerable == success.
public IEnumerable<string> Validate(MySettings settings)
{
if (settings.CacheCapacity <= 0)
yield return "Cache capacity must be positive.";
}
}[ValidateBy(typeof(TestConfigValidator))]
class TestConfig
{
public int Number;
public string String;
}class TestConfigValidator : ConstraintsValidator<TestConfig>
{
protected override IEnumerable<Constraint<TestConfig>> GetConstraints()
{
yield return new NotNullOrWhitespaceConstraint<TestConfig>(settings => settings.String);
yield return new RangeConstraint<TestConfig, int>(settings => settings.Number, 2, 10);
}
}[BindBy(typeof(MyComplexTypeBinder))]
class MyComplexType
{
}Data in scopedSource:
{
"Secrets":
{
"ApiKey": "xxxx-xxxx-xxxx"
}
}Data in scopedTooFarSource:
{
}class MySource : ManualFeedSource<string>
{
public MySource()
: base(Parse) { }
private static ISettingsNode Parse(string input) { ... }
}
var source = new MySource();
source.Push("input1");
source.Push("input2");
// Push method can be used by an internal periodical/event-based update routine.class MyFileSource : FileSource
{
public MyFileSource(string filePath)
: base(new FileSourceSettings(filePath), Parse) { }
private static ISettingsNode Parse(string input) { ... }
}Requires: abstractions module, main module.
var settings = new MySettings();
var source = GetSource(); // IConfigurationSource
source.ApplyTo(settings); // doesn't overwrite fields and properties not present in the source