Table of Contents

Bodu.Extensions.Configuration.Text - Core concepts

This page is the vocabulary the rest of the documentation assumes. Read it once before the getting-started samples, and refer back whenever a term feels imprecise.

Part of the Configuration topic.

For the high-level shape of the library, start with the introduction.

Source vs provider vs loader

The library follows the three-part contract every Microsoft.Extensions.Configuration provider uses:

Role Type Responsibility
Source TextConfigurationSource, TextStreamConfigurationSource Holds the configuration of "what to load and how" - the path, target path, parse and resolve options, reload behaviour. Implements Build(IConfigurationBuilder).
Provider TextConfigurationProvider, TextStreamConfigurationProvider Performs the actual load. Subclasses FileConfigurationProvider / StreamConfigurationProvider; inherits change-token plumbing. Populates the inherited Data dictionary.
Loader Internal (not public) Internal helper that parses a stream into a ConfigurationDocument, resolves it, and flattens the view into Dictionary<string, string?> for the provider.

Both providers share the same loader, so behaviour stays in lockstep across file and stream backings.

File vs stream source

The two source types correspond to the two file shapes Microsoft ships:

Source Backing Reload-on-change
TextConfigurationSource Path resolved through an IFileProvider Yes
TextStreamConfigurationSource Arbitrary System.IO.Stream No

File sources are the common case - they layer naturally with appsettings.json, support hot reload, and accept the same path-based options every provider does. Stream sources are useful for tests, embedded resources, or in-memory fixtures where the configuration data does not live on disk.

A third source, TomlConfigurationSource, backs the read-only TOML bridge (AddTomlFile / AddTomlStream). It carries Path, Optional, and Stream but no TargetPath, ParseOptions, ResolveOptions, or ReloadOnChange - TOML has no glob-anchored resolution layer and the bridge is read-once. A fourth, BencodeConfigurationSource, backs the read-only Bencode bridge (AddBencodeFile / AddBencodeStream) with the same three-property shape; its documents must be dictionary-rooted.

Target path

TargetPath is the value the source hands to ConfigurationDocument.Resolve(targetPath). It anchors glob matching for sections whose headers contain path separators.

builder.AddTextConfigurationFile(
    "team.boduconfig",
    targetPath: "src/MyApp/Program.cs");

When the file says

[src/**/*.cs]
logging.level.default = Warning

