Configuration.Writable 0.1.0-alpha.100

This is a prerelease version of Configuration.Writable.
There is a newer version of this package available.
See the version list below for details.
dotnet add package Configuration.Writable --version 0.1.0-alpha.100
                    
NuGet\Install-Package Configuration.Writable -Version 0.1.0-alpha.100
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Configuration.Writable" Version="0.1.0-alpha.100" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Configuration.Writable" Version="0.1.0-alpha.100" />
                    
Directory.Packages.props
<PackageReference Include="Configuration.Writable" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Configuration.Writable --version 0.1.0-alpha.100
                    
#r "nuget: Configuration.Writable, 0.1.0-alpha.100"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Configuration.Writable@0.1.0-alpha.100
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Configuration.Writable&version=0.1.0-alpha.100&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Configuration.Writable&version=0.1.0-alpha.100&prerelease
                    
Install as a Cake Tool

Configuration.Writable

NuGet Version GitHub Actions Workflow Status GitHub last commit (branch)

A lightweight library that allows for easy saving and referencing of settings, with extensive customization options.

Features

  • Read and write user settings with type safety.
  • Built-in: Atomic file writing, automatic retry, and backup creation.
  • Extends Microsoft.Extensions.Options interfaces, so it works seamlessly with existing code using IOptions<T>, IOptionsMonitor<T>, etc.
  • Simple API that can be easily used in applications both with and without DI.
  • Supports various file formats (Json, Xml, Yaml, Encrypted, etc...) via providers.

See more...

Usage

Setup

Install Configuration.Writable from NuGet.

dotnet add package Configuration.Writable

Then, prepare a class (UserSetting) in advance that you want to read and write as settings.

public class UserSetting
{
    public string Name { get; set; } = "default name";
    public int Age { get; set; } = 20;
}

Without DI

If you are not using DI (for example, in WinForms, WPF, console apps, etc.), Use WritableConfig as the starting point for reading and writing settings.

using Configuration.Writable;

// initialize once (at application startup)
WritableConfig.Initialize<SampleSetting>();

// -------------
// get the writable config instance with the specified setting class
var options = WritableConfig.GetOptions<SampleSetting>();

// get the UserSetting instance
var sampleSetting = options.CurrentValue;
Console.WriteLine($">> Name: {sampleSetting.Name}");

// and save to storage
await options.SaveAsync(setting =>
{
    setting.Name = "new name";
});
// By default, it's saved to ./usersettings.json

With DI

If you are using DI (for example, in ASP.NET Core, Blazor, Worker Service, etc.), register IReadOnlyOptions<T> and IWritableOptions<T> in the DI container. First, call AddWritableOptions<T> to register the settings class.

// Program.cs
builder.Services.AddWritableOptions<UserSetting>();

Then, inject IReadOnlyOptions<T> or IWritableOptions<T> to read and write settings.

// read config in your class
// you can also use IOptions<T>, IOptionsMonitor<T> or IOptionsSnapshot<T>
public class ConfigReadService(IReadOnlyOptions<UserSetting> options)
{
    public void Print()
    {
        // get the UserSetting instance
        var sampleSetting = options.CurrentValue;
        Console.WriteLine($">> Name: {sampleSetting.Name}");
    }
}

// read and write config in your class
public class ConfigReadWriteService(IWritableOptions<UserSetting> options)
{
    public async Task UpdateAsync()
    {
        // get the UserSetting instance
        var sampleSetting = options.CurrentValue;
        // and save to storage
        await options.SaveAsync(setting =>
        {
            setting.Name = "new name";
        });
    }
}

Configuration Structure

When saving settings, they are written to a configuration file in a structured format. By default, settings are stored in this structure:

{
  // root level section: the type name of the setting class (e.g. "UserSetting")
  // If InstanceName is specified, it becomes "TypeName-InstanceName"
  "UserSetting": {
    // properties of UserSetting
    "Name": "custom name",
    "Age": 30
  }
}

The reasons for this structure are as follows:

  • The type name is automatically used as the root section, eliminating the need for manual configuration in most cases.
  • When using InstanceName, multiple configurations of the same type can be managed separately.

Of course, you can customize this structure as needed. see SectionName.

Customization

Configuration Method

You can change various settings as arguments to Initialize or AddWritableOptions.

// Without DI
WritableConfig.Initialize<SampleSetting>(opt => { /* ... */ });

// With DI
builder.Services.AddWritableOptions<UserSetting>(opt => { /* ... */ });

