Configuration.Writable.Core
0.1.0-alpha.119
dotnet add package Configuration.Writable.Core --version 0.1.0-alpha.119
NuGet\Install-Package Configuration.Writable.Core -Version 0.1.0-alpha.119
<PackageReference Include="Configuration.Writable.Core" Version="0.1.0-alpha.119" />
<PackageVersion Include="Configuration.Writable.Core" Version="0.1.0-alpha.119" />
<PackageReference Include="Configuration.Writable.Core" />
paket add Configuration.Writable.Core --version 0.1.0-alpha.119
#r "nuget: Configuration.Writable.Core, 0.1.0-alpha.119"
#:package Configuration.Writable.Core@0.1.0-alpha.119
#addin nuget:?package=Configuration.Writable.Core&version=0.1.0-alpha.119&prerelease
#tool nuget:?package=Configuration.Writable.Core&version=0.1.0-alpha.119&prerelease
Configuration.Writable
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.
- Automatic detection of external changes to configuration files and reflection of the latest settings.
- Simple API that can be easily used in applications both with and without DI.
- Extends
Microsoft.Extensions.Optionsinterfaces, so it works seamlessly with existing code usingIOptions<T>,IOptionsMonitor<T>, etc. - Supports various file formats (Json, Xml, Yaml, Encrypted, etc...) via providers.
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";
});
}
}
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. | |
| WritableConfigYamlProvider | save in YAML format. | |
| WritableConfigEncryptProvider | save in AES-256-CBC encrypted JSON format. |
// 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.
FileProvider
Default FileProvider (CommonFileProvider) 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 IFileProvider and specify it in opt.FileProvider.
using Configuration.Writable.FileProvider;
opt.FileProvider = new CommonFileProvider() {
// 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
When saving settings, they are written to a configuration file in a structured format. By default, settings are stored directly at the root level:
{
// properties of UserSetting are stored directly at the root level
"Name": "custom name",
"Age": 30
}
To organize settings under a specific section, use opt.SectionName.
{
// opt.SectionName = "MyAppSettings:Foo:Bar"
"MyAppSettings": {
"Foo": {
"Bar": {
// properties of UserSetting
"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";
});
// second setting
builder.Services.AddWritableOptions<UserSetting>(opt => {
opt.FilePath = "secondsettings.json";
opt.InstanceName = "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;
}
To use source generators for DataAnnotations, use the following pattern.
builder.Services.AddWritableOptions<UserSetting>(opt => {
// disable attributes-based validation
opt.UseDataAnnotationsValidation = false;
// enable source-generator-based validation
opt.WithValidator<UserSettingValidator>();
});
internal class UserSetting { /* ... */ }
[OptionsValidator]
public partial class UserSettingValidator : IValidateOptions<UserSetting>;
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.
Advanced Usage
Dynamic Add/Remove Options
You can dynamically add or remove writable options at runtime using IConfigurationOptionsRegistry.
for example, in addition to common application settings, it is useful when you want to have individual settings for each document opened by the user.
// use IConfigurationOptionsRegistry from DI
public class DynamicOptionsService(IConfigurationOptionsRegistry<UserSetting> registry)
{
public void AddNewOptions(string instanceName, string filePath)
{
registry.TryAdd(opt => {
opt.InstanceName = instanceName;
opt.FilePath = filePath;
});
}
public void RemoveOptions(string instanceName)
{
registry.TryRemove(instanceName);
}
}
// and you can access IOptionsMonitor<T> or IWritableOptions<T> as usual
public class MyService(IWritableOptions<UserSetting> options)
{
public void UseOptions()
{
var commonSetting = options.Get("Common");
var documentSetting = options.Get("UserDocument1");
var name = documentSetting.Name ?? commonSetting.Name ?? "default";
Console.WriteLine($">> Name: {name}");
// and save to specific instance
await options.SaveWithNameAsync("UserDocument1", setting => {
setting.Name = "document specific name";
}
}
}
Multiple Settings in a Single File
Using ZipFileProvider, you can save multiple settings classes in a single configuration file.
for example, to save Foo(foo.json) and Bar(bar.json) in configurations.zip:
var zipFileProvider = new ZipFileProvider { ZipFileName = "configurations.zip" };
// initialize each setting with the same file provider
builder.Services.AddWritableOptions<Foo>(opt =>
{
opt.FilePath = "foo";
opt.FileProvider = zipFileProvider;
});
builder.Services.AddWritableOptions<Bar>(opt =>
{
opt.FilePath = "bar";
opt.FileProvider = zipFileProvider;
});
Direct Property Manipulation
You can directly manipulate configuration properties at the key level using IOptionOperator<T>. This is useful for operations like deleting specific keys from the configuration file.
await options.SaveAsync((setting, op) =>
{
// Update settings as usual
setting.Name = "new name";
// Delete specific keys from the configuration file
op.DeleteKey(s => s.SomeProperty);
op.DeleteKey(s => s.Parent.Child);
});
This pattern allows you to:
- Delete keys without affecting other properties in the configuration file
- Perform key-level operations that go beyond simple value updates
- Maintain more control over the configuration file structure
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, new()
The additional features compared to IOptionsMonitor<T> are as follows:
- The
GetConfigurationOptionsmethod to retrieve configuration options. - In environments where file change detection is not possible, you can always get the latest settings (internal cached value is updated when
SaveAsyncis called).
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, new()
ToDo
- Support version update migration
- Support multiple configurations merging
License
This project is licensed under the Apache-2.0 License.
| Product | Versions 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. |
-
.NETStandard 2.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.10)
- Microsoft.Extensions.Options (>= 9.0.10)
- System.ComponentModel.Annotations (>= 5.0.0)
- System.Text.Json (>= 9.0.10)
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.10)
- Microsoft.Extensions.Options (>= 9.0.10)
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.10)
- Microsoft.Extensions.Options (>= 9.0.10)
-
net9.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.10)
- Microsoft.Extensions.Options (>= 9.0.10)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on Configuration.Writable.Core:
| Package | Downloads |
|---|---|
|
Configuration.Writable
A lightweight library that allows for easy saving and referencing of settings, with extensive customization options. |
|
|
Configuration.Writable.Yaml
A lightweight library that allows for easy saving and referencing of settings, with extensive customization options. |
|
|
Configuration.Writable.Xml
A lightweight library that allows for easy saving and referencing of settings, with extensive customization options. |
|
|
Configuration.Writable.Encrypt
A lightweight library that allows for easy saving and referencing of settings, with extensive customization options. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0-alpha.119 | 124 | 10/22/2025 |
| 0.1.0-alpha.106 | 143 | 10/14/2025 |
| 0.1.0-alpha.101 | 126 | 10/13/2025 |
| 0.1.0-alpha.100 | 120 | 10/12/2025 |