Table of Contents

Configuration Framework

Versatile configuration capabilities with adjustable level of detail is an indispensable mainstay of any software system that aims to meet generally mandated architecture and design principles.

Some non exhaustive examples should help to illustrate this even more

  • Open-Close principle
    A piece of code (like a function or method) that is operating on the data contained in a file could be made much more useful if the name of the file would be passed as a parameter instead of having the file being hard-coded. This is pretty obvious since it avoids the need to change and recompile the program every time the name of the file would change.
    (So the principle says, the function is open for extension, but closed for modification…)

  • Separation of Concern
    By separating the definition and storage of parameters from the procedures and algorithms that are controlled and fine tuned by these parameters.

  • Dependency Injection
    In order to have loosely coupled application services we would follow all sorts of design pattern like dependency inversion and explicit dependency declaration - but at the point where the rubber meets the road, the big question is: Who finally defines which implementation of an interface is effectively injected into the service?
    Yes right, the configuration of course!

  • Architectural Agility
    Provides deployment and customization flexibility in any case where we would need to deploy an application into different environments like local development, integration testing, cloud containers or just different customers with even different requirements - we could avoid to create specifically targeted builds by just the adjustment of the configuration.

Now that we have gained some motivation for the demand of a flexible configuration, lets see how it can be supported by the Tlabs Library.

Configuration Source

The startup helpers of the Tlabs Library can arrange the application's configuration to be sourced as an aggregate of any location like:

  • Command line parameters

  • Environment variables

  • one ore more JSON files

  • custom extension
    (e.g. from a centralized configuration store)

The resulting configuration contents actually end up in an source agnostic hierarchical key/value representation to be consumed programmatically by any component or service of the application. But more typically these are specifically used by the Tlabs Library readily provided configuration framework to setup all application services.

Configuration Targets

In general all framework and application services that are aimed to be configured are in turn managed by a small set of core or root services:
(with the popular DDD approach these are also referred to as shared kernels)

  • Application Host
    (A root application service to mange all core lifetime functionality which could include optional HTTP or similar network protocol or system related low level background services.)

  • Application Service Provider
    (A container, service factory, dependency resolver and lifecycle manager for all configured application services.)

  • App Middleware (optional)
    (A pipeline (building a chain of responsibility) to coordinate data flow between the host layer, through optional selectable intermediate middleware filters and the effective application services.)

Technically these kernel services are being setup by application of the builder pattern in order to assemble a complex object by incrementally adding properties and details until the final service object is ready to be actually build or constructed. (The resulting complex service would now serve as a container for a large and complex set of information and functionality.)

Motivated by the reasons outlined at the beginning of this document the ambition of the Tlabs Library is to help making the usual programmatically hard-coded build and setup process for the core building blocks of an application much more configurable as it normally would be out of the box.

This is achieved with the introduction of Configurators used to incapsulate small pieces of the entire configuration. A Configurator takes the builder of the target root service and uses it to programmatically apply the configuration of one aspect (or even several closely related ones) of the global setup. (Please refer to this typical Configurator as an example).

The configuration itself now specifies which Configurators are getting applied (or more technical: are getting executed) and also which optional parameters these Configurators will take.

A minimal example of the configuration of the Application Service Provider would now look like this:

 "applicationServices": {
 "systemCli": {
 "type": "Tlabs.Sys.SystemCli+Configurator, Tlabs.Core",
 "sysCommands": {
 "LINUX": {
 "shell": ["/bin/bash", "{0}"],
 "cmdLines": {
 "hello": {"cmd": ["./hello.sh", "{0}", "{1}"], "wrkDir": "rsc/cmd"},
 }
 }
 }
 },
 //more services...
 }

This configures an example service used to run sub-processes with a command line given by the configuration. Here the Configurator systemCli (the name is by one's choice) is specified with its type (given by its AssemblyQualifiedName) - this specific Configurator takes parameters from the configuration section sysCommands where any further details for the sub-process execution are given.

While this is showing how to configure the set of application services, the same pattern even applies to every kernel service area like Application Host, Middleware and even Logging and thus allows to configure the entirety of an application which also implies another noteworthy configuration option:
If one would stick with the principle to use the Service Provider not just for dependency injection but also as an abstract factory, the configuration could now be used to control which particular implementation of an interface is actually being provided (as the dependency). Let’s see what this could mean:

While a closer look on the Tlabs Library’s Data Persistence Abstraction is given in a separat document, lets assume here an application that uses a database for storing data and is using a configuration to specify details about the database:

 "applicationServices": {
 "Persistence.SqLiteDbProvider": {
 "type": "Tlabs.Data.Store.SqliteConfigurator, App.SqLiteDataStore",
 "config": {
 "connection": "DataSource=rsc/store/application_db.sqlite;Foreign Keys=True"
 }
 },
 //more services...
 }

This would configure a Sqlite database system to be used for storage and would e.g. give the option to change the name of the file containing the database…
What if we were now about to get that same application deployed into a target environment with the requirement to store all the data in a PostgreSql db? Well, just let us adjust the configuration:

 "applicationServices": {
 "Persistence.PostgreSQL": {
 "type": "Tlabs.Data.Store.PostgreSqlConfigurator, App.PostgresDataStore",
 "config": {
 "connection": "Host=localhost;Database=myAppDb;Username=postgres;Timeout=5"
 }
 },
 //more services...
 }

Since by the help of the Tlabs Library our application is designed with complete persistence abstraction in place, the application would simply go on working and now storing/accessing its data from a PostgreSQL database with no need to change a single line of program code or rebuild. Adjusting the configuration is all what it takes (and what makes using a configuration such a big deal)!

Further Reading

interface IConfigurator<T>