Migration Settings
Key Takeaways
  • Properties.Settings.Default depends on System.Configuration, which is designed around the Windows desktop settings provider model
  • Modern .NET uses appsettings.json, and Microsoft directs Framework-to-.NET ports toward that format
  • Uno.Extensions.Configuration exposes both IOptions<T> and IWritableOptions<T> when you register a section with Section<T>()
  • IWritableOptions<T>.UpdateAsync() takes the current snapshot and returns a new instance; immutable records are the recommended shape
  • The configuration section does not need to exist in the file ahead of time; Uno creates it the first time UpdateAsync() runs

The WPF Settings.Default migration path in Uno Platform is to stop calling Properties.Settings.Default.Save() and instead model a settings record, register it with Section<T>() on the host builder, and mutate it through IWritableOptions<T>.UpdateAsync(). appsettings.json holds your defaults, and Uno.Extensions.Configuration handles persistence across iOS, Android, WebAssembly, and desktop.

Why It Doesn't Port

Why Properties.Settings.Default Doesn't Cross the Platform Boundary

The Properties.Settings.Default API is a thin wrapper over ApplicationSettingsBase. Under that class sits a settings provider. If no provider is specified, LocalFileSettingsProvider is used, which stores application-scoped values in app.exe.config and user-scoped values in a separate user.config file under %LOCALAPPDATA%. The path conventions are Windows-specific.

Microsoft's own guidance for porting .NET Framework apps to modern .NET is explicit: ".NET Framework uses app.config to load settings; modern .NET uses appsettings.json." For a cross-platform Uno Platform app that also targets iOS, Android, and WebAssembly, appsettings.json plus Microsoft.Extensions.Options is the portable replacement.

The Model

The Uno Platform Settings Model

Uno.Extensions.Configuration provides a uniform way to read or write configuration data and uses Microsoft.Extensions.Configuration under the hood. You opt into configuration on the host builder via UseConfiguration(), and declare an appsettings.json source with EmbeddedSource<App>():

App.xaml.cs
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
    var builder = this.CreateBuilder(args)
        .Configure(host =>
        {
            host.UseConfiguration(config =>
                config.EmbeddedSource<App>()
                      .Section<AppConfig>());
        });
}
EmbeddedSource vs. ContentSource

EmbeddedSource<App>() is recommended over ContentSource<App>() because configuration data read from embedded resources is available to the application immediately upon startup, particularly on WebAssembly.

Section<T>() is what makes settings writable: it ensures both IOptions<AppConfig> and IWritableOptions<AppConfig> are available from the container. Model the section as an immutable record:

AppConfig.cs
public record AppConfig
{
    public double? WindowWidth { get; init; }
    public string? Theme { get; init; }
    public string? LastFilePath { get; init; }
}
appsettings.json
{
  "AppConfig": {
    "WindowWidth": 1200,
    "Theme": "Light",
    "LastFilePath": null
  }
}
UpdateAsync

Writing at Runtime with IWritableOptions<T>.UpdateAsync

A view model resolves IWritableOptions<T> through constructor injection. UpdateAsync() receives the current snapshot and expects you to return a new instance with the modifications you want to persist:

SettingsViewModel.cs
public class SettingsViewModel
{
    private readonly IWritableOptions<AppConfig> _settings;

    public SettingsViewModel(IWritableOptions<AppConfig> settings)
        => _settings = settings;

    public async Task SaveWindowWidthAsync(double width) =>
        await _settings.UpdateAsync(current => current with
        {
            WindowWidth = width
        });
}
  • No pre-seed required. If the selected section does not exist in any backing store, Uno automatically creates it the first time UpdateAsync runs.
  • Snapshot semantics. IOptionsSnapshot<T> is a scoped service where options are computed once per request when accessed and cached for the lifetime of the request.
Before / After

Migrating a Typical Settings.Default Surface

WPF (Before)
Properties.Settings.Default.WindowWidth = 1200;
Properties.Settings.Default.Theme = "Dark";
Properties.Settings.Default.LastFilePath = @"C:\work\notes.txt";
Properties.Settings.Default.Save();
Uno Platform (After)
await _settings.UpdateAsync(current => current with
{
    WindowWidth = 1200,
    Theme = "Dark",
    LastFilePath = "/Users/me/notes.txt"
});

