Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Array nodes 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 binding.
roperty
Description
Name
Value
Children
Returns an ordered sequence of child nodes.
ChildrenCount
Returns the number of elements in the Children
sequence.
this[name]
Two array nodes are considered equal if their Children
sequences are equal elementwise and their names match up to differences in case.
Required if nested in an , optional otherwise.
Always returns null
. Only can have values.
Always returns null
. Arrays cannot be navigated with .
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
Description
Name
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
.
Two value nodes are considered equal if their values match exactly and their names match up to differences in case.
Required if nested in an , optional otherwise.
Object nodes 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 binding.
roperty
Description
Name
Value
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.
Merge is the operation of reducing two settings nodes to one: left + right --> result
.
Its primary use is to combine configuration sources.
Each node type implements a Merge
method that accept another node (right
in our terms) and an instance of merge options with following properties:
Property
Values
Description
ObjectMergeStyle
Deep
, Shallow
ArrayMergeStyle
Replace
, Concat
, Union
, PerElement
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
object + value --> value
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}
Required if nested in an , optional otherwise.
Always returns null
. Only can have values.
Type of the merge procedure performed on . Deep
is the default style.
Type of the merge procedure performed on . Replace
is the default style.
Configuration provider is responsible for the binding process and offers methods to obtain final settings models. It's also responsible for caching and error handling. Providers are used directly by the application code to either obtain settings on demand or subscribe to updates.
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;
A settings node is a tree with string keys and string values in its leaf nodes.
Settings nodes are immutable objects.
There are three types of nodes:
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).
This representation is extensively used on the rest of the pages.
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:
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.
produce settings nodes as their primary artifacts.
is the process of converting a settings node to an arbitrary .NET object.
Settings nodes are implemented in the .
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 to objects in C# code via different node types and .
Implementation of a ;
Implementation of a ;
of configuration sources
, used to hold data;
, used to represent sequences;
, used to represent objects with named properties.
All node types implement the interface and have following properties:
All nodes also implement a operation.
All node types implement a JSON-like ToString()
method. Its result may look like this for a sample with two nested :
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.
Due to caching, instances should be reused as much as possible. Ideally there should be just one singleton instance in the application.
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 . Default cache capacity but can be tuned in provider settings:
This pitfall is easy to fall into as all of the source-related extensions (, , , etc) return decorators that are treated as distinct sources. The only viable solution is to cache these derivative sources.
Configuration sources fetch data from storage (local files or remote APIs) and convert it to , 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 (see and scenarios).
Sources are also responsible for data change detection. They expose a with subscription support:
It's also possible to .
Property
Type
Description
Name
string
Node name. Case-insensitive.
Value
string
Node value. Only present in value nodes.
Children
IEnumerable<ISettingsNode>
this[name]
ISettingsNode
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.
This page describes how configuration providers deal with errors arising from sources and binders.
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.
If an error occurs and a correct settings instance has already been observed for the requested type at least once, the error is reported in background and does not cause Get method to fail: last seen correct instance is returned from cache instead.
The second guarantee can be violated by cache overflow events. Read the caching section to find out how to avoid them.
Observe method never produces OnError
or OnCompleted
notifications. It only reports successful settings updates. The errors are handled in background and can be logged.
Binding is the process of initializing a model (an instance of almost arbitrary type) with data from a settings tree obtained from a configuration source:
node {A: 1, B: 2} --> new CustomModel { A = 1, B = 2}
The resulting model is queried by the application code with a configuration provider to access settings.
Binding is implemented by a set of binders, each of which knows how to convert a settings node to an object of a specific type.
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.