Repository navigation
Saving
// Saves the user values back to the file that was loaded
// (or the first writable search path when no existing file was found).
config.Save();
// Async variant — does not block the calling thread:
await config.SaveAsync(cancellationToken);Note: Own
Save()/SaveAsync()calls are automatically detected and never trigger the file-change monitor, so a save does not cause an unwanted reload loop.
Only user values are written: values read from the user file or set by the application after the
load. Inherited values (compiled defaults, defaults files, value sources) are written as commented-out
lines (; Key = value), constants are never written. See User-Values for the details, the
WriteInheritedValuesAsComments(false) and SaveAllValues() options and the migration of files that
were written by Dapplo.Ini 1.x.
Saving is atomic: the content is written to a temporary file next to the INI file
(<name>.ini.<guid>.tmp), flushed to disk and then swapped in with File.Replace (retried a
few times when a virus scanner or editor briefly holds the file). A crash or a full disk
during the save never leaves a truncated or empty INI file behind.
The content is streamed into the file: it is encoded and written in blocks of 8 K characters while it is
produced, with pooled buffers, so a save neither creates the whole file as a string nor as a byte array.
SaveAsync writes the blocks asynchronously.
The file is written in place instead when it is a symbolic link (replacing it would turn the link into a plain file) or when no temporary file can be created in its folder (for example when only the file itself is writable).
Other details:
- A value that contains a line break is always written with escape sequences (
\n,\r), because a raw line break could inject keys or sections into the file. EnableEscapeSequenceson the parser (Parser-Options) to read it back as a line break. - A multi-line
Descriptionis written with;in front of every line. - By default only registered sections are written. Use
PreserveUnknownSections()to keep sections of plugins that are not loaded — see Plugin-Registrations#keeping-the-settings-of-plugins-that-are-not-loaded. - Comments are regenerated from
[Description]s and blank lines are not kept. UsePreserveFormatting()(which impliesPreserveUnknownSections()) to write blank lines and comments back exactly as they were in the file.
Load, Reload, Save and AddSection are serialised by one lifecycle gate:
| Situation | Behaviour |
|---|---|
Save() while another load, reload or save is running |
Waits for it to finish, then saves the latest values |
Save() from inside a save (e.g. an IBeforeSave hook) |
Returns immediately |
Save() from a load/reload hook or a listener |
Runs directly, as part of that operation |
| Auto-save tick while another operation is running | The tick is skipped; the next tick tries again |
Caution: do not block the UI thread on
Save()while an async hook (IBeforeSaveAsync/IAfterSaveAsync) awaits that same UI thread — this sync-over-async pattern deadlocks. Useawait SaveAsync()in UI code.
Use IniWriterOptions (file-level) or convenience methods on IniConfigBuilder:
using var config = IniConfigRegistry.ForFile("app.ini")
.AddSearchPath(AppContext.BaseDirectory)
.WithWriterOptions(new IniWriterOptions
{
AssignmentSeparator = " = ",
QuoteStyle = IniValueQuoteStyle.Double,
EscapeSequences = true,
WriteComments = false
})
.RegisterSection<IAppSettings>(new AppSettingsImpl())
.Build();Convenience methods:
AssignmentSeparator(...)EnableEscapeSequencesOnWrite()QuoteValuesOnWrite(...)SkipCommentsOnWrite()-
WriteInheritedValuesAsComments(bool)— inherited keys as; Key = valuelines (default) or not at all -
SaveAllValues()— write every value, as Dapplo.Ini 1.x did (constants are still never written)
IniConfig always writes with its writer options (default separator =). When you use the
low-level IniFileWriter directly without options, it keeps the AssignmentSeparator of the
IniFile; explicit IniWriterOptions always win.
You can also override write behavior per section/property via attributes:
[IniSection("Server", QuoteValues = IniValueQuoteStyle.Double, WriteComments = IniBooleanOption.Disabled)]
public interface IServerSettings : IIniSection
{
[IniValue(EscapeSequences = IniBooleanOption.Enabled)]
string? Path { get; set; }
}Implement IBeforeSave<TSelf> (or the non-generic IBeforeSave) to run logic before
the file is written. Returning false cancels the save.
[IniSection("Server")]
public interface IServerSettings
: IIniSection,
IBeforeSave<IServerSettings>
{
[IniValue(DefaultValue = "8080")]
int Port { get; set; }
/// <summary>Validate before saving. Return false to abort.</summary>
static new bool OnBeforeSave(IServerSettings self)
=> self.Port is >= 1 and <= 65535;
}Implement IAfterSave<TSelf> (or the non-generic IAfterSave) to run logic after a
successful write.
[IniSection("Server")]
public interface IServerSettings
: IIniSection,
IAfterSave<IServerSettings>
{
[IniValue(DefaultValue = "8080")]
int Port { get; set; }
/// <summary>Notify other components after a successful save.</summary>
static new void OnAfterSave(IServerSettings self)
=> Console.WriteLine($"Server settings saved — port {self.Port}");
}Use HasPendingChanges() to avoid writing to disk when nothing has changed:
if (config.HasPendingChanges())
config.Save();See Reloading#change-tracking for details on the dirty flag.
See Loading-Configuration#auto-save-on-a-timer for the AutoSaveInterval builder method.
-
Lifecycle-Hooks — full
IAfterLoad,IBeforeSave,IAfterSavedocumentation including async variants -
Loading-Configuration#save-on-process-exit —
SaveOnExit()builder method -
Reloading —
Reload()/ReloadAsync()andHasPendingChanges() -
Async-Support —
SaveAsync()and async save hooks