Save Location

Default behavior is to save to {AppContext.BaseDirectory}/usersettings.json (in general, the same directory as the executable). If you want to change the save location, use opt.FilePath or opt.UseStandardSaveLocation("MyAppId").

For example:

// to save to the parent directory
opt.FilePath = "../myconfig";

// to save to child directory
opt.FilePath = "config/myconfig";

// to save to a common settings directory
//   in Windows: %APPDATA%/MyAppId
//   in macOS: $XDG_CONFIG_HOME/MyAppId or ~/Library/Application Support/MyAppId
//   in Linux: $XDG_CONFIG_HOME/MyAppId or ~/.config/MyAppId
opt.UseStandardSaveLocation("MyAppId");

If you want to toggle between development and production environments, you can use #if RELEASE pattern or builder.Environtment.IsProduction().

// those pattern are saved to
// - development: ./mysettings.json (executable directory)
// - production:  %APPDATA%/MyAppId/mysettings.json (on Windows)

// without DI
WritableConfig.Initialize<UserSetting>(opt => {
    opt.FilePath = "mysettings.json";
#if RELEASE
    opt.UseStandardSaveLocation("MyAppId");
#endif
});

// if using IHostApplicationBuilder
builder.Services.AddWritableOptions<UserSetting>(opt => {
    opt.FilePath = "mysettings.json";
    if (builder.Environment.IsProduction()) {
        opt.UseStandardSaveLocation("MyAppId");
    }
});

Provider

If you want to change the format when saving files, specify opt.Provider. Currently, the following providers are available:

Provider Description NuGet Package
WritableConfigJsonProvider save in JSON format. Built-in
WritableConfigXmlProvider save in XML format. NuGet Version
WritableConfigYamlProvider save in YAML format. NuGet Version
WritableConfigEncryptProvider save in AES-256-CBC encrypted JSON format. NuGet Version
// use Json format with indentation
opt.Provider = new WritableConfigJsonProvider() {
    JsonSerializerOptions = { WriteIndented = true },
};

// use Yaml format
// (you need to install Configuration.Writable.Yaml package)
opt.Provider = new WritableConfigYamlProvider();

// use encrypted format
// NOTE: Be aware that this is a simple encryption.
// (you need to install Configuration.Writable.Encrypt package)
opt.Provider = new WritableConfigEncryptProvider("any-encrypt-password");

To reduce dependencies and allow users to choose only the features they need, providers are offered as separate packages. That said, the JSON provider is built into the main package because since many users are likely to use the JSON format.

FileWriter

Default FileWriter (CommonFileWriter) supports the following features:

  • Automatically retry when file access fails (default is max 3 times, wait 100ms each)
  • Create backup files rotated by timestamp (default is disabled)
  • Atomic file writing (write to a temporary file first, then rename it)
  • Thread-safe: uses internal semaphore to ensure safe concurrent access

If you want to change the way files are written, create a class that implements IFileWriter and specify it in opt.FileWriter.

using Configuration.Writable.FileWriter;

opt.FileWriter = new CommonFileWriter() {
    // retry up to 5 times when file access fails
    MaxRetryCount = 5,
    // wait 100ms, 200ms, 300ms, ... before each retry
    RetryDelay = (attempt) => 100 * attempt,
    // keep 5 backup files when saving
    BackupMaxCount = 5,
};

Direct Reference Without Option Type

If you want to directly reference the settings class, specify opt.RegisterInstanceToContainer = true.

The dynamic update functionality provided by IOptionsMonitor<T> will no longer be available. Be mindful of lifecycle management, as settings applied during instance creation will be reflected.

builder.Services.AddWritableOptions<UserSetting>(opt => {
    opt.RegisterInstanceToContainer = true;
});

// you can use UserSetting directly
public class MyService(UserSetting setting)
{
    public void Print()
    {
        Console.WriteLine($">> Name: {setting.Name}");
    }
}

// and you can also use IReadOnlyOptions<T> as usual
public class MyOtherService(IReadOnlyOptions<UserSetting> options)
{
    public void Print()
    {
        var setting = options.CurrentValue;
        Console.WriteLine($">> Name: {setting.Name}");
    }
}

Logging

Logging is enabled by default in DI environments.
If you are not using DI, or if you want to override the logging settings, you can enable logging by specifying opt.Logger.

// without DI
// package add Microsoft.Extensions.Logging.Console
opt.Logger = LoggerFactory
    // enable console logging 
    .Create(builder => builder.AddConsole())
    .CreateLogger("Configuration.Writable");

