Using INI
Bodu.Text.Ini reads and writes sectioned [name] / key=value configuration files. The value model is a two-level object-of-objects: global keys (before the first section header) hoist onto the root, and each section is a nested object of string values.
Pattern 1 - query a document
using Bodu.Text.Ini;
using Bodu.Text.Ini.Document;
using IniDocument document = IniDocument.Parse(File.ReadAllBytes("app.ini"));
IniElement root = document.RootElement;
string environment = root.GetProperty("environment").GetString(); // global key
IniElement server = root.GetProperty("server"); // [server] section
string host = server.GetProperty("host").GetString();
foreach (IniProperty property in root.EnumerateObject())
{
// Globals surface first (String kind), then sections (Object kind).
Console.WriteLine($"{property.Name}: {property.Value.ValueKind}");
}
Pattern 2 - typed binding via the serializer
Scalar members bind global keys; object-shaped members (section POCOs or Dictionary<string, string>) bind sections:
using Bodu.Text.Serialization;
sealed class AppConfig
{
public string? Environment { get; set; } // global key
public ServerSection? Server { get; set; } // [server]
public Dictionary<string, string>? Logging { get; set; } // [logging]
}
sealed class ServerSection
{
public string? Host { get; set; }
public int Port { get; set; }
}
AppConfig config = IniSerializer.Deserialize<AppConfig>(
iniText, new IniSerializerOptions { PropertyNamingPolicy = NamingPolicy.SnakeCaseLower });
Dictionary<string, Dictionary<string, string>> works as an all-sections root. A member nested beyond INI's two levels throws IniSerializationException.
The global section name
Hoisting global keys onto the root is ergonomic but ambiguous when a global key and a section share a name (always rejected), and impossible for a nested-dictionary root. IniSerializerOptions.GlobalSectionName routes the global entries to and from a reserved root key instead:
var options = new IniSerializerOptions { GlobalSectionName = "global" };
var all = IniSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(iniText, options);
string env = all["global"]["environment"];
Reflection-free binding
Annotate a partial section type with [IniSection] and reference the Bodu.Text.Formats.Generators source generator, and a static IniFactory property (IIniSectionFactory<ServerSection>) is emitted at compile time. The section overloads - IniSerializer.SerializeSection("server", section, ServerSection.IniFactory) / DeserializeSection(iniText, "server", ServerSection.IniFactory) - bind one section through the factory instead of the reflection binder, making the path trimming- and AOT-safe. An empty section name addresses the document's global keys; duplicate-section merge still applies before binding.
Pattern 3 - comment-preserving edits with the mutable DOM
using Bodu.Text.Ini.Nodes;
IniObject root = IniNode.Parse(File.ReadAllBytes("app.ini"));
root["server"].AsObject()["port"].AsValue().Value = "9090"; // edit in place, trivia kept
var metrics = new IniObject();
var enabled = new IniValue("true");
enabled.LeadingComments.Add(" scrape target"); // authored comment
metrics["enabled"] = enabled;
root["metrics"] = metrics;
File.WriteAllBytes("app.ini", root.ToUtf8Bytes());
Every comment line from the source survives (LeadingComments on sections and values, TrailingComments per object). Layout is canonicalized: key=value without padding, and global entries always precede the first section header. Inline comments are not modeled - the dialect keeps everything after = as value content.
Pattern 4 - duplicate policies
Duplicates are resolved when the document is materialized, controlled by IniDocumentOptions (also on IniSerializerOptions):
using Bodu.Text.Ini.Reader;
var strict = new IniDocumentOptions
{
DuplicateSectionBehavior = IniDuplicateSectionBehavior.Disallowed,
DuplicateKeyBehavior = IniDuplicateKeyBehavior.Disallowed,
};
using IniDocument document = IniDocument.Parse(bytes, IniReaderOptions.Default, strict);
The defaults merge repeated sections and keep the last duplicate key - the permissive Windows-profile reading. IniSerializerDefaults.Strict selects Disallowed for both - Python configparser strict mode.
The two readers
Utf8IniReader streams the file as authored (section headers, keys, values, comments, in source order). The normalized IniDocumentReader pre-parses the whole document - duplicate-section merge declares structure out of source order - and emits the logical object-of-objects token stream that the serializer and read-only DOM consume.
Dialect
= only (no :), values literal to end of line (quotes preserved, inline ;/# kept as content), ; and # full-line comments, BOM skipped, LF/CRLF/CR equivalent. See Parser policies.
Exceptions
IniFormatException for malformed input and duplicate-policy violations (line/offset attached); IniSerializationException for binding failures (non-object root, depth beyond two levels, missing [Required] member, non-convertible value).
When to reach for Bodu.Text.Configuration instead
When you need EditorConfig-style behaviour - glob-targeted sections, layered resolution, typed views with diagnostics - use Bodu.Text.Configuration. It carries its own INI document model and does not depend on this package.