.. _csharp-api: .. default-domain:: csharp .. namespace:: RedDove.Config .. meta:: :description: CFG configuration format C# API :keywords: CFG, configuration, .NET, C# The CFG API for .NET -------------------- The CFG reference implementation for the .NET Common Language Runtime (CLR) is written in C#. The implementation assumes a recent version of .NET - either .NET Framework >= 4.8 or .NET Core >= 3.0.0. It's implemented using the namespace ``RedDove.Config`` and packaged in ``RedDove.Config.dll``. Installation ++++++++++++ The library can be installed using ``nuget`` and the package name ``RedDove.Config``. There's a minimal example of a program and project that uses CFG `here `__. Exploration +++++++++++ To explore CFG functionality for .NET, we use the `dotnet-script` Read-Eval-Print-Loop (REPL), which is available from `here `_. Once installed, you can invoke a shell using .. code-block:: shell $ dotnet dotnet-script Getting Started with CFG in C# ++++++++++++++++++++++++++++++ .. include:: api-snip-01.inc .. include:: cs-test0.cfg.txt Loading a configuration ~~~~~~~~~~~~~~~~~~~~~~~ The configuration above can be loaded as shown below. In the REPL shell: .. code-block:: csx > #r "RedDove.Config.dll" > using RedDove.Config; > var cfg = new Config("test0a.cfg"); > cfg["a"] "Hello, " > cfg["b"] "world!" Access elements with keys ~~~~~~~~~~~~~~~~~~~~~~~~~ Accessing elements of the configuration with a simple key is just like using a ``Dictionary``: .. code-block:: csx > cfg["a"] "Hello, " > cfg["b"] "world!" Access elements with paths ~~~~~~~~~~~~~~~~~~~~~~~~~~ As well as simple keys, elements can also be accessed using :term:`path` strings: .. code-block:: csx > cfg["c.d"] "e" .. include:: api-snip-02.inc .. code-block:: csx > cfg["f.g"] "h" .. include:: api-snip-03.inc Access to date/time objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can also get native CLR ``System.DateTime`` and ``System.DateTimeOffset`` objects from a configuration, by using an ISO date/time pattern in a :term:`backtick-string`: .. code-block:: csx > cfg["christmas_morning"] [25/12/2019 08:39:49] Access to other CLR objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Access to other CLR objects is also possible using the :term:`backtick-string` syntax, provided that they are either environment values or objects accessible via public static fields, properties or methods which take no arguments: .. code-block:: csx > cfg["access"] ReadWrite > cfg["today"] [15/01/2020 00:00:00] Accessing the "access" element of the above configuration accesses the named value in the ``System.IO.FileAccess`` enumeration. Accessing the "today" element of the above configuration invokes the static method ``System.DateTime.Today()`` and returns its value. Access to environment variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To access an environment variable, use a :term:`backtick-string` of the form ```$VARNAME```: .. code-block:: csx > cfg["home"].Equals(Environment.GetEnvironmentVariable("HOME")) true .. include:: api-snip-07.inc .. code-block:: csx > cfg["foo"] "bar" Access to computed values ~~~~~~~~~~~~~~~~~~~~~~~~~ .. include:: api-snip-04.inc .. code-block:: cfg-body :caption: test0a.cfg total_period : 100 header_time: 0.3 * ${total_period} steady_time: 0.5 * ${total_period} trailer_time: 0.2 * ${total_period} base_prefix: '/my/app/' log_file: ${base_prefix} + 'test.log' When this file is read in, the computed values can be accessed directly: .. code-block:: csx > cfg["header_time"] 30 > cfg["steady_time"] 50 > cfg["trailer_time"] 20 > cfg["log_file"] "/my/app/test.log" Including one configuration inside another ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There are times when it's useful to include one configuration inside another. For example, consider the following configuration files: .. code-block:: cfg-body :caption: logging.cfg layouts: { brief: { class: `log4net,log4net.Layout.PatternLayout`, pattern: '%d [%t] %p %c - %m%n' } }, appenders: { file: { class: `log4net,log4net.Appender.FileAppender`, layout: 'brief', append: false, charset: 'UTF-8' level: 'INFO', filename: 'run/server.log', append: true, }, error: { class: `log4net,log4net.Appender.FileAppender`, layout: 'brief', append: false, charset: 'UTF-8' level: 'ERROR', filename: 'run/server-errors.log', }, debug: { class: `log4net,log4net.Appender.FileAppender`, layout: 'brief', append: false, charset: 'UTF-8' level: 'DEBUG', filename: 'run/server-debug.log', } } loggers: { mylib: { level: 'INFO' } 'mylib.detail': { level: 'DEBUG' } }, root: { handlers: ['file', 'error', 'debug'], level: 'WARNING' } .. code-block:: cfg-body :caption: redirects.cfg cookies: { url: 'http://www.allaboutcookies.org/', permanent: false }, freeotp: { url: 'https://freeotp.github.io/', permanent: false }, 'google-auth': { url: 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', permanent: false } .. code-block:: cfg-body :caption: main.cfg secret: 'some random application secret', port: 8000, sitename: 'My Test Site', default_access: 'public', ignore_trailing_slashes: true, site_options: { want_ipinfo: false, show_form: true, cookie_bar: true }, connection: 'postgres+pool://db_user:db_pwd@localhost:5432/db_name', debug: true, captcha_length: 4, captcha_timeout: 5, session_timeout: 7 * 24 * 60 * 60, # 7 days in seconds redirects: @'redirects.cfg', email: { sender: 'no-reply@my-domain.com', host: 'smtp.my-domain.com:587', user: 'smtp-user', password: 'smtp-pwd' } logging: @'logging.cfg' .. include:: api-snip-05.inc .. code-block:: csx $ dotnet dotnet-script > #r "RedDove.Config.dll" > #r "log4net.dll" > using RedDove.Config; > var cfg = new Config("main.cfg"); > cfg["logging.appenders.file.class"] [log4net.Appender.FileAppender] Avoiding unnecessary repetition ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. include:: api-snip-08.inc .. code-block:: cfg-body :caption: logging.cfg (partial) appenders: { file: { class: `log4net,log4net.Appender.FileAppender`, layout: 'brief', append: false, charset: 'UTF-8' level: 'INFO', filename: 'run/server.log', append: true, }, error: { class: `log4net,log4net.Appender.FileAppender`, layout: 'brief', append: false, charset: 'UTF-8' level: 'ERROR', filename: 'run/server-errors.log', }, debug: { class: `log4net,log4net.Appender.FileAppender`, layout: 'brief', append: false, charset: 'UTF-8' level: 'DEBUG', filename: 'run/server-debug.log', } } This portion could be rewritten as: .. code-block:: cfg-body :caption: logging.cfg (partial) defs: { base_appender: { class: `log4net,log4net.Appender.FileAppender`, layout: 'brief', append: false, charset: 'UTF-8' } }, appenders: { file: ${defs.base_appender} + { level: 'INFO', filename: 'run/server.log', append: true, }, error: ${defs.base_appender} + { level: 'ERROR', filename: 'run/server-errors.log', }, debug: ${defs.base_appender} + { level: 'DEBUG', filename: 'run/server-debug.log', } } .. include:: api-snip-06.inc .. code-block:: csx $ dotnet dotnet-script > #r "RedDove.Config.dll" > #r "log4net.dll" > using RedDove.Config; > var cfg = new Config("main.cfg"); > cfg["logging.appenders.file.class"] [log4net.Appender.FileAppender] > cfg["logging.appenders.file.level"] "INFO" > cfg["logging.appenders.file.layout"] "brief" > cfg["logging.appenders.file.append"] true > cfg["logging.appenders.file.filename"] "run/server.log" > cfg["logging.appenders.error.append"] false > cfg["logging.appenders.error.filename"] "run/server-errors.log" .. include:: api-snip-09.inc .. code-block:: cfg-body :caption: logging.cfg (partial) defs: { base_appender: { class: `log4net,log4net.Appender.FileAppender`, layout: 'brief', append: false, charset: 'UTF-8' } log_prefix: 'run/', }, layouts: { brief: { class: `log4net,log4net.Layout.PatternLayout`, pattern: '%d [%t] %p %c - %m%n' } }, appenders: { file: ${defs.base_appender} + { level: 'INFO', filename: ${defs.log_prefix} + 'server.log', append: true, }, error: ${defs.base_appender} + { level: 'ERROR', filename: ${defs.log_prefix} + 'server-errors.log', }, debug: ${defs.base_appender} + { level: 'DEBUG', filename: ${defs.log_prefix} + 'server-debug.log', } } with the same result as before. It *is* slightly more verbose than before, but the location of all files can be changed in just one place now, as opposed to three, as it was before. Namespaces ++++++++++ Everything is defined in the ``RedDove.Config`` namespace. Classes +++++++ The ``Config`` class ~~~~~~~~~~~~~~~~~~~~ This class implements a CFG configuration. You'll generally interface to CFG files using this class. When you pass in a stream or path to the constructor or the :meth:`Config.Load` / :meth:`Config.LoadFile` methods, the CFG source in the stream is parsed and converted into an internal form using Abstract Syntax Tree (AST) nodes. .. index:: single: Config; C# class .. class:: Config .. cssclass:: class-members-heading Properties .. property:: bool NoDuplicates { get; set; } Whether this instance allows duplicate keys. If it doesn't, later values with the same key overwrite earlier values. If it does, a ``ConfigException`` is raised. .. property:: bool StrictConversions { get; set; } Whether string conversions by this instance are strict. If not strict, a string which cannot be converted is returned as is. If strict, a string which cannot be converted will cause a ``ConfigException`` to be thrown. .. property:: IDictionary Context { get; set; } A mapping within which to look up identifiers during evaluation of AST nodes. .. property:: bool Cached { get; set; } If ``true``, an internal cache is used. The default is ``false``. .. property:: string Path { get; set; } The path to the file from which the configuration has been read. You won't usually need to set this, unless you want to load a configuration from text that references included files. In that case, set the path to a value whose parent directory contains the included files, or specify relevant directories using :prop:`IncludePath`. When a configuration is loaded from a file, this attribute is automatically set. .. property:: IList IncludePath { get; set; } A list of directories which is searched for included sub-configurations if the parent directory of :prop:`Path` (or the current directory, if that isn't set) doesn't contain them. .. cssclass:: class-members-heading Constructors .. method:: Config() Constructs an instance with no actual configuration loaded. A call to :meth:`Load` or :meth:`LoadFile` would be needed to actually make a usable instance. .. method:: Config(System.IO.TextReader reader) Construct this instance with the configuration read from the provided reader. .. method:: Config(string path) Construct this instance with the configuration read from a file named by the provided path. .. cssclass:: class-members-heading Methods .. method:: void Load(System.IO.TextReader reader) Load this instance with the configuration read from the provided reader. :param reader: A :class:`System.IO.TextReader` instance for the configuration source. :throws ConfigException: If there is an I/O error when reading the source or an error in the configuration syntax. .. method:: void LoadFile(string path) Load this instance with the configuration read from the file at the provided path. The :prop:`Path` property is set from `path`. :param path: A String specifying the location of the file which contains the configuration. :throws ConfigException: If there is an I/O error when reading the source or an error in the configuration syntax. .. method:: object Get(string key) This method implements the key access operator. The value of `key` can be a simple key (identifier) or a valid path string. :param key: A string describing a simple key or a path in the configuration. :returns: The object found at ``key``. :throws InvalidPathException: If there is an error trying to interpret a non-existent key as a path. :throws ConfigException: If a key isn't found. .. method:: object Get(string key, object defaultValue) This method implements the key access operator. The value of `key` can be a simple key (identifier) or a valid path string. If a key isn't found, `defaultValue` is returned. :param key: A string describing a simple key or a path in the configuration. :param defaultValue: The default value to use if a key isn't present in the configuration. :returns: The object found at ``key``, or ``defaultValue``. .. indexer:: object this [string key] { get; } This acts the same way as the :meth:`Get` method without a default value. :returns: The object found at ``key``. :throws InvalidPathException: If there is an error trying to interpret a non-existent key as a path. :throws ConfigException: If a key isn't found.