// with DI
// no setup required (uses the logger from DI)

When the output level is set to Information, mainly the following two logs are output.

info: Configuration.Writable[0]
      Configuration file change detected: mysettings.json (Renamed)
info: Configuration.Writable[0]
      Configuration saved successfully to mysettings.json

SectionName

By default, the section path is automatically determined as {TypeName}(-{InstanceName}). To customize the entire section path, use opt.SectionName.

{
  // opt.SectionName = "MyAppSettings:Foo:Bar"
  "MyAppSettings": {
    "Foo": {
      "Bar": {
        // properties of UserSetting
        "Name": "custom name",
        "Age": 30
      }
    }
  }
}
{
  // opt.SectionName = "" (empty string)
  // properties of UserSetting directly at the root level (not recommended)
  "Name": "custom name",
  "Age": 30
}

InstanceName

If you want to manage multiple settings of the same type, you must specify different InstanceName for each setting.

// first setting
builder.Services.AddWritableOptions<UserSetting>(opt => {
    opt.FilePath = "firstsettings.json";
    opt.InstanceName = "First";
    // save section will be "UserSetting-First"
});
// second setting
builder.Services.AddWritableOptions<UserSetting>(opt => {
    opt.FilePath = "secondsettings.json";
    opt.InstanceName = "Second";
    // save section will be "UserSetting-Second"
});

// and get each setting from DI
public class MyService(IWritableOptions<UserSetting> options)
{
    public void GetAndSave()
    {
        // cannot use .CurrentValue if multiple settings of the same type are registered
        var firstSetting = options.Get("First");
        var secondSetting = options.Get("Second");
        // and you must specify instance name when saving
        await options.SaveWithNameAsync("First", setting => {
            setting.Name = "first name";
        });
        await options.SaveWithNameAsync("Second", setting => {
            setting.Name = "second name";
        });
    }
}

When not using DI (direct use of WritableConfig), managing multiple configurations is intentionally not supported. This is to avoid complicating usage.

Validation

By default, validation using DataAnnotations is enabled. If validation fails, an OptionsValidationException is thrown and the settings are not saved.

using Microsoft.Extensions.Options;

builder.Services.AddWritableOptions<UserSetting>(opt => {
    // if you want to disable validation of DataAnnotations, do the following:
    // opt.UseDataAnnotationsValidation = false;
});

var options = WritableConfig.GetOptions<UserSetting>();
try {
    await options.SaveAsync(setting => {
        setting.Name = "ab"; // too short
        setting.Age = 200;  // out of range
    });
}
catch (OptionsValidationException ex)
{
    Console.WriteLine($">> Validation failed: {ex.Message}");
    // setting is not saved if validation fails
}

internal class UserSetting
{
    [Required, MinLength(3)]
    public string Name { get; set; } = "default name";
    [Range(0, 150)]
    public int Age { get; set; } = 20;
}

Alternatively, you can add custom validation using WithValidatorFunction or WithValidator.

using Microsoft.Extensions.Options;

builder.Services.AddWritableOptions<UserSetting>(opt => {
    // add custom validation function
    opt.WithValidatorFunction(setting => {
        if (setting.Name.Contains("invalid"))
            return ValidateOptionsResult.Fail("Name must not contain 'invalid'.");
        return ValidateOptionsResult.Success;
    });
    // or use a custom validator class
    opt.WithValidator<MyCustomValidator>();
});

// IValidateOptions sample
internal class MyCustomValidator : IValidateOptions<UserSetting>
{
    public ValidateOptionsResult Validate(string? name, UserSetting options)
    {
        if (options.Age < 10)
            return ValidateOptionsResult.Fail("Age must be at least 10.");
        if (options.Age > 100)
            return ValidateOptionsResult.Fail("Age must be 100 or less.");
        return ValidateOptionsResult.Success;
    }
}

Validation at startup is intentionally not provided. The reason is that in the case of user settings, it is preferable to prompt for correction rather than prevent startup when a validation error occurs.

Tips

Default Values

Due to the specifications of MS.E.C, properties that do not exist in the configuration file will use their default values.
If a new property is added to the settings class during an update, that property will not exist in the configuration file, so the default value will be used.

// if the settings file contains only {"Name": "custom name"}
var setting = options.CurrentValue;
// setting.Name is "custom name"
// setting.Age is 20 (the default value)

Secret Values

A good way to include user passwords and the like in settings is to split the class and save one part encrypted.

