-
Notifications
You must be signed in to change notification settings - Fork 0
Async Support
Dapplo.Ini ships with first-class async support. Every major I/O operation has an
*Async twin while the synchronous API remains fully functional for simple scenarios.
var config = await IniConfigRegistry.ForFile("appsettings.ini")
.AddSearchPath(AppContext.BaseDirectory)
.RegisterSection<IAppSettings>(new AppSettingsImpl())
.BuildAsync(cancellationToken);
var settings = config.GetSection<IAppSettings>();BuildAsync is the async counterpart of Build. It reads all INI layers
(AddDefaultsFile, user file, AddConstantsFile, external value sources), fires async
lifecycle hooks, and registers the IniConfig in the global registry — all without
blocking a thread.
var config = await IniConfigRegistry.ForFile("app.ini")
.AddSearchPath(AppContext.BaseDirectory)
.RegisterSection<IAppSettings>(new AppSettingsImpl())
.BuildAsync(cancellationToken);In dependency-injection scenarios you may need to register section objects (and the
IniConfig) as singletons before the INI file has been read.
BuildAsync handles this by registering the IniConfig in IniConfigRegistry and
exposing InitialLoadTask before any I/O starts. Consumers can await
InitialLoadTask to know when values are ready.
// Program.cs / Startup.cs
var section = new AppSettingsImpl();
// Start loading — do NOT await here; loading happens in the background
_ = IniConfigRegistry.ForFile("appsettings.ini")
.AddSearchPath(AppContext.BaseDirectory)
.RegisterSection<IAppSettings>(section)
.BuildAsync(); // fire-and-forget
// The IniConfig is already registered at this point (before I/O completes)
var iniConfig = IniConfigRegistry.Get("appsettings.ini");
// Register as singletons for injection — references are stable after loading
builder.Services.AddSingleton<IAppSettings>(section);
builder.Services.AddSingleton(iniConfig);// A consumer that needs values to be ready before proceeding
public class MyWorker
{
private readonly IAppSettings _settings;
private readonly IniConfig _config;
public MyWorker(IAppSettings settings, IniConfig config)
{
_settings = settings;
_config = config;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
// Wait for initial load before reading values
await _config.InitialLoadTask;
Console.WriteLine(_settings.AppName); // safe to read now
}
}Note:
InitialLoadTaskisTask.CompletedTaskafter a synchronousBuild(). Awaiting it in that case is a no-op and incurs no overhead.
// Reload all sections from disk without blocking the calling thread
await config.ReloadAsync(cancellationToken);
// The same Reloaded event fires after async reload too
config.Reloaded += (_, _) => Console.WriteLine("Reloaded!");ReloadAsync re-applies the full loading life-cycle (Loading-Life-Cycle) in place,
just like Reload(), but asynchronously. Section object references remain the same
(singleton guarantee is preserved).
When ReloadAsync is used, sections that implement IAfterLoadAsync have their
OnAfterLoadAsync method called. Sections that implement only the synchronous
IAfterLoad hook fall back to that automatically, so mixing sync and async sections in
the same config is supported.
await config.SaveAsync(cancellationToken);SaveAsync is the async counterpart of Save(). It respects the cancellation token
and prefers async lifecycle hooks (IBeforeSaveAsync, IAfterSaveAsync) before falling
back to the synchronous IBeforeSave / IAfterSave hooks.
Three async lifecycle interfaces parallel the existing synchronous ones:
| Interface | Trigger | Return type |
|---|---|---|
IAfterLoadAsync |
After BuildAsync() or ReloadAsync()
|
Task |
IBeforeSaveAsync |
Before writing to disk during SaveAsync()
|
Task<bool> — return false to cancel |
IAfterSaveAsync |
After a successful async write | Task |
These hooks are called only from the async code paths (BuildAsync, ReloadAsync,
SaveAsync). The synchronous Build(), Reload(), and Save() continue to call the
synchronous hooks only.
Add the async interface to your section interface, then implement the methods in a
partial class file:
// IMySettings.cs
[IniSection("App")]
public interface IMySettings : IIniSection, IAfterLoadAsync, IBeforeSaveAsync, IAfterSaveAsync
{
string? Value { get; set; }
}// MySettingsImpl.cs ← consumer-written partial alongside the generated MySettingsImpl.g.cs
public partial class MySettingsImpl
{
public async Task OnAfterLoadAsync(CancellationToken cancellationToken)
{
// e.g. decrypt a value fetched from a secrets vault
Value = await SecretsVault.DecryptAsync(Value, cancellationToken);
}
public async Task<bool> OnBeforeSaveAsync(CancellationToken cancellationToken)
{
// Validate remotely — return false to cancel the save
return await RemoteValidator.IsValidAsync(Value, cancellationToken);
}
public Task OnAfterSaveAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Settings saved!");
return Task.CompletedTask;
}
}When BuildAsync / ReloadAsync are used:
- If the section implements
IAfterLoadAsync→OnAfterLoadAsyncis called. - Otherwise, if the section implements
IAfterLoad→OnAfterLoadis called (sync fallback).
When SaveAsync is used:
- If the section implements
IBeforeSaveAsync→OnBeforeSaveAsyncis called. - Otherwise, if the section implements
IBeforeSave→OnBeforeSaveis called (sync fallback). - Same pattern applies to
IAfterSaveAsync/IAfterSave.
IValueSourceAsync is the async counterpart of IValueSource. Use it when values must
be fetched from an inherently asynchronous store such as a REST API, Azure App
Configuration, AWS Parameter Store, or any other networked service.
public interface IValueSourceAsync
{
Task<(bool Found, string? Value)> TryGetValueAsync(
string sectionName, string key, CancellationToken cancellationToken = default);
event EventHandler<ValueChangedEventArgs>? ValueChanged;
}Why a tuple instead of
out?outparameters are not permitted in async methods, so the return type is a(bool Found, string? Value)tuple.
public sealed class RemoteConfigSource : IValueSourceAsync
{
private readonly HttpClient _http;
public event EventHandler<ValueChangedEventArgs>? ValueChanged;
public RemoteConfigSource(HttpClient http) => _http = http;
public async Task<(bool Found, string? Value)> TryGetValueAsync(
string sectionName, string key, CancellationToken cancellationToken = default)
{
var response = await _http.GetAsync(
$"/config/{sectionName}/{key}", cancellationToken);
if (!response.IsSuccessStatusCode)
return (false, null);
var value = await response.Content.ReadAsStringAsync(cancellationToken);
return (true, value);
}
}var config = await IniConfigRegistry.ForFile("app.ini")
.AddSearchPath(AppContext.BaseDirectory)
.AddValueSource(new RemoteConfigSource(httpClient)) // IValueSourceAsync overload
.RegisterSection<IAppSettings>(new AppSettingsImpl())
.BuildAsync(cancellationToken);AddValueSource is overloaded to accept either IValueSource (sync) or
IValueSourceAsync. Async sources are applied after all synchronous sources, in
registration order.
Important: Async value sources are only consulted during
BuildAsync()andReloadAsync(). The synchronousBuild()andReload()skip async sources.
// Wire up a polling or push-notification mechanism:
remoteConfigSource.ValueChanged += async (_, _) =>
await config.ReloadAsync();When both sync and async sources are registered, values are applied in this order:
- Synchronous
IValueSourcesources (registration order) — applied first - Asynchronous
IValueSourceAsyncsources (registration order) — applied after sync
The last source to set a value wins, so async sources take precedence over sync ones when both target the same key.
All async APIs (Task-based) are available on .NET Framework 4.8 as well as .NET 10+.
The ValueTask-based generic static-virtual variants of the lifecycle hooks (IAfterLoad<TSelf>,
etc.) require at least .NET 7 / C# 11 and are not available on .NET Framework 4.8.
-
Loading-Configuration —
BuildAsync()builder method -
Reloading —
ReloadAsync()and the singleton guarantee -
Saving —
SaveAsync()and async save hooks - Lifecycle-Hooks — full lifecycle hook reference including async variants
-
External-Value-Sources —
IValueSourceandIValueSourceAsync -
Singleton-and-DI — using
InitialLoadTaskin DI scenarios - Registry-API — complete API reference including async members