I am currently working on project using asp.net core v1.1, and in my appsettings.json I have:
"AppSettings": {
"AzureConnectionKey": "***",
"AzureContainerName": "**",
"NumberOfTicks": 621355968000000000,
"NumberOfMiliseconds": 10000,
"SelectedPvInstalationIds": [ 13, 137, 126, 121, 68, 29 ],
"MaxPvPower": 160,
"MaxWindPower": 5745.35
},
I also have class that I use to store them:
public class AppSettings
{
public string AzureConnectionKey { get; set; }
public string AzureContainerName { get; set; }
public long NumberOfTicks { get; set; }
public long NumberOfMiliseconds { get; set; }
public int[] SelectedPvInstalationIds { get; set; }
public decimal MaxPvPower { get; set; }
public decimal MaxWindPower { get; set; }
}
And DI enabled to use then in Startup.cs:
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
Is there any way to change and save MaxPvPower and MaxWindPower from Controller?
I tried using
private readonly AppSettings _settings;
public HomeController(IOptions<AppSettings> settings)
{
_settings = settings.Value;
}
[Authorize(Policy = "AdminPolicy")]
public IActionResult UpdateSettings(decimal pv, decimal wind)
{
_settings.MaxPvPower = pv;
_settings.MaxWindPower = wind;
return Redirect("Settings");
}
But it did nothing.
Basically you can set the values in IConfiguration like this:
IConfiguration configuration = ...
// ...
configuration["key"] = "value";
The issue there is that e.g. the JsonConfigurationProvider does not implement the saving of the configuration into the file. As you can see in the source it does not override the Set method of ConfigurationProvider. (see source)
You can create your own provider and implement the saving there. Here (Basic sample of Entity Framework custom provider) is an example how to do it.
Here is a relevant article from Microsoft regarding Configuration setup in .Net Core Apps:
Asp.Net Core Configuration
The page also has sample code which may also be helpful.
Update
I thought In-memory provider and binding to a POCO class might be of some use but does not work as OP expected.
The next option can be setting reloadOnChange parameter of AddJsonFile to true while adding the configuration file and
manually parsing the JSON configuration file and making changes as intended.
public class Startup
{
...
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
...
}
... reloadOnChange is only supported in ASP.NET Core 1.1 and higher.
Update appsettings.json file in ASP.NET Core at runtime.
Take this sample appsettings.json file:
{
Config: {
IsConfig: false
}
}
This is the code to update IsConfig property to true:
Main()
{
AddOrUpdateAppSetting("Config:IsConfig", true);
}
public static void AddOrUpdateAppSetting<T>(string key, T value)
{
try
{
var filePath = Path.Combine(AppContext.BaseDirectory, "appSettings.json");
string json = File.ReadAllText(filePath);
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
var sectionPath = key.Split(":")[0];
if (!string.IsNullOrEmpty(sectionPath))
{
var keyPath = key.Split(":")[1];
jsonObj[sectionPath][keyPath] = value;
}
else
{
jsonObj[sectionPath] = value; // if no sectionpath just set the value
}
string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(filePath, output);
}
catch (ConfigurationErrorsException)
{
Console.WriteLine("Error writing app settings");
}
}
I took Qamar Zamans code (thank you) and modified it to allow for editing parameters which are more:than:one:layer:deep.
Hope it helps someone out, surprised that this isn't a library feature somewhere.
public static class SettingsHelpers
{
public static void AddOrUpdateAppSetting<T>(string sectionPathKey, T value)
{
try
{
var filePath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
string json = File.ReadAllText(filePath);
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
SetValueRecursively(sectionPathKey, jsonObj, value);
string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(filePath, output);
}
catch (Exception ex)
{
Console.WriteLine("Error writing app settings | {0}", ex.Message);
}
}
private static void SetValueRecursively<T>(string sectionPathKey, dynamic jsonObj, T value)
{
// split the string at the first ':' character
var remainingSections = sectionPathKey.Split(":", 2);
var currentSection = remainingSections[0];
if (remainingSections.Length > 1)
{
// continue with the procress, moving down the tree
var nextSection = remainingSections[1];
SetValueRecursively(nextSection, jsonObj[currentSection], value);
}
else
{
// we've got to the end of the tree, set the value
jsonObj[currentSection] = value;
}
}
public static void SetAppSettingValue(string key, string value, string appSettingsJsonFilePath = null)
{
if (appSettingsJsonFilePath == null)
{
appSettingsJsonFilePath = System.IO.Path.Combine(System.AppContext.BaseDirectory, "appsettings.json");
}
var json = System.IO.File.ReadAllText(appSettingsJsonFilePath);
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JObject>(json);
jsonObj[key] = value;
string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
System.IO.File.WriteAllText(appSettingsJsonFilePath, output);
}
According to Qamar Zaman and Alex Horlock codes, I've changed it a little bit.
public static class SettingsHelpers
{
public static void AddOrUpdateAppSetting<T>(T value, IWebHostEnvironment webHostEnvironment)
{
try
{
var settingFiles = new List<string> { "appsettings.json", $"appsettings.{webHostEnvironment.EnvironmentName}.json" };
foreach (var item in settingFiles)
{
var filePath = Path.Combine(AppContext.BaseDirectory, item);
string json = File.ReadAllText(filePath);
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
SetValueRecursively(jsonObj, value);
string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(filePath, output);
}
}
catch (Exception ex)
{
throw new Exception($"Error writing app settings | {ex.Message}", ex);
}
}
private static void SetValueRecursively<T>(dynamic jsonObj, T value)
{
var properties = value.GetType().GetProperties();
foreach (var property in properties)
{
var currentValue = property.GetValue(value);
if (property.PropertyType.IsPrimitive || property.PropertyType == typeof(string) || property.PropertyType == typeof(decimal))
{
if (currentValue == null) continue;
try
{
jsonObj[property.Name].Value = currentValue;
}
catch (RuntimeBinderException)
{
jsonObj[property.Name] = new JValue(currentValue);
}
continue;
}
try
{
if (jsonObj[property.Name] == null)
{
jsonObj[property.Name] = new JObject();
}
}
catch (RuntimeBinderException)
{
jsonObj[property.Name] = new JObject(new JProperty(property.Name));
}
SetValueRecursively(jsonObj[property.Name], currentValue);
}
}
}
I see most of the answers use Newtonsoft.Json package for updating the settings. If you need to update the settings that are one layer deep, you can go without Newtonsoft.Json and use System.Text.Json (built-in on .Net Core 3.0 and above) functionality. Here's a simple implementation:
public void UpdateAppSetting(string key, string value)
{
var configJson = File.ReadAllText("appsettings.json");
var config = JsonSerializer.Deserialize<Dictionary<string, object>>(configJson);
config[key] = value;
var updatedConfigJson = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText("appsettings.json", updatedConfigJson);
}
Suppose appsettings.json has an eureka port, and want to change it dynamically in args (-p 5090). By doing this, can make change to the port easily for docker when creating many services.
"eureka": {
"client": {
"serviceUrl": "http://10.0.0.101:8761/eureka/",
"shouldRegisterWithEureka": true,
"shouldFetchRegistry": false
},
"instance": {
"port": 5000
}
}
public class Startup
{
public static string port = "5000";
public Startup(IConfiguration configuration)
{
configuration["eureka:instance:port"] = port;
Configuration = configuration;
}
public static void Main(string[] args)
{
int port = 5000;
if (args.Length>1)
{
if (int.TryParse(args[1], out port))
{
Startup.port = port.ToString();
}
}
}
I'm using my own configuration section and my own strongly typed object. I'm always injecting IOptions with this strongly typed object. And I'm able to change configuration in runtime. Be very careful with scopes of objects. New configuration values are picked up by request scoped object. I'm using constructor injection.
Documentation on this is very unclear though .. I'm no sure if this is meant to be. Read this in-depth discussion
There is an esier answer to modify the appsettings.json at runtime.
Json File structure
var filePath = Path.Combine(System.AppContext.BaseDirectory, "appSettings.json");
string jsonString = System.IO.File.ReadAllText(filePath);
//use https://json2csharp.com/ to create the c# classes from your json
Root root = JsonSerializer.Deserialize<Root>(jsonString);
var dbtoadd = new Databas()
{
Id = "myid",
Name = "mynewdb",
ConnectionString = ""
};
//add or change anything to this object like you do on any list
root.DatabaseSettings.Databases.Add(dbtoadd);
//serialize the new updated object to a string
string towrite = JsonSerializer.Serialize(root);
//overwrite the file and it wil contain the new data
System.IO.File.WriteAllText(filePath, towrite);
The way I address this issue is by adding an "override" property is stored in a memory cache. So for example, my application has a "CacheEnabled" setting in the "appSettings.json" file that determines whether or not data query results are cached or not. During application / db testing, it is sometimes desirable to set this property to "false".
Through an administrator menu, an Administrator can override the "CacheEnabled" setting. The logic that determines whether or not the Cache is enabled, first checks for the override. If it doesn't find an override value, then it uses the "appSettings.json" value.
This probably isn't a good solution for a lot of people given the extra infrastructure needed to implement it. However, my application already had a Caching Service as well as an Administrator menu, so it was pretty easy to implement.
In my project I'm work with Active Directory Settings this way:
//...
public class Startup
{
public void ConfigureServices(IServicesCollection services)
{
//...
services.Configure<Ldap>(opts=> {
opts.Url = "example.com";
opts.UseSsl = true;
opts.Port = 111;
opts.BindDn = "CN=nn,OU=nn,OU=nn,DC=nn,DC=nn";
opts.BindCredentials = "nn";
opts.SearchBase = "DC=nn,DC=nn";
opts.SearchFilter = "(&(objectClass=User){0})";
opts.AdminCn = "CN=nn,OU=nn,OU=nn,DC=nn,DC=nn";
opts.SearchGroupBase = "OU=nn,DC=nn,DC=nn";
});
//...
}
}
So, without using appsettings.json.
After that I can update this settings from controller:
//...
[HttpPost("setActiveDirectorySettings")]
public ActionResult<IOptions<Ldap>> SetActiveDirectorySettings(ActiveDirectorySettings clientActiveDirectorySettings)
{
LdapOptions.Value.Url = clientActiveDirectorySettings.Url;
LdapOptions.Value.UseSsl = clientActiveDirectorySettings.UseSsl;
LdapOptions.Value.Port = clientActiveDirectorySettings.Port;
LdapOptions.Value.BindDn = clientActiveDirectorySettings.BindDn;
LdapOptions.Value.BindCredentials = clientActiveDirectorySettings.BindCredentials;
LdapOptions.Value.SearchBase = clientActiveDirectorySettings.SearchBase;
LdapOptions.Value.SearchFilter = clientActiveDirectorySettings.SearchFilter;
LdapOptions.Value.AdminCn = clientActiveDirectorySettings.AdminCn;
LdapOptions.Value.SearchGroupBase = clientActiveDirectorySettings.SearchGroupBase;
return Ok(LdapOptions.Value);
}
//...
Looks like it works for me
Based on #Alper Ebicoglu answer
GET:
// ===== || GET || GET appsettings.js property =====================================================================
[HttpGet]
[Route("GetNotificationDays")]
public async Task<IActionResult> GetNotificationDays()
{
var path = System.IO.Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
var json = await System.IO.File.ReadAllTextAsync(path);
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JObject>(json);
return StatusCode(200, new { daysBefore = (int)jsonObj.InvoicementNotificationSettings.DaysBefore});
}
Exp:
(int)jsonObj.InvoicementNotificationSettings.DaysBefore =
(int) = cast to int - depending on the property
jsonObj = appsettings.js,
InvoicementNotificationSettings = object in appsettings.js,
DaysBefore = property in InvoicementNotificationSettings
UPDATE: appsettings.js
// ===== || PUT || UPDATE appsettings.js property =====================================================================
[HttpPut]
[Route("SetNotificationDays")]
public async Task<IActionResult> SetNotificationDays(int notificationDays)
{
if (notificationDays != 0)
{
var path = System.IO.Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
var json = await System.IO.File.ReadAllTextAsync(path);
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JObject>(json);
jsonObj.InvoicementNotificationSettings.DaysBefore = notificationDays;
string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
await System.IO.File.WriteAllTextAsync(path, output);
return await GetNotificationDays();
}
return StatusCode(409);
}
If reading appsettings from memmory:
Ex: int daysBefore = configuration.GetValue<int>("InvoicementNotificationSettings:DaysBefore");
Than In Startup.js - to autoreload appsettings.js after updating
public class Startup
{
public static IConfiguration Configuration { get; set; }
// Constructor -----------------------------------------------------------------------------------------------------------------------------
public Startup(IConfiguration configuration, Microsoft.Extensions.Hosting.IHostEnvironment env)
{
Configuration = configuration;
// To autoreload appsettings.js after update -------------------------
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
appsettings.js
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=ItlCrmsDb;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
},
"InvoicementNotificationSettings": {
"DaysBefore": 4
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Related
I am using the IOptions pattern as described in the official documentation.
This works fine when I am reading values from appsetting.json, but how do I update values and save changes back to appsetting.json?
In my case, I have a few fields that can be edited from the user interface (by admin user in application). Hence I am looking for the ideal approach to update these values via the option accessor.
At the time of writing this answer it seemed that there is no component provided by the Microsoft.Extensions.Options package that has functionality to write configuration values back to appsettings.json.
In one of my ASP.NET Core projects I wanted to enable the user to change some application settings - and those setting values should be stored in appsettings.json, more precisly in an optional appsettings.custom.json file, that gets added to the configuration if present.
Like this...
public Startup(IHostingEnvironment env)
{
IConfigurationBuilder builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile("appsettings.custom.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
this.Configuration = builder.Build();
}
I declared the IWritableOptions<T> interface that extends IOptions<T>; so I can just replace IOptions<T> by IWritableOptions<T> whenever I want to read and write settings.
public interface IWritableOptions<out T> : IOptions<T> where T : class, new()
{
void Update(Action<T> applyChanges);
}
Also, I came up with IOptionsWriter, which is a component that is intended to be used by IWritableOptions<T> to update a configuration section. This is my implementation for the beforementioned interfaces...
class OptionsWriter : IOptionsWriter
{
private readonly IHostingEnvironment environment;
private readonly IConfigurationRoot configuration;
private readonly string file;
public OptionsWriter(
IHostingEnvironment environment,
IConfigurationRoot configuration,
string file)
{
this.environment = environment;
this.configuration = configuration;
this.file = file;
}
public void UpdateOptions(Action<JObject> callback, bool reload = true)
{
IFileProvider fileProvider = this.environment.ContentRootFileProvider;
IFileInfo fi = fileProvider.GetFileInfo(this.file);
JObject config = fileProvider.ReadJsonFileAsObject(fi);
callback(config);
using (var stream = File.OpenWrite(fi.PhysicalPath))
{
stream.SetLength(0);
config.WriteTo(stream);
}
this.configuration.Reload();
}
}
Since the writer is not aware about the file structure, I decided to handle sections as JObject objects. The accessor tries to find the requested section and deserializes it to an instance of T, uses the current value (if not found), or just creates a new instance of T, if the current value is null. This holder object is than passed to the caller, who will apply the changes to it. Than the changed object gets converted back to a JToken instance that is going to replace the section...
class WritableOptions<T> : IWritableOptions<T> where T : class, new()
{
private readonly string sectionName;
private readonly IOptionsWriter writer;
private readonly IOptionsMonitor<T> options;
public WritableOptions(
string sectionName,
IOptionsWriter writer,
IOptionsMonitor<T> options)
{
this.sectionName = sectionName;
this.writer = writer;
this.options = options;
}
public T Value => this.options.CurrentValue;
public void Update(Action<T> applyChanges)
{
this.writer.UpdateOptions(opt =>
{
JToken section;
T sectionObject = opt.TryGetValue(this.sectionName, out section) ?
JsonConvert.DeserializeObject<T>(section.ToString()) :
this.options.CurrentValue ?? new T();
applyChanges(sectionObject);
string json = JsonConvert.SerializeObject(sectionObject);
opt[this.sectionName] = JObject.Parse(json);
});
}
}
Finally, I implemented an extension method for IServicesCollection allowing me to easily configure a writable options accessor...
static class ServicesCollectionExtensions
{
public static void ConfigureWritable<T>(
this IServiceCollection services,
IConfigurationRoot configuration,
string sectionName,
string file) where T : class, new()
{
services.Configure<T>(configuration.GetSection(sectionName));
services.AddTransient<IWritableOptions<T>>(provider =>
{
var environment = provider.GetService<IHostingEnvironment>();
var options = provider.GetService<IOptionsMonitor<T>>();
IOptionsWriter writer = new OptionsWriter(environment, configuration, file);
return new WritableOptions<T>(sectionName, writer, options);
});
}
}
Which can be used in ConfigureServices like...
services.ConfigureWritable<CustomizableOptions>(this.Configuration,
"MySection", "appsettings.custom.json");
In my Controller class I can just demand an IWritableOptions<CustomizableOptions> instance, that has the same characteristics as IOptions<T>, but also allows to change and store configuration values.
private IWritableOptions<CustomizableOptions> options;
...
this.options.Update((opt) => {
opt.SampleOption = "...";
});
Simplified version of Matze's answer:
public interface IWritableOptions<out T> : IOptionsSnapshot<T> where T : class, new()
{
void Update(Action<T> applyChanges);
}
public class WritableOptions<T> : IWritableOptions<T> where T : class, new()
{
private readonly IHostingEnvironment _environment;
private readonly IOptionsMonitor<T> _options;
private readonly string _section;
private readonly string _file;
public WritableOptions(
IHostingEnvironment environment,
IOptionsMonitor<T> options,
string section,
string file)
{
_environment = environment;
_options = options;
_section = section;
_file = file;
}
public T Value => _options.CurrentValue;
public T Get(string name) => _options.Get(name);
public void Update(Action<T> applyChanges)
{
var fileProvider = _environment.ContentRootFileProvider;
var fileInfo = fileProvider.GetFileInfo(_file);
var physicalPath = fileInfo.PhysicalPath;
var jObject = JsonConvert.DeserializeObject<JObject>(File.ReadAllText(physicalPath));
var sectionObject = jObject.TryGetValue(_section, out JToken section) ?
JsonConvert.DeserializeObject<T>(section.ToString()) : (Value ?? new T());
applyChanges(sectionObject);
jObject[_section] = JObject.Parse(JsonConvert.SerializeObject(sectionObject));
File.WriteAllText(physicalPath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
}
}
public static class ServiceCollectionExtensions
{
public static void ConfigureWritable<T>(
this IServiceCollection services,
IConfigurationSection section,
string file = "appsettings.json") where T : class, new()
{
services.Configure<T>(section);
services.AddTransient<IWritableOptions<T>>(provider =>
{
var environment = provider.GetService<IHostingEnvironment>();
var options = provider.GetService<IOptionsMonitor<T>>();
return new WritableOptions<T>(environment, options, section.Key, file);
});
}
}
Usage:
services.ConfigureWritable<MyOptions>(Configuration.GetSection("MySection"));
Then:
private readonly IWritableOptions<MyOptions> _options;
public MyClass(IWritableOptions<MyOptions> options)
{
_options = options;
}
To save the changes to the file:
_options.Update(opt => {
opt.Field1 = "value1";
opt.Field2 = "value2";
});
And you can pass a custom json file as optional parameter (it will use appsettings.json by default):
services.ConfigureWritable<MyOptions>(Configuration.GetSection("MySection"), "appsettings.custom.json");
public static void SetAppSettingValue(string key, string value, string appSettingsJsonFilePath = null) {
if (appSettingsJsonFilePath == null) {
appSettingsJsonFilePath = System.IO.Path.Combine(System.AppContext.BaseDirectory, "appsettings.json");
}
var json = System.IO.File.ReadAllText(appSettingsJsonFilePath);
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject < Newtonsoft.Json.Linq.JObject > (json);
jsonObj[key] = value;
string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
System.IO.File.WriteAllText(appSettingsJsonFilePath, output);
}
I see a lot of answers use Newtonsoft.Json package to update appsettings. I will provide some some solutions that use System.Text.Json package (built-in on .Net Core 3 and above).
OPTION 1
Before you start updating appsettings.json file dynamically, ask yourself a question, how comlex is that part of appsettings.json that needs to be updated. If the part that needs to be updated is not very complex, you can use appsettings transformation functionality just for that part that that needs to be updated. Here's an example:
Let's say my appsettings.json file looks like that:
{
"Username": "Bro300",
"Job": {
"Title": "Programmer",
"Type": "IT"
}
}
And let's say I need to update only Job section. Instead of updating appsettings.json directly I can create a smaller file appsettings.MyOverrides.json that will look like this:
{
"Job": {
"Title": "Farmer",
"Type": "Agriculture"
}
}
And then make sure that this new file is added in my .Net Core app, and .Net Core will figure out how to load the new updated settings.
Now the next step is to create a wrapper class that will hold values from appsettings.MyOverrides.json like this:
public class OverridableSettings
{
public JobSettings Job { get; set; }
}
public class JobSettings
{
public string Title { get; set; }
public string Type { get; set; }
}
And then I can create my updater class that will look like this (notice that it takes in OverridableSettings and completely overrides appsettings.MyOverrides.json file:
public class AppSettingsUpdater
{
public void UpdateSettings(OverridableSettings settings)
{
// instead of updating appsettings.json file directly I will just write the part I need to update to appsettings.MyOverrides.json
// .Net Core in turn will read my overrides from appsettings.MyOverrides.json file
const string SettinsgOverridesFileName = "appsettings.MyOverrides.json";
var newConfig = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(SettinsgOverridesFileName, newConfig);
}
}
Finally this is the code that demonstrates how to use it:
public static class Program
{
public static void Main()
{
// Notice that appsettings.MyOverrides.json will contain only the part that we need to update, other settings will live in appsettings.json
// Also appsettings.MyOverrides.json is optional so if it doesn't exist at the program start it's not a problem
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.MyOverrides.json", optional: true)
.Build();
// Here we read our current settings
var settings = configuration.Get<OverridableSettings>();
var settingsUpdater = new AppSettingsObjectUpdater();
settings.Job.Title = "Farmer";
settings.Job.Type = "Agriculture";
settingsUpdater.UpdateSettings(settings);
// Here we reload the settings so the new values from appsettings.MyOverrides.json will be read
configuration.Reload();
// and here we retrieve the new updated settings
var newJobSettings = configuration.GetSection("Job").Get<JobSettings>();
}
}
OPTION 2
If the appsetting transformation does not fit you case, and you have to update values only one level deep, you can use this simple implementation:
public void UpdateAppSetting(string key, string value)
{
var configJson = File.ReadAllText("appsettings.json");
var config = JsonSerializer.Deserialize<Dictionary<string, object>>(configJson);
config[key] = value;
var updatedConfigJson = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText("appsettings.json", updatedConfigJson);
}
OPTION 3
Finally, if you have some complex case and you need to update appsettings, multiple levels deep, here is another implementation, that expands on the previous option, and uses recursion to update the settings at any level:
public class AppSettingsUpdater
{
private const string EmptyJson = "{}";
public void UpdateAppSetting(string key, object value)
{
// Empty keys "" are allowed in json by the way
if (key == null)
{
throw new ArgumentException("Json property key cannot be null", nameof(key));
}
const string settinsgFileName = "appsettings.json";
// We will create a new file if appsettings.json doesn't exist or was deleted
if (!File.Exists(settinsgFileName))
{
File.WriteAllText(settinsgFileName, EmptyJson);
}
var config = File.ReadAllText(settinsgFileName);
var updatedConfigDict = UpdateJson(key, value, config);
// After receiving the dictionary with updated key value pair, we serialize it back into json.
var updatedJson = JsonSerializer.Serialize(updatedConfigDict, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(settinsgFileName, updatedJson);
}
// This method will recursively read json segments separated by semicolon (firstObject:nestedObject:someProperty)
// until it reaches the desired property that needs to be updated,
// it will update the property and return json document represented by dictonary of dictionaries of dictionaries and so on.
// This dictionary structure can be easily serialized back into json
private Dictionary<string, object> UpdateJson(string key, object value, string jsonSegment)
{
const char keySeparator = ':';
var config = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonSegment);
var keyParts = key.Split(keySeparator);
var isKeyNested = keyParts.Length > 1;
if (isKeyNested)
{
var firstKeyPart = keyParts[0];
var remainingKey = string.Join(keySeparator, keyParts.Skip(1));
// If the key does not exist already, we will create a new key and append it to the json
var newJsonSegment = config.ContainsKey(firstKeyPart) && config[firstKeyPart] != null
? config[firstKeyPart].ToString()
: EmptyJson;
config[firstKeyPart] = UpdateJson(remainingKey, value, newJsonSegment);
}
else
{
config[key] = value;
}
return config;
}
}
You can use, like this:
var settingsUpdater = new AppSettingsUpdater();
settingsUpdater.UpdateAppSetting("OuterProperty:NestedProperty:PropertyToUpdate", "new value");
Update value through this code
it's simply run console application that reads application settings, adds a new setting, and updates an existing setting. and after update refresh the application on server without closed application.
For more information: See Microsoft .Net Docs, ConfigurationManager.AppSettings Property
static void AddUpdateAppSettings(string key, string value)
{
try
{
var configFile = System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("~");
var settings = configFile.AppSettings.Settings;
if (settings[key] == null)
{
settings.Add(key, value);
}
else
{
settings[key].Value = value;
}
configFile.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection(configFile.AppSettings.SectionInformation.Name);
}
catch (ConfigurationErrorsException ex)
{
Console.WriteLine("Error writing app settings. Error: "+ ex.Message);
}
}
While there is still not a way via the Options accessor, I'd like to preset a .NET 6 class that makes it quite easy to write back to the file. You can use the JsonNode class in the System.Text.Json.Nodes class. I'm using it to write back an encrypted connection string after reading a plain text one from appsettings.json.
There are examples of using Newtonsoft.Json.JsonConvert.DeserializeObject and deserializing into a dynamic type like #Alper suggested - but System.Text.Json could not do that. Well, now you sort of can :) (though not with a dynamic type).
In my example below, I tried to be minimalistic and simple. I used JsonNode to retrieve the value instead of a Dependency Injected IConfiguration. In a real web application, I'd be using the DI method. It really doesn't matter how you retrieve the setting, writing it back still means reconstructing the Json and updating the file on disk.
MS Link for JsonNode: https://learn.microsoft.com/en-us/dotnet/api/system.text.json.nodes.jsonnode?view=net-6.0
My appsettings.json sample:
{
"sampleSection": {
"someStringSetting": "Value One",
"deeperValues": {
"someIntSetting": 23,
"someBooleanSetting": true
}
}
}
C# .NET 6 console application:
using System.Text.Json;
using System.Text.Json.Nodes;
const string AppSettingsPath = #"<PathToYourAppSettings.JsonFile>>\appsettings.json";
string appSettingsJson = File.ReadAllText(AppSettingsPath);
var jsonNodeOptions = new JsonNodeOptions { PropertyNameCaseInsensitive = true };
var node = JsonNode.Parse(appSettingsJson, jsonNodeOptions);
var options = new JsonSerializerOptions { WriteIndented = true };
Console.WriteLine("=========== Before ============");
Console.WriteLine(node.ToJsonString(options));
// Now you have access to all the structure using node["blah"] syntax
var stringSetting = (string) node["sampleSection"]["someStringSetting"];
var intSetting = (int) node["sampleSection"]["deeperValues"]["someIntSetting"];
var booleanSetting = (bool) node["sampleSection"]["deeperValues"]["someBooleanSetting"];
Console.WriteLine($"stringSetting: {stringSetting}, intSetting: {intSetting}, booleanSetting: {booleanSetting}");
// Now write new values back
node["sampleSection"]["someStringSetting"] = $"New setting at {DateTimeOffset.Now}";
node["sampleSection"]["deeperValues"]["someIntSetting"] = -6;
node["sampleSection"]["deeperValues"]["someBooleanSetting"] = false;
Console.WriteLine("=========== After ============");
Console.WriteLine(node.ToJsonString(options));
// Or, to actually write it to disk:
// File.WriteAllText(AppSettingsPath, node.ToJsonString(options));
I hope that my scenario covers your intent, I wanted to override the appsettings.json values if there are environment variables passed to the app at startup.
I made use of the ConfigureOptions method that is available in dotnet core 2.1.
Here is the Model that is used for the JSON from appsettings.json
public class Integration
{
public string FOO_API {get;set;}
}
For the services in the statup.cs:
var section = Configuration.GetSection ("integration");
services.Configure<Integration> (section);
services.ConfigureOptions<ConfigureIntegrationSettings>();
Here is the implemenation:
public class ConfigureIntegrationSettings : IConfigureOptions<Integration>
{
public void Configure(Integration options)
{
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FOO")))
options.FOO_API = Environment.GetEnvironmentVariable("FOO_API");
}
}
so if there is no value set it falls back to the appsettings.json
I solved similar problem - I needed override appSettings like this:
For 'IConfigurationBuilder':
configurationBuilder
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{environmentName}.json", false, true)
.AddConfigurationObject(TenantsTimeZoneConfigurationOverrides(configurationBuilder)); // Override Tenants TimeZone configuration due the OS platform (https://dejanstojanovic.net/aspnet/2018/july/differences-in-time-zones-in-net-core-on-windows-and-linux-host-os/)
private static Dictionary<string, string> TenantsTimeZoneConfigurationOverrides(IConfigurationBuilder configurationBuilder)
{
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var overridesDictionary = new Dictionary<string, string>();
var configuration = configurationBuilder.Build() as IConfiguration;
var tenantsSection = configuration.GetSection(TenantsConfig.TenantsCollectionConfigSectionName).Get<Tenants>();
foreach (var tenant in tenantsSection)
{
if (!string.IsNullOrEmpty(tenant.Value.TimeZone))
{
overridesDictionary.Add($"Tenants:{tenant.Key}:TimeZone", GetSpecificTimeZoneDueOsPlatform(isWindows, tenant.Value.TimeZone));
}
}
return overridesDictionary;
}
private static string GetSpecificTimeZoneDueOsPlatform(bool isWindows, string timeZone)
{
return isWindows ? timeZone : TZConvert.WindowsToIana(timeZone);
}
I'm trying to bind appsettings file into a generic class, but currently I can't map List<object> values.
I have an appsettings.Development.json hierarchy like this:
{
"AppSettings":{
"ApplicationName":"FOO"
"MyValues":[
{
"Name":"Tryout1",
"QuestionCount": 7
},
{
"Name":"Tryout2"
"SettingName":"ABCDEFG"
}
]
}
}
And my generic class is like this:
public class AppSettings
{
public string ApplicationName {get;set;}
public List<object> MyValues {get;set;}
}
When I use below code to bind this appsettings.Development.json into the generic class like this:
static ConfigurationHelper()
{
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile($"appsettings.{environmentName}.json")
.Build();
try
{
var tryout = config.Get(typeof(AppSettings));
}
catch (Exception ex)
{
throw ex;
}
}
tryout variable has the ApplicationName filled with value foo but the MyValues is just an empty array.
I also tried using object[] instead of List<object> and at that point it recognizes the amount of objects but all of the elements have the value null.
I don't want the MyValues array to be pre-defined because every element can and will have different fields and I want to make this process as generic as possible. How can I achieve this?
So, I found a way of achieving my goal, just not that straightforward but still pretty simple:
First I had to change my appsettings.Development.json file like this:
{
"AppSettings":{
"ApplicationName":"FOO"
"MyValues":[
"Tryout1": {
"Name":"Tryout1",
"QuestionCount": 7
},
"Tryout2": {
"Name":"Tryout2"
"SettingName":"ABCDEFG"
}
]
}
}
After this change, another change in the generic type is necessary, as follows:
public class AppSettings
{
public string ApplicationName {get;set;}
public Dictionary<string, CustomBaseClass> MyValues {get;set;}
public List<object> _MyValues {get;set;}
}
public class CustomBaseClass
{
public string Name { get; set; }
}
public class Tryout1 : CustomBaseClass
{
public int QuestionCount {get;set;}
}
public class Tryout2 : CustomBaseClass
{
public string SettingName {get;set;}
}
This base class is important because we can't really bind the appsettings to an object, we still need a class. And base class, helps us bind these custom values like this:
private static List<Type> CustomTypes = new List<Type> { typeof(Tryout1), typeof(Tryout2) };
static ConfigurationHelper()
{
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile($"appsettings.{environmentName}.json")
.Build();
try
{
AppSettings = config.Get<AppSettingsContainer>();
foreach (var keyValuePair in AppSettings.MyValues)
{
var type = CustomTypes.Where(t => t.Name == keyValuePair.Key).FirstOrDefault();
if (type != null)
{
var myValue = config.GetSection(string.Format("AppSettings:MyValues:{0}", keyValuePair.Key)).Get(type);
if (job != null)
{
AppSettings._MyValues.Add(myValue);
}
}
}
}
catch (Exception ex)
{
throw ex;
}
}
This should solve this problem, and huge thanks to andrewlock.net because I got the inital Dictionary<string, T> idea from there.
I am exploring Function App running on .net5 in the new isolated mode. I have HTTP triggered functions that I want to advertise via OpenAPI / Swagger.
To do so, I am using the package Microsoft.Azure.WebJobs.Extensions.OpenApi in preview (0.7.2) to add the OpenAPI functionality to my Function App.
I am trying to have the enums to be shown as string in the OpenAPI page but I can't have it working properly.
Here is the setup in the Program.cs file:
public static class Program
{
private static Task Main(string[] args)
{
IHost host = new HostBuilder()
.ConfigureAppConfiguration(configurationBuilder =>
{
configurationBuilder.AddCommandLine(args);
})
.ConfigureFunctionsWorkerDefaults(builder =>
{
builder.Services.Configure<JsonSerializerOptions>(options =>
{
options.Converters.Add(new JsonStringEnumConverter());
options.PropertyNameCaseInsensitive = true;
});
})
.ConfigureServices(services =>
{
// Registers any services.
})
.Build();
return host.RunAsync();
}
}
Here is the enum:
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ApprovalContract
{
[EnumMember(Value = "Approved")]
Approved = 1,
[EnumMember(Value = "Rejected")]
Rejected = 2
}
And one of the class that uses it:
public sealed class DeletionResponseContract
{
[JsonPropertyName("approval")]
public ApprovalContract Approval { get; set; }
}
I replaced any references to Newtonsoft.Json by System.Text.Json everywhere.
Here is the output in the Swagger page:
Question
How can I serialize enum as string instead of int in the Swagger page with an HTTP triggered Azure Function running on .net5?
Update
I saw that the JsonStringEnumConverter's constructor gives the indication to allow integer values:
public JsonStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true)
{
this._namingPolicy = namingPolicy;
this._converterOptions = allowIntegerValues ? EnumConverterOptions.AllowStrings | EnumConverterOptions.AllowNumbers : EnumConverterOptions.AllowStrings;
}
I modified my configuration like this, without any success:
builder.Services.Configure<JsonSerializerOptions>(options =>
{
options.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false));
options.PropertyNameCaseInsensitive = true;
});
You must implemente ISchemaFilter and set it on AddSwaggerGen. It will generate a better description of your enum items.
builder.Services.AddSwaggerGen(c =>
{
c.SchemaFilter<EnumSchemaFilter>();
});
//your implementation
public class EnumSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
if (context.Type.IsEnum)
{
model.Enum.Clear();
Enum.GetNames(context.Type)
.ToList()
.ForEach(name => model.Enum.Add(new OpenApiString($"{Convert.ToInt64(Enum.Parse(context.Type, name))} - {name}")));
}
}
}
I was inspired by Murilo's answer, but couldn't get it to work. So here is an alternative solution:
Create a document filter that will:
find all the enum properties in you swagger schemas
then find the matching property in your code using reflection (note: this doesn't take account of namespaces, so if you have multiple classes with the same name it could fail)
update the swagger property with the values from the c# enum
public class EnumDocumentFilter : IDocumentFilter
{
public void Apply(IHttpRequestDataObject req, OpenApiDocument document)
{
foreach(var schema in document.Components.Schemas)
foreach(var property in schema.Value.Properties)
if (property.Value.Enum.Any())
{
var schemaType = Assembly.GetExecutingAssembly().GetTypes().Single(t => t.Name == Camel(schema.Key));
var propertyType = schemaType.GetProperty(Camel(property.Key)).PropertyType;
property.Value.Enum = Enum.GetNames(propertyType)
.Select(name => new OpenApiString(name))
.Cast<IOpenApiAny>()
.ToList();
property.Value.Type = "string";
property.Value.Default = property.Value.Enum.First();
property.Value.Format = null;
}
}
private static string Camel(string key)
=> $"{char.ToUpperInvariant(key[0])}{key[1..]}";
}
Then register that filter in your OpenApiConfigurationOptions
public class OpenApiConfigurationOptions : DefaultOpenApiConfigurationOptions
{
...
public override List<IDocumentFilter> DocumentFilters { get => base.DocumentFilters.Append(new EnumDocumentFilter()).ToList(); }
}
Thanks Alan Hinton for your answer, I was able to get the custom document filter working using reflection.
The problem:
The enums were auto generated and I cannot keep adding StringEnumCovertor attribute each time the code was refreshed.
Auto-generated code:
public enum Status
{
[System.Runtime.Serialization.EnumMember(Value = #"new")]
New = 0,
[System.Runtime.Serialization.EnumMember(Value = #"confirmed")]
Confirmed = 1,
[System.Runtime.Serialization.EnumMember(Value = #"processing")]
Processing = 2
}
public partial class Order
{
/// <summary>Status</summary>
[Newtonsoft.Json.JsonProperty("status", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public Status Status { get; set; }
}
Solution:
public OpenApiConfigurationOptions()
{
DocumentFilters.Add(new OpenApiEnumAsStringsDocumentFilter());
}
public class OpenApiEnumAsStringsDocumentFilter : IDocumentFilter
{
private const string YourNamespace = "your.namespace";
private const string EnumDefaultMemberValue = "value__";
private const string StringSchemaType = "string";
public void Apply(IHttpRequestDataObject request, OpenApiDocument document)
{
var assemblyTypes = Assembly
.GetExecutingAssembly()
.GetTypes()
.Where(x => !string.IsNullOrEmpty(x.FullName) && x.FullName.StartsWith(YourNamespace, StringComparison.InvariantCulture));
// Loop all DTO classes
foreach (var schema in document.Components.Schemas)
{
foreach (var property in schema.Value.Properties)
{
if (property.Value.Enum.Any())
{
var schemaType = assemblyTypes.SingleOrDefault(t => t.Name.Equals(schema.Key, StringComparison.InvariantCultureIgnoreCase));
if (schemaType == null)
continue;
var enumType = schemaType.GetProperty(string.Concat(property.Key[0].ToString().ToUpper(), property.Key.AsSpan(1))).PropertyType;
UpdateEnumValuesAsString(property.Value, enumType);
}
}
}
// Loop all request parameters
foreach (var path in document.Paths)
{
foreach (var operation in path.Value.Operations)
{
foreach (var parameter in operation.Value.Parameters)
{
if (parameter.Schema.Enum.Any())
{
var enumType = assemblyTypes.SingleOrDefault(t => t.Name.Equals(parameter.Name, StringComparison.InvariantCultureIgnoreCase));
if (enumType == null)
continue;
UpdateEnumValuesAsString(parameter.Schema, enumType);
}
}
}
}
}
private static void UpdateEnumValuesAsString(OpenApiSchema schema, Type enumType)
{
schema.Enum.Clear();
enumType
.GetTypeInfo()
.DeclaredMembers
.Where(m => !m.Name.Equals(EnumDefaultMemberValue, StringComparison.InvariantCulture))
.ToList()
.ForEach(m =>
{
var attribute = m.GetCustomAttribute<EnumMemberAttribute>(false);
schema.Enum.Add(new OpenApiString(attribute.Value));
});
schema.Type = StringSchemaType;
schema.Default = schema.Enum.FirstOrDefault();
schema.Format = null;
}
}
According to the Microsoft.Azure.WebJobs.Extensions.OpenApi.Core documentation you should be able to set the [JsonConverter(typeof(StringEnumConverter))] (with the Newtonsoft package) attribute on the enum to trigger the usage of strings in the Swagger.
I had issues however that the OpenAPI document still didn't show the enum as strings, and I believe the issue is related some compatibility between Newtonsoft version 13.0.1 (which is dependency for my project) and Azure Function Core Tools (AFCT) v. 3.41. It is anyway solved when either downgrading Newtonsoft to 12.0.3 or lower OR upgrading the project to use Azure Functions V4 and thus also Azure Function Core Tools v. 4.x.x.
The reason I suspect the Azure Function Core Tools to be the cause and not something else related to the Azure Function version, is that AFCT loads Newtonsoft assembly 12.0.0.0 when you start it, and if you're using Newtonsoft 12.0.3 in the project, the same assembly may be used by Microsoft.Azure.WebJobs.Extensions.OpenApi.Core. But if the project uses 13.0.1, it refers to assembly version 13.0.0.0, which is loaded by Microsoft.Azure.WebJobs.Extensions.OpenApi.Core alongside the 12.0.0.0 assembly. This mismatch in versions could be why the attribute isn't working as expected.
Is there any way how to read polymorphic objects from appsettings.json in a strongly-typed way? Below is a very simplified example of what I need.
I have multiple app components, named Features here. These components are created in runtime by a factory. My design intent is that each component is configured by its separate strongly-typed options. In this example FileSizeCheckerOptions and PersonCheckerOption are instances of these. Each feature can be included multiple times with different option.
But with the existing ASP.NET Core configuration system, I am not able to read polymorphic strongly typed options. If the settings were read by a JSON deserializer, I could use something like this. But this is not the case of appsettings.json, where options are just key-value pairs.
appsettings.json
{
"DynamicConfig":
{
"Features": [
{
"Type": "FileSizeChecker",
"Options": { "MaxFileSize": 1000 }
},
{
"Type": "PersonChecker",
"Options": {
"MinAge": 10,
"MaxAge": 99
}
},
{
"Type": "PersonChecker",
"Options": {
"MinAge": 15,
"MaxAge": 20
}
}
]
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FeaturesOptions>(Configuration.GetSection("DynamicConfig"));
ServiceProvider serviceProvider = services.BuildServiceProvider();
// try to load settings in strongly typed way
var options = serviceProvider.GetRequiredService<IOptions<FeaturesOptions>>().Value;
}
Other definitions
public enum FeatureType
{
FileSizeChecker,
PersonChecker
}
public class FeaturesOptions
{
public FeatureConfig[] Features { get; set; }
}
public class FeatureConfig
{
public FeatureType Type { get; set; }
// cannot read polymorphic object
// public object Options { get; set; }
}
public class FileSizeCheckerOptions
{
public int MaxFileSize { get; set; }
}
public class PersonCheckerOption
{
public int MinAge { get; set; }
public int MaxAge { get; set; }
}
The key to answer this question is to know how the keys are generated. In your case, the key / value pairs will be:
DynamicConfig:Features:0:Type
DynamicConfig:Features:0:Options:MaxFileSize
DynamicConfig:Features:1:Type
DynamicConfig:Features:1:Options:MinAge
DynamicConfig:Features:1:Options:MaxAge
DynamicConfig:Features:2:Type
DynamicConfig:Features:2:Options:MinAge
DynamicConfig:Features:2:Options:MaxAge
Notice how each element of the array is represented by DynamicConfig:Features:{i}.
The second thing to know is that you can map any section of a configuration to an object instance, with the ConfigurationBinder.Bind method:
var conf = new PersonCheckerOption();
Configuration.GetSection($"DynamicConfig:Features:1:Options").Bind(conf);
When we put all this together, we can map your configuration to your data structure:
services.Configure<FeaturesOptions>(opts =>
{
var features = new List<FeatureConfig>();
for (var i = 0; ; i++)
{
// read the section of the nth item of the array
var root = $"DynamicConfig:Features:{i}";
// null value = the item doesn't exist in the array => exit loop
var typeName = Configuration.GetValue<string>($"{root}:Type");
if (typeName == null)
break;
// instantiate the appropriate FeatureConfig
FeatureConfig conf = typeName switch
{
"FileSizeChecker" => new FileSizeCheckerOptions(),
"PersonChecker" => new PersonCheckerOption(),
_ => throw new InvalidOperationException($"Unknown feature type {typeName}"),
};
// bind the config to the instance
Configuration.GetSection($"{root}:Options").Bind(conf);
features.Add(conf);
}
opts.Features = features.ToArray();
});
Note: all options must derive from FeatureConfig for this to work (e.g. public class FileSizeCheckerOptions : FeatureConfig). You could even use reflection to automatically detect all the options inheriting from FeatureConfig, to avoid the switch over the type name.
Note 2: you can also map your configuration to a Dictionary, or a dynamic object if you prefer; see my answer to Bind netcore IConfigurationSection to a dynamic object.
Based on Metoule answer, I've created reusable extension method, that accepts delegate that accepts section and returns instance to bind to.
Please note that not all edge cases are handled (e.g. Features must be list, not array).
public class FeaturesOptions
{
public List<FeatureConfigOptions> Features { get; set; }
}
public abstract class FeatureConfigOptions
{
public string Type { get; set; }
}
public class FileSizeCheckerOptions : FeatureConfigOptions
{
public int MaxFileSize { get; set; }
}
public class PersonCheckerOptions : FeatureConfigOptions
{
public int MinAge { get; set; }
public int MaxAge { get; set; }
}
FeaturesOptions options = new FeaturesOptions();
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("path-to-the-appsettings.json")
.Build();
configuration.Bind(options, (propertyType, section) =>
{
string type = section.GetValue<string>("Type");
switch (type)
{
case "FileSizeChecker": return new FileSizeCheckerOptions();
case "PersonChecker": return new PersonCheckerOptions();
default: throw new InvalidOperationException($"Unknown feature type {type}"); // or you can return null to skip the binding.
};
});
appsettings.json
{
"Features":
[
{
"Type": "FileSizeChecker",
"MaxFileSize": 1000
},
{
"Type": "PersonChecker",
"MinAge": 10,
"MaxAge": 99
},
{
"Type": "PersonChecker",
"MinAge": 15,
"MaxAge": 20
}
]
}
IConfigurationExtensions.cs
using System.Collections;
namespace Microsoft.Extensions.Configuration
{
/// <summary>
/// </summary>
/// <param name="requestedType">Abstract type or interface that is about to be bound.</param>
/// <param name="configurationSection">Configuration section to be bound from.</param>
/// <returns>Instance of object to be used for binding, or <c>null</c> if section should not be bound.</returns>
public delegate object? ObjectFactory(Type requestedType, IConfigurationSection configurationSection);
public static class IConfigurationExtensions
{
public static void Bind(this IConfiguration configuration, object instance, ObjectFactory objectFactory)
{
if (configuration is null)
throw new ArgumentNullException(nameof(configuration));
if (instance is null)
throw new ArgumentNullException(nameof(instance));
if (objectFactory is null)
throw new ArgumentNullException(nameof(objectFactory));
// first, bind all bindable instance properties.
configuration.Bind(instance);
// then scan for all interfaces or abstract types
foreach (var property in instance.GetType().GetProperties())
{
var propertyType = property.PropertyType;
if (propertyType.IsPrimitive || propertyType.IsValueType || propertyType.IsEnum || propertyType == typeof(string))
continue;
var propertySection = configuration.GetSection(property.Name);
if (!propertySection.Exists())
continue;
object? propertyValue;
if (propertyType.IsAbstract || propertyType.IsInterface)
{
propertyValue = CreateAndBindValueForAbstractPropertyTypeOrInterface(propertyType, objectFactory, propertySection);
property.SetValue(instance, propertyValue);
}
else
{
propertyValue = property.GetValue(instance);
}
if (propertyValue is null)
continue;
var isGenericList = propertyType.IsAssignableTo(typeof(IList)) && propertyType.IsGenericType;
if (isGenericList)
{
var listItemType = propertyType.GenericTypeArguments[0];
if (listItemType.IsPrimitive || listItemType.IsValueType || listItemType.IsEnum || listItemType == typeof(string))
continue;
if (listItemType.IsAbstract || listItemType.IsInterface)
{
var newListPropertyValue = (IList)Activator.CreateInstance(propertyType)!;
for (int i = 0; ; i++)
{
var listItemSection = propertySection.GetSection(i.ToString());
if (!listItemSection.Exists())
break;
var listItem = CreateAndBindValueForAbstractPropertyTypeOrInterface(listItemType, objectFactory, listItemSection);
if (listItem is not null)
newListPropertyValue.Add(listItem);
}
property.SetValue(instance, newListPropertyValue);
}
else
{
var listPropertyValue = (IList)property.GetValue(instance, null)!;
for (int i = 0; i < listPropertyValue.Count; i++)
{
var listItem = listPropertyValue[i];
if (listItem is not null)
{
var listItemSection = propertySection.GetSection(i.ToString());
listItemSection.Bind(listItem, objectFactory);
}
}
}
}
else
{
propertySection.Bind(propertyValue, objectFactory);
}
}
}
private static object? CreateAndBindValueForAbstractPropertyTypeOrInterface(Type abstractPropertyType, ObjectFactory objectFactory, IConfigurationSection section)
{
if (abstractPropertyType is null)
throw new ArgumentNullException(nameof(abstractPropertyType));
if (objectFactory is null)
throw new ArgumentNullException(nameof(objectFactory));
if (section is null)
throw new ArgumentNullException(nameof(section));
var propertyValue = objectFactory(abstractPropertyType, section);
if (propertyValue is not null)
section.Bind(propertyValue, objectFactory);
return propertyValue;
}
}
}
I am using the IOptions pattern as described in the official documentation.
This works fine when I am reading values from appsetting.json, but how do I update values and save changes back to appsetting.json?
In my case, I have a few fields that can be edited from the user interface (by admin user in application). Hence I am looking for the ideal approach to update these values via the option accessor.
At the time of writing this answer it seemed that there is no component provided by the Microsoft.Extensions.Options package that has functionality to write configuration values back to appsettings.json.
In one of my ASP.NET Core projects I wanted to enable the user to change some application settings - and those setting values should be stored in appsettings.json, more precisly in an optional appsettings.custom.json file, that gets added to the configuration if present.
Like this...
public Startup(IHostingEnvironment env)
{
IConfigurationBuilder builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile("appsettings.custom.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
this.Configuration = builder.Build();
}
I declared the IWritableOptions<T> interface that extends IOptions<T>; so I can just replace IOptions<T> by IWritableOptions<T> whenever I want to read and write settings.
public interface IWritableOptions<out T> : IOptions<T> where T : class, new()
{
void Update(Action<T> applyChanges);
}
Also, I came up with IOptionsWriter, which is a component that is intended to be used by IWritableOptions<T> to update a configuration section. This is my implementation for the beforementioned interfaces...
class OptionsWriter : IOptionsWriter
{
private readonly IHostingEnvironment environment;
private readonly IConfigurationRoot configuration;
private readonly string file;
public OptionsWriter(
IHostingEnvironment environment,
IConfigurationRoot configuration,
string file)
{
this.environment = environment;
this.configuration = configuration;
this.file = file;
}
public void UpdateOptions(Action<JObject> callback, bool reload = true)
{
IFileProvider fileProvider = this.environment.ContentRootFileProvider;
IFileInfo fi = fileProvider.GetFileInfo(this.file);
JObject config = fileProvider.ReadJsonFileAsObject(fi);
callback(config);
using (var stream = File.OpenWrite(fi.PhysicalPath))
{
stream.SetLength(0);
config.WriteTo(stream);
}
this.configuration.Reload();
}
}
Since the writer is not aware about the file structure, I decided to handle sections as JObject objects. The accessor tries to find the requested section and deserializes it to an instance of T, uses the current value (if not found), or just creates a new instance of T, if the current value is null. This holder object is than passed to the caller, who will apply the changes to it. Than the changed object gets converted back to a JToken instance that is going to replace the section...
class WritableOptions<T> : IWritableOptions<T> where T : class, new()
{
private readonly string sectionName;
private readonly IOptionsWriter writer;
private readonly IOptionsMonitor<T> options;
public WritableOptions(
string sectionName,
IOptionsWriter writer,
IOptionsMonitor<T> options)
{
this.sectionName = sectionName;
this.writer = writer;
this.options = options;
}
public T Value => this.options.CurrentValue;
public void Update(Action<T> applyChanges)
{
this.writer.UpdateOptions(opt =>
{
JToken section;
T sectionObject = opt.TryGetValue(this.sectionName, out section) ?
JsonConvert.DeserializeObject<T>(section.ToString()) :
this.options.CurrentValue ?? new T();
applyChanges(sectionObject);
string json = JsonConvert.SerializeObject(sectionObject);
opt[this.sectionName] = JObject.Parse(json);
});
}
}
Finally, I implemented an extension method for IServicesCollection allowing me to easily configure a writable options accessor...
static class ServicesCollectionExtensions
{
public static void ConfigureWritable<T>(
this IServiceCollection services,
IConfigurationRoot configuration,
string sectionName,
string file) where T : class, new()
{
services.Configure<T>(configuration.GetSection(sectionName));
services.AddTransient<IWritableOptions<T>>(provider =>
{
var environment = provider.GetService<IHostingEnvironment>();
var options = provider.GetService<IOptionsMonitor<T>>();
IOptionsWriter writer = new OptionsWriter(environment, configuration, file);
return new WritableOptions<T>(sectionName, writer, options);
});
}
}
Which can be used in ConfigureServices like...
services.ConfigureWritable<CustomizableOptions>(this.Configuration,
"MySection", "appsettings.custom.json");
In my Controller class I can just demand an IWritableOptions<CustomizableOptions> instance, that has the same characteristics as IOptions<T>, but also allows to change and store configuration values.
private IWritableOptions<CustomizableOptions> options;
...
this.options.Update((opt) => {
opt.SampleOption = "...";
});
Simplified version of Matze's answer:
public interface IWritableOptions<out T> : IOptionsSnapshot<T> where T : class, new()
{
void Update(Action<T> applyChanges);
}
public class WritableOptions<T> : IWritableOptions<T> where T : class, new()
{
private readonly IHostingEnvironment _environment;
private readonly IOptionsMonitor<T> _options;
private readonly string _section;
private readonly string _file;
public WritableOptions(
IHostingEnvironment environment,
IOptionsMonitor<T> options,
string section,
string file)
{
_environment = environment;
_options = options;
_section = section;
_file = file;
}
public T Value => _options.CurrentValue;
public T Get(string name) => _options.Get(name);
public void Update(Action<T> applyChanges)
{
var fileProvider = _environment.ContentRootFileProvider;
var fileInfo = fileProvider.GetFileInfo(_file);
var physicalPath = fileInfo.PhysicalPath;
var jObject = JsonConvert.DeserializeObject<JObject>(File.ReadAllText(physicalPath));
var sectionObject = jObject.TryGetValue(_section, out JToken section) ?
JsonConvert.DeserializeObject<T>(section.ToString()) : (Value ?? new T());
applyChanges(sectionObject);
jObject[_section] = JObject.Parse(JsonConvert.SerializeObject(sectionObject));
File.WriteAllText(physicalPath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
}
}
public static class ServiceCollectionExtensions
{
public static void ConfigureWritable<T>(
this IServiceCollection services,
IConfigurationSection section,
string file = "appsettings.json") where T : class, new()
{
services.Configure<T>(section);
services.AddTransient<IWritableOptions<T>>(provider =>
{
var environment = provider.GetService<IHostingEnvironment>();
var options = provider.GetService<IOptionsMonitor<T>>();
return new WritableOptions<T>(environment, options, section.Key, file);
});
}
}
Usage:
services.ConfigureWritable<MyOptions>(Configuration.GetSection("MySection"));
Then:
private readonly IWritableOptions<MyOptions> _options;
public MyClass(IWritableOptions<MyOptions> options)
{
_options = options;
}
To save the changes to the file:
_options.Update(opt => {
opt.Field1 = "value1";
opt.Field2 = "value2";
});
And you can pass a custom json file as optional parameter (it will use appsettings.json by default):
services.ConfigureWritable<MyOptions>(Configuration.GetSection("MySection"), "appsettings.custom.json");
public static void SetAppSettingValue(string key, string value, string appSettingsJsonFilePath = null) {
if (appSettingsJsonFilePath == null) {
appSettingsJsonFilePath = System.IO.Path.Combine(System.AppContext.BaseDirectory, "appsettings.json");
}
var json = System.IO.File.ReadAllText(appSettingsJsonFilePath);
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject < Newtonsoft.Json.Linq.JObject > (json);
jsonObj[key] = value;
string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
System.IO.File.WriteAllText(appSettingsJsonFilePath, output);
}
I see a lot of answers use Newtonsoft.Json package to update appsettings. I will provide some some solutions that use System.Text.Json package (built-in on .Net Core 3 and above).
OPTION 1
Before you start updating appsettings.json file dynamically, ask yourself a question, how comlex is that part of appsettings.json that needs to be updated. If the part that needs to be updated is not very complex, you can use appsettings transformation functionality just for that part that that needs to be updated. Here's an example:
Let's say my appsettings.json file looks like that:
{
"Username": "Bro300",
"Job": {
"Title": "Programmer",
"Type": "IT"
}
}
And let's say I need to update only Job section. Instead of updating appsettings.json directly I can create a smaller file appsettings.MyOverrides.json that will look like this:
{
"Job": {
"Title": "Farmer",
"Type": "Agriculture"
}
}
And then make sure that this new file is added in my .Net Core app, and .Net Core will figure out how to load the new updated settings.
Now the next step is to create a wrapper class that will hold values from appsettings.MyOverrides.json like this:
public class OverridableSettings
{
public JobSettings Job { get; set; }
}
public class JobSettings
{
public string Title { get; set; }
public string Type { get; set; }
}
And then I can create my updater class that will look like this (notice that it takes in OverridableSettings and completely overrides appsettings.MyOverrides.json file:
public class AppSettingsUpdater
{
public void UpdateSettings(OverridableSettings settings)
{
// instead of updating appsettings.json file directly I will just write the part I need to update to appsettings.MyOverrides.json
// .Net Core in turn will read my overrides from appsettings.MyOverrides.json file
const string SettinsgOverridesFileName = "appsettings.MyOverrides.json";
var newConfig = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(SettinsgOverridesFileName, newConfig);
}
}
Finally this is the code that demonstrates how to use it:
public static class Program
{
public static void Main()
{
// Notice that appsettings.MyOverrides.json will contain only the part that we need to update, other settings will live in appsettings.json
// Also appsettings.MyOverrides.json is optional so if it doesn't exist at the program start it's not a problem
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.MyOverrides.json", optional: true)
.Build();
// Here we read our current settings
var settings = configuration.Get<OverridableSettings>();
var settingsUpdater = new AppSettingsObjectUpdater();
settings.Job.Title = "Farmer";
settings.Job.Type = "Agriculture";
settingsUpdater.UpdateSettings(settings);
// Here we reload the settings so the new values from appsettings.MyOverrides.json will be read
configuration.Reload();
// and here we retrieve the new updated settings
var newJobSettings = configuration.GetSection("Job").Get<JobSettings>();
}
}
OPTION 2
If the appsetting transformation does not fit you case, and you have to update values only one level deep, you can use this simple implementation:
public void UpdateAppSetting(string key, string value)
{
var configJson = File.ReadAllText("appsettings.json");
var config = JsonSerializer.Deserialize<Dictionary<string, object>>(configJson);
config[key] = value;
var updatedConfigJson = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText("appsettings.json", updatedConfigJson);
}
OPTION 3
Finally, if you have some complex case and you need to update appsettings, multiple levels deep, here is another implementation, that expands on the previous option, and uses recursion to update the settings at any level:
public class AppSettingsUpdater
{
private const string EmptyJson = "{}";
public void UpdateAppSetting(string key, object value)
{
// Empty keys "" are allowed in json by the way
if (key == null)
{
throw new ArgumentException("Json property key cannot be null", nameof(key));
}
const string settinsgFileName = "appsettings.json";
// We will create a new file if appsettings.json doesn't exist or was deleted
if (!File.Exists(settinsgFileName))
{
File.WriteAllText(settinsgFileName, EmptyJson);
}
var config = File.ReadAllText(settinsgFileName);
var updatedConfigDict = UpdateJson(key, value, config);
// After receiving the dictionary with updated key value pair, we serialize it back into json.
var updatedJson = JsonSerializer.Serialize(updatedConfigDict, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(settinsgFileName, updatedJson);
}
// This method will recursively read json segments separated by semicolon (firstObject:nestedObject:someProperty)
// until it reaches the desired property that needs to be updated,
// it will update the property and return json document represented by dictonary of dictionaries of dictionaries and so on.
// This dictionary structure can be easily serialized back into json
private Dictionary<string, object> UpdateJson(string key, object value, string jsonSegment)
{
const char keySeparator = ':';
var config = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonSegment);
var keyParts = key.Split(keySeparator);
var isKeyNested = keyParts.Length > 1;
if (isKeyNested)
{
var firstKeyPart = keyParts[0];
var remainingKey = string.Join(keySeparator, keyParts.Skip(1));
// If the key does not exist already, we will create a new key and append it to the json
var newJsonSegment = config.ContainsKey(firstKeyPart) && config[firstKeyPart] != null
? config[firstKeyPart].ToString()
: EmptyJson;
config[firstKeyPart] = UpdateJson(remainingKey, value, newJsonSegment);
}
else
{
config[key] = value;
}
return config;
}
}
You can use, like this:
var settingsUpdater = new AppSettingsUpdater();
settingsUpdater.UpdateAppSetting("OuterProperty:NestedProperty:PropertyToUpdate", "new value");
Update value through this code
it's simply run console application that reads application settings, adds a new setting, and updates an existing setting. and after update refresh the application on server without closed application.
For more information: See Microsoft .Net Docs, ConfigurationManager.AppSettings Property
static void AddUpdateAppSettings(string key, string value)
{
try
{
var configFile = System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("~");
var settings = configFile.AppSettings.Settings;
if (settings[key] == null)
{
settings.Add(key, value);
}
else
{
settings[key].Value = value;
}
configFile.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection(configFile.AppSettings.SectionInformation.Name);
}
catch (ConfigurationErrorsException ex)
{
Console.WriteLine("Error writing app settings. Error: "+ ex.Message);
}
}
While there is still not a way via the Options accessor, I'd like to preset a .NET 6 class that makes it quite easy to write back to the file. You can use the JsonNode class in the System.Text.Json.Nodes class. I'm using it to write back an encrypted connection string after reading a plain text one from appsettings.json.
There are examples of using Newtonsoft.Json.JsonConvert.DeserializeObject and deserializing into a dynamic type like #Alper suggested - but System.Text.Json could not do that. Well, now you sort of can :) (though not with a dynamic type).
In my example below, I tried to be minimalistic and simple. I used JsonNode to retrieve the value instead of a Dependency Injected IConfiguration. In a real web application, I'd be using the DI method. It really doesn't matter how you retrieve the setting, writing it back still means reconstructing the Json and updating the file on disk.
MS Link for JsonNode: https://learn.microsoft.com/en-us/dotnet/api/system.text.json.nodes.jsonnode?view=net-6.0
My appsettings.json sample:
{
"sampleSection": {
"someStringSetting": "Value One",
"deeperValues": {
"someIntSetting": 23,
"someBooleanSetting": true
}
}
}
C# .NET 6 console application:
using System.Text.Json;
using System.Text.Json.Nodes;
const string AppSettingsPath = #"<PathToYourAppSettings.JsonFile>>\appsettings.json";
string appSettingsJson = File.ReadAllText(AppSettingsPath);
var jsonNodeOptions = new JsonNodeOptions { PropertyNameCaseInsensitive = true };
var node = JsonNode.Parse(appSettingsJson, jsonNodeOptions);
var options = new JsonSerializerOptions { WriteIndented = true };
Console.WriteLine("=========== Before ============");
Console.WriteLine(node.ToJsonString(options));
// Now you have access to all the structure using node["blah"] syntax
var stringSetting = (string) node["sampleSection"]["someStringSetting"];
var intSetting = (int) node["sampleSection"]["deeperValues"]["someIntSetting"];
var booleanSetting = (bool) node["sampleSection"]["deeperValues"]["someBooleanSetting"];
Console.WriteLine($"stringSetting: {stringSetting}, intSetting: {intSetting}, booleanSetting: {booleanSetting}");
// Now write new values back
node["sampleSection"]["someStringSetting"] = $"New setting at {DateTimeOffset.Now}";
node["sampleSection"]["deeperValues"]["someIntSetting"] = -6;
node["sampleSection"]["deeperValues"]["someBooleanSetting"] = false;
Console.WriteLine("=========== After ============");
Console.WriteLine(node.ToJsonString(options));
// Or, to actually write it to disk:
// File.WriteAllText(AppSettingsPath, node.ToJsonString(options));
I hope that my scenario covers your intent, I wanted to override the appsettings.json values if there are environment variables passed to the app at startup.
I made use of the ConfigureOptions method that is available in dotnet core 2.1.
Here is the Model that is used for the JSON from appsettings.json
public class Integration
{
public string FOO_API {get;set;}
}
For the services in the statup.cs:
var section = Configuration.GetSection ("integration");
services.Configure<Integration> (section);
services.ConfigureOptions<ConfigureIntegrationSettings>();
Here is the implemenation:
public class ConfigureIntegrationSettings : IConfigureOptions<Integration>
{
public void Configure(Integration options)
{
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FOO")))
options.FOO_API = Environment.GetEnvironmentVariable("FOO_API");
}
}
so if there is no value set it falls back to the appsettings.json
I solved similar problem - I needed override appSettings like this:
For 'IConfigurationBuilder':
configurationBuilder
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{environmentName}.json", false, true)
.AddConfigurationObject(TenantsTimeZoneConfigurationOverrides(configurationBuilder)); // Override Tenants TimeZone configuration due the OS platform (https://dejanstojanovic.net/aspnet/2018/july/differences-in-time-zones-in-net-core-on-windows-and-linux-host-os/)
private static Dictionary<string, string> TenantsTimeZoneConfigurationOverrides(IConfigurationBuilder configurationBuilder)
{
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var overridesDictionary = new Dictionary<string, string>();
var configuration = configurationBuilder.Build() as IConfiguration;
var tenantsSection = configuration.GetSection(TenantsConfig.TenantsCollectionConfigSectionName).Get<Tenants>();
foreach (var tenant in tenantsSection)
{
if (!string.IsNullOrEmpty(tenant.Value.TimeZone))
{
overridesDictionary.Add($"Tenants:{tenant.Key}:TimeZone", GetSpecificTimeZoneDueOsPlatform(isWindows, tenant.Value.TimeZone));
}
}
return overridesDictionary;
}
private static string GetSpecificTimeZoneDueOsPlatform(bool isWindows, string timeZone)
{
return isWindows ? timeZone : TZConvert.WindowsToIana(timeZone);
}