// register multiple settings in DI
builder
    .AddWritableOptions<UserSetting>(opt => {
        opt.FilePath = "usersettings";
    })
    .AddWritableOptions<UserSecretSetting>(opt => {
        opt.FilePath = "my-secret-folder/secrets";
        // dotnet add package Configuration.Writable.Encrypt
        opt.Provider = new WritableConfigEncryptProvider("any-encrypt-password");
    });

// and get/save each setting
var userOptions = WritableConfig.GetOptions<UserSetting>();
var secretOptions = WritableConfig.GetOptions<UserSecretSetting>();
// ...

// ---
// setting classes
public class UserSetting(string Name, int Age);  // non-secret
public class UserSecretSetting(string Password); // secret

Do not store values that must not be disclosed to others (e.g., database passwords). This feature is solely intended to prevent others from viewing values entered by the user.

Testing

If you simply want to obtain IReadOnlyOptions<T> or IWritableOptions<T>, using WritableOptionsStub is straightforward.

using Configuration.Writable.Testing;

var settingValue = new UserSetting();
var options = WritableOptionsStub.Create(settingValue);

// and use options in your test
var yourService = new YourService(options);
yourService.DoSomething();

// settingValue is updated when yourService changes it
Assert.Equal("expected name", settingValue.Name);

If you want to perform tests that actually involve writing to the file system, use WritableOptionsSimpleInstance.

var sampleFilePath = Path.GetTempFileName();
var instance = new WritableOptionsSimpleInstance<UserSetting>();
instance.Initialize(opt => {
    opt.FilePath = sampleFilePath;
});
var option = instance.GetOptions();

// and use options in your test
var yourService = new YourService(options);
yourService.DoSomething();

// sampleFilePath now contains the updated settings
var json = File.ReadAllText(sampleFilePath);
Assert.Contains("expected name", json);

Interfaces

IReadOnlyOptions<T>

An interface for reading the settings of the registered type T.
It automatically reflects the latest settings when the underlying configuration is updated.
This interface provides functionality equivalent to IOptionsMonitor<T> from MS.E.O.

public interface IReadOnlyOptions<T> : IOptionsMonitor<T> where T : class

The additional features compared to IOptionsMonitor<T> are as follows:

  • The GetConfigurationOptions method to retrieve configuration options.
  • In environments where file change detection is not possible (for example, on network shares or in Docker environments where change detection is not supported by default), you can always get the latest settings. This is achieved by using the cache maintained when saving via IWritableOptions<T>

IWritableOptions<T>

An interface for reading and writing the settings of the registered type T.
It provides the same functionality as IReadOnlyOptions<T>, with additional support for saving settings.

public interface IWritableOptions<T> : IReadOnlyOptions<T> where T : class

Limitations

this library currently does not support the following features.

Saving Integrated Settings

MS.E.C provides a feature to integrate multiple configuration sources, but saving settings in this scenario introduces a problem.
Since the settings are presented as a merged view, it becomes unclear "which source" should be updated with "which value" when saving.
Therefore, this library currently does not support saving integrated (merged) settings.

Dynamic Addition and Removal of Configuration Files

For example, in applications like VSCode, in addition to global settings, you can manage settings by dynamically adding or removing files such as .vscode/settings.json found in the currently opened folder. This library assumes that configuration files are added all at once during application startup, and does not support dynamic addition or removal of configuration files at runtime. (Also, related to the first limitation, it becomes unclear which configuration file should be saved to.)

License

This project is licensed under the Apache-2.0 License.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.4.3 371 2/4/2026
0.4.2 134 2/3/2026
0.4.1 120 2/2/2026
0.4.1-gb9a214a6bc 112 2/2/2026
0.4.0 145 1/15/2026
0.3.0 123 1/6/2026
0.2.1 113 1/5/2026
0.2.0 111 1/5/2026
0.1.12 113 1/2/2026
0.1.3 187 12/23/2025
0.1.0 173 12/21/2025
0.1.0-alpha.159 178 12/19/2025
0.1.0-alpha.149 236 12/18/2025
0.1.0-alpha.119 138 10/22/2025
0.1.0-alpha.106 171 10/14/2025
0.1.0-alpha.101 146 10/13/2025
0.1.0-alpha.100 147 10/12/2025
0.1.0-alpha.53 147 10/8/2025
0.1.0-alpha.46 175 9/29/2025
0.1.0-alpha.2 124 12/21/2025
Loading failed