Saving IOptions<T>
Let's say you have an application that sends SMTP messages and where administrator can modify SMTP settings from GUI.
Changes need to be saved on file and then reloaded so that changes reflect to IOptionsMonitor<SmtpSettings>.
In this example a console application reads IOptions<SmtpSettings> from a .yaml.
// Settings file
string settingsFile = "appsettings-saved.yaml";
// Create configuration
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddYamlFile(settingsFile, optional: true, reloadOnChange: true)
.Build();
// Create dependency injection
IServiceCollection serviceCollection = new ServiceCollection()
.AddSingleton<IConfiguration>(configuration);
serviceCollection.AddOptions<SmtpSettings>().BindConfiguration("Smtp");
IServiceProvider services = serviceCollection.BuildServiceProvider();
// Read stmp settings
IOptionsMonitor<SmtpSettings> options = services.GetRequiredService<IOptionsMonitor<SmtpSettings>>();
/// <summary>"Smtp" settings</summary>
/// <example>
/// appsettings.yaml:
///
/// Smtp:
/// UserName: contact@server
/// Server: server
/// Port: 465
/// Password: password here
/// FromAddress noreply@server
/// Tls: true
/// </example>
public record SmtpSettings
{
/// <summary></summary>
public string? Server { get; set; }
/// <summary></summary>
public int Port { get; set; }
/// <summary></summary>
public string? FromAddress { get; set; }
/// <summary></summary>
public string? UserName { get; set; }
/// <summary></summary>
public string? Password { get; set; }
/// <summary></summary>
public bool? Tls { get; set; }
}
Modifications are saved to a file and then automatically reloaded. IOptionsMonitor receives a change event.
// Create new password
string newPassword = $"NewPassword-{new Random().Next(0, 1000)}";
// Create modified version of settings
SmtpSettings settingsChanged = options.CurrentValue with { Password = newPassword, UserName = "NewSmtpUserName@server" };
// Convert to key-values
List<KeyValuePair<string, string?>> keyValues = ConfigurationBindingExtensions.BindAsKeyValues(settingsChanged, typeof(SmtpSettings), path: "Smtp").ToList();
// Create yaml stream
YamlStream yamlStream = new YamlStream();
// Place here the document where code-managed changes are saved
YamlDocument? codegeneratedDocument = null!;
// Read existing .yaml file
if (File.Exists(settingsFile))
{
using Stream stream = new FileStream(settingsFile, FileMode.Open, FileAccess.Read);
using StreamReader reader = new StreamReader(stream);
yamlStream.Load(reader);
codegeneratedDocument = yamlStream.Documents.FirstOrDefault(d => d.RootNode.Anchor == "AppManaged");
}
// Create-or-update codegenerated root
YamlNode root = YamlNodeExtensions.ToYaml(
properties: keyValues,
policy: YamlNodeExtensions.CorrelatePolicy.RemoveUnusedNodes | YamlNodeExtensions.CorrelatePolicy.RemoveAmbiguentNodes,
targetRoot: codegeneratedDocument?.RootNode,
delimiter: ':');
// Indicate that this root is managed by code
root.Anchor = "AppManaged";
// Create new document
if (codegeneratedDocument == null)
{
codegeneratedDocument = new YamlDocument(root);
yamlStream.Documents.Add(codegeneratedDocument);
}
// Add change listener (before save)
options.OnChange((SmtpSettings newSettings) => Console.WriteLine($"Settings were modified, password = {newSettings.Password}"));
// Save
{
using Stream stream = new FileStream(settingsFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
using StreamWriter writer = new StreamWriter(stream);
yamlStream.Save(writer, assignAnchors: false);
}
// Wait for options to reload changes (token listener could also be used)
Thread.Sleep(5000);
WriteLine(options.CurrentValue.Password);
// "Settings were modified, password = NewPassword-128"
// "NewPassword - 128"
appsettings-saved.yml:
&AppManaged
Smtp:
FromAddress: ''
Password: NewPassword-849
Port: 0
Server: ''
Tls: ''
UserName: NewSmtpUserName@server
...
appsettings-saved.yml can contain multiple document parts. Only one part is managed by code. Other parts can be managed by user with text editor:
# This part is managed by the application.
# It can be modified externally, but the structure may be modified by the application.
&AppManaged
Smtp:
FromAddress: ''
Password: NewPassword-679
Port: 0
Server: ''
Tls: ''
UserName: NewSmtpUserName@server
...
---
# This part is not code-generated and can be edited externally.
Key: Value
...
Full Example
Full example
using Avalanche.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using YamlDotNet.RepresentationModel;
using static System.Console;
public class saving
{
public static void Run()
{
{
// <01>
// Settings file
string settingsFile = "appsettings-saved.yaml";
// Create configuration
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddYamlFile(settingsFile, optional: true, reloadOnChange: true)
.Build();
// Create dependency injection
IServiceCollection serviceCollection = new ServiceCollection()
.AddSingleton<IConfiguration>(configuration);
serviceCollection.AddOptions<SmtpSettings>().BindConfiguration("Smtp");
IServiceProvider services = serviceCollection.BuildServiceProvider();
// Read stmp settings
IOptionsMonitor<SmtpSettings> options = services.GetRequiredService<IOptionsMonitor<SmtpSettings>>();
// </01>
// <02>
// Create new password
string newPassword = $"NewPassword-{new Random().Next(0, 1000)}";
// Create modified version of settings
SmtpSettings settingsChanged = options.CurrentValue with { Password = newPassword, UserName = "NewSmtpUserName@server" };
// Convert to key-values
List<KeyValuePair<string, string?>> keyValues = ConfigurationBindingExtensions.BindAsKeyValues(settingsChanged, typeof(SmtpSettings), path: "Smtp").ToList();
// Create yaml stream
YamlStream yamlStream = new YamlStream();
// Place here the document where code-managed changes are saved
YamlDocument? codegeneratedDocument = null!;
// Read existing .yaml file
if (File.Exists(settingsFile))
{
using Stream stream = new FileStream(settingsFile, FileMode.Open, FileAccess.Read);
using StreamReader reader = new StreamReader(stream);
yamlStream.Load(reader);
codegeneratedDocument = yamlStream.Documents.FirstOrDefault(d => d.RootNode.Anchor == "AppManaged");
}
// Create-or-update codegenerated root
YamlNode root = YamlNodeExtensions.ToYaml(
properties: keyValues,
policy: YamlNodeExtensions.CorrelatePolicy.RemoveUnusedNodes | YamlNodeExtensions.CorrelatePolicy.RemoveAmbiguentNodes,
targetRoot: codegeneratedDocument?.RootNode,
delimiter: ':');
// Indicate that this root is managed by code
root.Anchor = "AppManaged";
// Create new document
if (codegeneratedDocument == null)
{
codegeneratedDocument = new YamlDocument(root);
yamlStream.Documents.Add(codegeneratedDocument);
}
// Add change listener (before save)
options.OnChange((SmtpSettings newSettings) => Console.WriteLine($"Settings were modified, password = {newSettings.Password}"));
// Save
{
using Stream stream = new FileStream(settingsFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
using StreamWriter writer = new StreamWriter(stream);
yamlStream.Save(writer, assignAnchors: false);
}
// Wait for options to reload changes (token listener could also be used)
Thread.Sleep(5000);
WriteLine(options.CurrentValue.Password);
// "Settings were modified, password = NewPassword-128"
// "NewPassword - 128"
// </02>
}
}
}
// <03>
/// <summary>"Smtp" settings</summary>
/// <example>
/// appsettings.yaml:
///
/// Smtp:
/// UserName: contact@server
/// Server: server
/// Port: 465
/// Password: password here
/// FromAddress noreply@server
/// Tls: true
/// </example>
public record SmtpSettings
{
/// <summary></summary>
public string? Server { get; set; }
/// <summary></summary>
public int Port { get; set; }
/// <summary></summary>
public string? FromAddress { get; set; }
/// <summary></summary>
public string? UserName { get; set; }
/// <summary></summary>
public string? Password { get; set; }
/// <summary></summary>
public bool? Tls { get; set; }
}
// </03>