Configuration
🌍 Language: English | Español
Layered configuration system for Delphi inspired by .NET’s Microsoft.Extensions.Configuration. Read settings from JSON files, INI files, environment variables, in-memory dictionaries or chained sources — all unified behind a single IConfiguration interface.
Why use this?
- 📄 Multiple sources — JSON, INI, environment variables, in-memory, or custom
- 🔗 Layered & overridable — sources are chained; later sources override earlier ones
- 🗂️ Hierarchical keys — use
:as separator (Database:ConnectionString) - 🔄 Reload support —
IConfigurationRoot.Reloadre-reads all providers - 🎯 Binder — map a configuration section directly to a Delphi class
Quick Start
uses
Daf.Configuration.Builder,
Daf.Configuration.Json,
Daf.Configuration.Env,
Daf.Extensions.Configuration;
var Builder: IConfigurationBuilder := TConfigurationBuilder.Create;
JsonConfig.AddFile(Builder, 'appsettings.json');
Builder.Add(TEnvironmentVariablesConfigurationSource.Create);
var Config := Builder.Build;
// Read a value
var ConnStr := Config['Database:ConnectionString'];
// Read a section
var DbSection := Config.GetSection('Database');
var Host := DbSection['Host'];
// Tell an absent key from one whose value is empty -- the indexer gives '' for both
if Config.Exists('Database:Password') then ; // presence, and nothing else
var Read := Config.Find('Database:Password'); // presence AND value, one lookupAbsent is not empty
Two members that .NET does not have, because there the distinction is carried by nullability: the indexer is string? this[key], null for absent and '' for present-but-empty. A Delphi string cannot be nil, so what the ? does there needs a member of its own here — Exists for the presence, Find for presence and value in a single lookup.
The indexer returns a string, so C['absent'] = '' and C['empty'] = '' are both True. The guide has the section semantics of Exists, and when the distinction bites hardest.
Supported Providers
| Provider | Unit | Description |
|---|---|---|
| JSON file | Daf.Configuration.Json |
appsettings.json, optional/required |
| INI file | Daf.Configuration.Ini |
Classic [Section]\nKey=Value format |
| Environment variables | Daf.Configuration.Env |
OS env vars, optionally filtered by prefix |
| In-memory | Daf.Configuration.Memory |
Dictionary of string → string pairs |
| Command line | Daf.Configuration.CommandLine |
Process arguments, with .NET’s syntax and short aliases |
| Chained | Daf.Configuration.Chained |
Wrap an existing IConfiguration as a source |
The first four are the .NET set: Microsoft.Extensions.Configuration’s Json, Ini, EnvironmentVariables and CommandLine. Precedence is the order in which you add them — the last source wins. No provider claims a precedence of its own.
The command line
The highest-precedence source, and the one that needs no file: it lets you override any setting when invoking the executable.
uses
Daf.Configuration.Builder,
Daf.Configuration.Json,
Daf.Configuration.CommandLine,
Daf.Extensions.Configuration;
var Builder: IConfigurationBuilder := TConfigurationBuilder.Create;
JsonConfig.AddFile(Builder, 'appsettings.json');
CommandLineConfig.AddArgs(Builder); // added last, so it overrides the file
var Config := Builder.Build;AddArgs with no arguments reads the ones this process was invoked with. The overload taking an explicit array is what makes the source testable — a process cannot fake its own ParamStr:
CommandLineConfig.AddArgs(Builder, ['--Logging:LogLevel:Default=Debug']);Accepted forms
| Form | Example |
|---|---|
key=value |
Logging:LogLevel:Default=Debug |
--key=value |
--Logging:LogLevel:Default=Debug |
--key value |
--Logging:LogLevel:Default Debug |
/key=value |
/Logging:LogLevel:Default=Debug |
/key value |
/Logging:LogLevel:Default Debug |
-alias value |
only when the alias is declared |
Keys are case-insensitive, a repeated key keeps its last value, and key= with nothing after the equals sign is a key whose value is the empty string. Nested keys use the same separator as every other source, so a value set here and a value read from appsettings.json land on the same key.
Short aliases
An alias maps a short switch onto a long key. It must start with one or two dashes, and declaring the same alias twice is a composition error — reported when the source is assembled, not when the process runs.
CommandLineConfig.AddArgs(Builder, ['-v', 'Debug'],
[TSwitchMapping.Create('-v', 'Logging:LogLevel:Default')]);⚠️ One deliberate difference from .NET. There, a single-dash switch with no alias declared and an equals sign — -x=1 — raises a FormatException. Here it is ignored, like every other argument that names no key.
Not to be confused with Daf.CommandLine
Writing a command-line application — commands, subcommands, typed options, generated help — is a different job, and it belongs to Daf.CommandLine. The two share no engine, the same way Microsoft.Extensions.Configuration.CommandLine does not use System.CommandLine.
Binding to Objects
Use TConfigurationBinder to populate a Delphi object from a configuration section:
uses Daf.Configuration.Binder;
type
TDatabaseOptions = class
public
Host: string;
Port: Integer;
Name: string;
end;
// Builds the object and binds it in one call. `boFields` because this class exposes public
// fields; the default, `boProperties`, reads published properties instead.
var Opts := TConfigurationBinder.Bind<TDatabaseOptions>(Config, 'Database', boFields);
// Opts.Host, Opts.Port, Opts.Name are now populatedA key that no source mentions is not written, so a default set in the constructor survives the bind.
Settings with an owner
TConfigurationBinder builds the object and hands it over: the caller owns it, and the caller frees it. That is still true, and it is the right thing for a utility, a script, or a startup step running before there is a container.
Inside a container it is not. ConfigOptions.Bind declares that some settings are filled from a section, and from then on the container builds them, owns them and frees them:
uses Daf.Options.Configuration;
ConfigOptions.Bind<TAppSettings>(Services, Config); // the root
ConfigOptions.Bind<TDbSettings>(Services, Config, 'Database', boFields); // a section
Services.AddSingleton<IGreeter, TGreeter>;constructor TGreeter.Create(const Options: IOptions<TAppSettings>);
begin
inherited Create;
FTitle := Options.Value.Title;
end;The service never names its section: it asks for its settings by constructor and startup decides where they come from. Changing the section is one line at the composition site and no change at all in the service.
The contract of IOptions<T> — ownership, the dangling-pointer trap, lazy construction, validation, and what happens with settings nobody registered — is in DependencyInjection.Abstractions.
A photograph, not a mirror
The section is read once, when the value is built, and changing the configuration afterwards does not change it. The providers report no change, so what a service holds is the state of the section at the moment its settings were built.
Integration with Hosting
When using Hosting, the builder already loads the appsettings files, the application’s own settings files and the environment variables; which ones and in what order is in Hosting’s guide. Add your own sources to the builder’s Configuration:
var Builder := THost.CreateBuilder;
JsonConfig.AddFile(Builder.Configuration,
TPath.Combine(Builder.Environment.ContentRootPath, 'features.json'), [csoOptional]);
// The manager reads as well as builds, so the registration consumes it on the spot. The cast
// is because the manager is reader and builder at once, and Delphi allows one parent interface.
Builder.Services.AddSingleton<IMyOptions>(
TMyOptions.Create(Builder.Configuration as IConfiguration));
var Host := Builder.Build;
try
Host.Start;
Host.WaitForShutdown;
finally
StopAndNil(Host);
end;Documentation
- 📖 Usage Guide — providers in depth, hierarchical keys, binder, custom sources
- 📁 ConfigSample — the provider chain and its precedence, running
Abstractions
Configuration.Abstractions
🌍 Language: English | Español
Core contracts for the DAF configuration system. This module defines interfaces and types only — no implementation. Reference it in libraries that need to read configuration without being bound to a specific provider.
What’s in this module
Two units, and which one declares a type is what goes in your uses:
| Unit | What it declares |
|---|---|
Daf.Extensions.Configuration |
the contracts below, TConfigurationPath, TConfigurationSourceOption and ConfigurationKeyName |
Daf.Configuration.Binder |
TConfigurationBinder and TBindOption |
Core interfaces
| Interface | Role |
|---|---|
IConfiguration |
Read/write key-value store with section navigation; Exists and Find answer presence |
IConfigurationSection |
A named subtree of IConfiguration — adds Key, Path, Value, HasChildren |
IConfigurationRoot |
Root of the tree — adds Reload and Providers |
IConfigurationBuilder |
Fluent builder: add sources, then call Build |
IConfigurationProvider |
Single source implementation: TryGet, Set, Load, GetChildKeys |
IConfigurationSource |
Factory for a provider: Build(Builder): IConfigurationProvider |
IConfigurationManager |
Builder and reader on one object: it is read while it is still being built |
Key access
Config['Database:Host'] // direct key
Config.GetSection('Database')['Host'] // via sectionKey separator is :. Sections expose GetChildren to iterate sub-keys.
Binding a subtree onto an object
uses Daf.Configuration.Binder;
var Settings := TConfigurationBinder.Bind<TAppSettings>(Config, 'AppSettings');Bind<T> constructs the instance and returns it, and the caller owns it. The overload taking an Instance: TObject fills one that already exists. TBindOption says what gets filled: boProperties, the default, or boFields.
Binding needs this package and nothing else: the binder works over any IConfiguration and knows nothing about JSON, INI or the environment. What it does with an absent key, nested objects, collections and a member it cannot convert is in the Configuration guide.
TConfigurationPath
Utility class for path manipulation:
TConfigurationPath.Combine(['Database', 'Host']) // 'Database:Host'
TConfigurationPath.GetSectionKey('Database:Host') // 'Host'
TConfigurationPath.GetParentPath('Database:Host') // 'Database'TConfigurationSourceOption
type
TConfigurationSourceOption = (csoOptional);
TConfigurationSourceOptions = set of TConfigurationSourceOption;What a source takes is the set, and csoOptional is the value that changes what happens: without it, a file that is not there aborts the load with File "<name>" not Found; with it, the source warns and contributes nothing.
ConfigurationKeyName
type
TDbSettings = class
private
FConnectionString: string;
public
[ConfigurationKeyName('connection-string')]
property ConnectionString: string read FConnectionString write FConnectionString;
end;The attribute names the key a member is read from, so a key a file writes as connection-string reaches a property Delphi cannot name that way. It replaces the member name: with the attribute there, a value under ConnectionString no longer answers for that member. On a member of object type it renames the whole section. It applies under boProperties and boFields alike, where .NET declares it for properties only.
Dependencies
Daf.Commons only, for the TNullable<T> that Find returns and the RTTI the binder walks types with.
The implementation is in Configuration.