-
Notifications
You must be signed in to change notification settings - Fork 0
Migration
INI files evolve over time. Dapplo.Ini provides two complementary mechanisms to handle schema changes without losing user data:
- Unknown-key callbacks — surface unrecognised keys so you can copy old values to new properties.
-
The
[__metadata__]section — persist file-level metadata (version, application name, timestamp) soIAfterLoadhooks can apply version-gated upgrade steps.
A key is unknown when it exists in the INI file but has no matching property on the registered section interface. This happens when a property is renamed or removed.
Implement IUnknownKey<TSelf> directly on the section interface using the same
static-virtual pattern as IAfterLoad<TSelf>:
[IniSection("App")]
public interface IAppSettings : IIniSection, IUnknownKey<IAppSettings>
{
// New name for what used to be called "OldTimeout"
[IniValue(DefaultValue = "30")]
int Timeout { get; set; }
static void OnUnknownKey(IAppSettings self, string key, string? value)
{
if (key.Equals("OldTimeout", StringComparison.OrdinalIgnoreCase))
self.Timeout = int.TryParse(value, out var t) ? t : 30;
}
}The source generator detects IUnknownKey<TSelf> and emits a bridge in the generated class
so the framework calls the static hook at runtime.
Add IUnknownKey to the interface declaration and implement OnUnknownKey in a partial class:
// IAppSettings.cs
[IniSection("App")]
public interface IAppSettings : IIniSection, IUnknownKey
{
int Timeout { get; set; }
}// AppSettingsImpl.cs (sits alongside the generated AppSettingsImpl.g.cs)
public partial class AppSettingsImpl
{
public void OnUnknownKey(string key, string? value)
{
if (key.Equals("OldTimeout", StringComparison.OrdinalIgnoreCase))
Timeout = int.TryParse(value, out var t) ? t : 30;
}
}When the migration logic is central (e.g. a logger or a shared migration service), register a callback at the builder level instead:
IniConfigRegistry.ForFile("appsettings.ini")
.AddSearchPath(AppContext.BaseDirectory)
.RegisterSection<IAppSettings>(new AppSettingsImpl())
.OnUnknownKey((sectionName, key, value) =>
{
// sectionName lets you scope the handler to a particular section
if (sectionName == "App" && key == "OldTimeout")
{
var settings = config.GetSection<IAppSettings>();
settings.Timeout = int.TryParse(value, out var t) ? t : 30;
}
})
.Build();Renaming a property changes the key name in the INI file. Existing user files still contain the old key name. Use an unknown-key hook to copy the value:
Old interface:
[App]
OldTimeout = 60New interface:
[IniSection("App")]
public interface IAppSettings : IIniSection, IUnknownKey<IAppSettings>
{
[IniValue(DefaultValue = "30")]
int Timeout { get; set; } // renamed from OldTimeout
static void OnUnknownKey(IAppSettings self, string key, string? value)
{
if (key.Equals("OldTimeout", StringComparison.OrdinalIgnoreCase))
self.Timeout = int.TryParse(value, out var t) ? t : 30;
}
}When the next Save() occurs the old key is gone and Timeout is written with the migrated value.
Changing a property's type (e.g. string → int, or int → enum) requires parsing the
stored string value inside the unknown-key hook or inside IAfterLoad.
Example — string → LogLevel enum:
[IniSection("Logging")]
public interface ILoggingSettings : IIniSection, IUnknownKey<ILoggingSettings>
{
[IniValue(DefaultValue = "Information")]
LogLevel Level { get; set; }
// Old file had: LogLevelString = Warning
static void OnUnknownKey(ILoggingSettings self, string key, string? value)
{
if (key.Equals("LogLevelString", StringComparison.OrdinalIgnoreCase)
&& Enum.TryParse<LogLevel>(value, ignoreCase: true, out var parsed))
{
self.Level = parsed;
}
}
}Use IAfterLoad<TSelf> to clamp, sanitise, or substitute invalid values after loading:
[IniSection("Server")]
public interface IServerSettings : IIniSection, IAfterLoad<IServerSettings>
{
[IniValue(DefaultValue = "8080")]
int Port { get; set; }
static void OnAfterLoad(IServerSettings self)
{
// Clamp port to valid range; silently correct invalid user edits
if (self.Port is < 1 or > 65535)
self.Port = 8080;
}
}Validation that should surface errors to the UI instead belongs in IDataValidation<TSelf> —
see Validation.
When you need to know which version of the application wrote the INI file you can opt in to
the metadata section. On every Save() the framework prepends a [__metadata__] section to
the file:
[__metadata__]
Version = 1.2.0
CommitHash = abc1234def5678
CreatedBy = Greenshot
SavedOn = 2026-03-12T07:43:32+01:00SavedOn is the local save time in ISO 8601 with the UTC offset (culture-independent), so it
can be parsed with DateTimeOffset.Parse(value, CultureInfo.InvariantCulture). Files written by
earlier versions contain a locale-formatted time; IniMetadata.SavedOn keeps the raw string.
CommitHash is only written when it can be determined (see below).
When version is not supplied to EnableMetadata(), the framework resolves it from the
entry assembly using the following priority:
-
AssemblyInformationalVersionAttribute.InformationalVersion— the preferred source. Build tools such as Nerdbank.GitVersioning andMinVerset this to a string like"1.2.0+abc1234def5678". The framework splits on+and stores the left part asVersionand the right part asCommitHash. -
AssemblyName.Version— used as a plain-text fallback when the informational version attribute is absent or empty.
Call EnableMetadata() on the builder. Both parameters are optional; the framework falls
back to Assembly.GetEntryAssembly() when they are not supplied:
IniConfigRegistry.ForFile("appsettings.ini")
.AddSearchPath(AppContext.BaseDirectory)
.RegisterSection<IAppSettings>(new AppSettingsImpl())
.EnableMetadata(version: "1.2.0", applicationName: "Greenshot")
.Build();After Build() or Reload() completes, IniConfig.Metadata holds the values read from the
file (or null when the section was absent — for example on first run or when a user removed it):
[IniSection("App")]
public interface IAppSettings : IIniSection, IAfterLoad<IAppSettings>
{
string? DisplayName { get; set; }
static void OnAfterLoad(IAppSettings self)
{
var config = IniConfigRegistry.Get("appsettings.ini");
var meta = config.Metadata;
// meta is null when the file has no [__metadata__] section yet (first run).
if (meta is null) return;
// meta.Version holds the SemVer string (before '+' in InformationalVersion).
// meta.CommitHash holds the source-control hash (after '+'), or null when absent.
var stored = Version.TryParse(meta.Version, out var v) ? v : new Version(0, 0);
var current = typeof(IAppSettings).Assembly.GetName().Version!;
if (stored < new Version(1, 2, 0) && current >= new Version(1, 2, 0))
{
// Upgrade step: apply default for a newly added property
self.DisplayName ??= "Migrated App";
}
}
}| Property | INI key | Description |
|---|---|---|
Version |
Version |
SemVer string (the portion of InformationalVersion before +). |
CommitHash |
CommitHash |
Source-control commit hash (the portion after +); null when absent. |
ApplicationName |
CreatedBy |
Entry assembly name or the applicationName argument. |
SavedOn |
SavedOn |
Save time as written, ISO 8601 with UTC offset (e.g. 2026-09-30T11:18:00+02:00). |
The [__metadata__] section is always written as the first section in the file, so its
values are available when IAfterLoad hooks run — even for properties declared before
other sections.
Both features can be active at the same time. Keys from [__metadata__] are never
forwarded to OnUnknownKey callbacks; only keys from registered application sections trigger them:
IniConfigRegistry.ForFile("appsettings.ini")
.AddSearchPath(AppContext.BaseDirectory)
.RegisterSection<IAppSettings>(new AppSettingsImpl())
.EnableMetadata(version: "2.0.0", applicationName: "MyApp")
.OnUnknownKey((section, key, value) =>
logger.Warning("Unknown key {Key} in [{Section}]", key, section))
.Build();| Scenario | Recommended approach |
|---|---|
| Rename a key |
IUnknownKey<TSelf> or IUnknownKey partial class |
| Remove a key |
IUnknownKey<TSelf> (no-op handler silences it) |
| Change a type |
IUnknownKey<TSelf> with type conversion in the hook |
| Clamp / sanitise a value | IAfterLoad<TSelf> |
| Version-gated step (e.g. set a new default) |
EnableMetadata + IAfterLoad<TSelf>
|
| Central logging of unrecognised keys |
OnUnknownKey(callback) on the builder |
-
Lifecycle-Hooks —
IAfterLoad,IBeforeSave,IAfterSaveand async variants -
Validation —
IDataValidation<TSelf>andINotifyDataErrorInfo -
Registry-API —
EnableMetadata,OnUnknownKey, andIniConfig.Metadatareference