Reads become IOptions<AppConfig> or IOptionsSnapshot<AppConfig> injection, depending on whether you want values frozen at construction or refreshed per scope.

Per-platform storage: Do not hard-code paths the way a WPF project might when "upgrading" user.config between versions. Treat IWritableOptions<T>.UpdateAsync() as the only supported surface for mutation and let the extension decide where bytes land.

Checklist

Migration Checklist

  1. Inventory every Properties.Settings.Default.* access. Only user-scoped values map cleanly to IWritableOptions<T>. Application-scoped static values are a better fit for read-only IOptions<T>.
  2. Replace the designer file with a record. The Settings.settings designer-generated wrapper class disappears; the record you define is the new typed surface.
  3. Register the section. Add Configuration to <UnoFeatures>, call UseConfiguration(), and register every section with Section<T>(). Without it, IWritableOptions<T> will not be in the DI container.
  4. Replace static access with DI. Settings.Default.X is a static property on a singleton wrapper; IWritableOptions<T> is a dependency that must be injected.
  5. Pick your read API. IOptions<T> is a singleton that does not reread the file after startup; IOptionsSnapshot<T> rebuilds per scope and sees updated values.
  6. Move defaults into appsettings.json. Microsoft directs .NET Framework-to-modern-.NET ports to appsettings.json for app settings.
Read API

IOptions vs. IOptionsSnapshot vs. IOptionsMonitor

InterfaceLifetimeSees UpdateAsync changes?
IOptions<T>SingletonNo (frozen at startup)
IOptionsSnapshot<T>ScopedYes (recomputed per scope)
IOptionsMonitor<T>SingletonYes (exposes change notifications)

Choose based on whether the calling code needs to observe UpdateAsync results mid-session. IOptionsMonitor<T> covers the Settings.Default.Reload() use case.

Binding

Binding a Control to a Writable Option

XAML
<Slider Minimum="800" Maximum="2400"
        Value="{x:Bind ViewModel.WindowWidth, Mode=TwoWay}" />
ViewModel
public sealed class MainViewModel : INotifyPropertyChanged
{
    private readonly IWritableOptions<AppConfig> _options;

    public MainViewModel(IWritableOptions<AppConfig> options)
        => _options = options;

    public double WindowWidth
    {
        get => _options.Value.WindowWidth ?? 1200;
        set => _ = _options.UpdateAsync(
            c => c with { WindowWidth = value });
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

x:Bind default mode: {x:Bind} defaults to OneTime, so use Mode=TwoWay (or OneWay) whenever the UI must track UpdateAsync results.

FAQ

FAQ

Do I need both appsettings.json and IWritableOptions?

appsettings.json holds your defaults and the sections you load with EmbeddedSource<App>(). IWritableOptions<T> is what makes a given section writable at runtime. A section registered with Section<T>() does not have to exist in the file to start with; Uno creates it on first UpdateAsync().

How do I preserve existing user.config values on first run?

The practical pattern is to read the legacy file on first launch using System.Configuration APIs on Windows, then write each value through IWritableOptions<T>.UpdateAsync() before ignoring the legacy file. Because UpdateAsync() creates sections on demand, you do not need to pre-populate appsettings.json.

Does IWritableOptions work on WebAssembly?

Yes. Uno.Extensions.Configuration explicitly supports WebAssembly, and EmbeddedSource<App>() is the recommended approach because embedded data is available immediately on startup.

What happens to Settings.Default.Reload() and Upgrade()?

Reload() semantics are covered by IOptionsMonitor<T> (singleton with change notifications) and IOptionsSnapshot<T> (scoped, recomputed per request). Upgrade() is a cross-version migration primitive tied to LocalFileSettingsProvider; there is no built-in Uno equivalent. Handle version upgrades explicitly in startup code.

Next Step

Follow the IWritableOptions walkthrough to persist your settings. If you are driving the migration with AI, pair it with the Claude Code integration guide and the Uno MCP servers so the agent can look up IWritableOptions<T> and related APIs while it refactors.