the [src/**] pattern matches src/MyApp/Program.cs, so the resolved view picks up that section's properties on top of any earlier matches. When TargetPath is null, only unanchored patterns and preamble values contribute.

TargetPath is per source - different sources in the same builder can have different anchors. If your application needs configuration evaluated for several paths in parallel, add several sources with the same Path but different TargetPath values.

Parse and resolve option propagation

A source carries two optional bags:

Source property Drives
ParseOptions The ConfigurationParseOptions passed to the reader. Controls inline-comment mode, duplicate handling, diagnostic mode, length limits.
ResolveOptions The ConfigurationResolveOptions passed to the resolver. Controls PathRoot, MissingPathRootMode, UnsetValueMode, PathComparison.

Both default to null, in which case the library uses Bodu and Bodu. Set them per-source when one file in a builder needs different semantics - for example, an EditorConfig-strict file alongside a Bodu-permissive one.

builder.AddTextConfigurationFile(src =>
{
    src.Path = ".editorconfig";
    src.ParseOptions   = ConfigurationParseOptions.EditorConfigCompatible;
    src.ResolveOptions = ConfigurationResolveOptions.EditorConfigCompatible;
    src.TargetPath     = "src/Foo.cs";
});

Conventional file probe

The no-argument overload of AddTextConfiguration is a convenience helper for the common "drop a file in the project root" pattern:

builder.AddTextConfiguration(optional: true, reloadOnChange: true);

The probe runs against the builder's default file provider and looks for two file names in order:

  1. .boduconfig - the dotfile form, common in version-control-friendly repos.
  2. bodu.config - the plain form, common on Windows where dotfiles need explicit attribute toggles.

The first file found is added; if neither is present and optional is true, the helper returns the builder unchanged (it registers a .boduconfig source marked optional so the slot still exists). When neither is present and optional is false, the helper throws FileNotFoundException. The probe consults IFileProvider.GetFileInfo(name).Exists and runs once, at registration time.

Note

A default PhysicalFileProvider filters dot-prefixed files via its ExclusionFilters (Sensitive by default), so .boduconfig will not resolve unless you register a PhysicalFileProvider constructed with ExclusionFilters.None. The plain bodu.config fallback is resolvable without that change.

Reload-on-change

FileConfigurationSource.ReloadOnChange is inherited as-is. When true, the provider:

  1. Attaches a file watcher via the source's IFileProvider.
  2. On a change notification, reparses the file and rebuilds the resolved view.
  3. Triggers the standard IConfiguration reload tokens, so callers using IOptionsMonitor<TOptions> re-bind to the new values automatically.

Reload is not atomic with respect to multiple providers - if your builder has three file sources and two change at once, the providers reload independently, in the order their watchers fire. This matches the MEC contract and is not specific to Bodu.

The stream source does not support reload. The stream is parsed once when Build is called; the stream's lifetime ends with that parse. If you need dynamic stream-backed inputs, rebuild the configuration.

File provider precedence

AddTextConfigurationFile resolves the IFileProvider in the standard MEC order:

  1. If a provider is supplied directly to the overload, use it.
  2. Otherwise, if the source's FileProvider is set, use that.
  3. Otherwise, defer to the builder's default file provider - typically a PhysicalFileProvider rooted at Directory.GetCurrentDirectory().

Tests typically supply a PhysicalFileProvider rooted at a temp directory; production code typically relies on the builder default.

Options binding

ConfigurationOptionsExtensions wraps the standard services.Configure<TOptions>(...) shape:

services.AddConfigurationOptions<ServiceOptions>(configuration, "service");
// equivalent to:
services.Configure<ServiceOptions>(configuration.GetSection("service"));

The wrapper exists for discoverability - call sites that reach for an AddTextConfiguration* API by IntelliSense find an options helper with the same prefix. The shape is identical to the MEC version; callers who already use Configure<T> are not penalised, and callers who switch to AddConfigurationOptions are not locked in.

The two overloads:

Overload Use when
AddConfigurationOptions<T>(services, configuration, sectionName) Section is identified by colon-delimited name in an IConfiguration root.
AddConfigurationOptions<T>(services, section) Section is already projected via configuration.GetSection(...).

Both throw ArgumentNullException for null services / configuration / section; the name-based overload throws ArgumentException for an empty or whitespace section name.

Colon-delimited key model

IConfiguration keys are colon-delimited by convention - service:name, logging:level:default. Bodu's reader projects raw keys to the same shape under the default DotToColon mapping, so a file written as

service.name = Bodu
service.port = 8080
logging.level.default = Information

surfaces as configuration["service:name"], configuration["service:port"], configuration["logging:level:default"]. The mapping is the Mapping property on ConfigurationKeyOptions; switching it to Identity emits keys unchanged - useful when the file is also consumed by tools that interpret dots as path separators (e.g. AppSettings patches).

The projection is lossy by design. The flatten step in the providers' shared internal loader keeps only the resolved key/value pairs; comments, source locations, and duplicate-section provenance present on the parsed ConfigurationDocument do not survive. Keys are stored case-insensitively (StringComparer.OrdinalIgnoreCase), and a literal colon inside a key segment is interpreted as a hierarchy boundary once flattened. Reach for ConfigurationDocument directly when you need any of that discarded metadata.

Pre-parsed document source

AddTextConfigurationDocument takes an already-parsed IniDocumentBase - such as a ConfigurationDocument - resolves it once against targetPath, and adds the flattened pairs via the in-memory provider. It is a one-shot snapshot: the document is captured by value when the method is called, so later edits to the document (or its backing file) are not reflected. Use it to share a single parse across several builders, or to feed a document built or mutated in code. There is no reload-on-change for this overload.

Where to go next