439*_*439 24 asp.net-core-mvc asp.net-core
我正在使用官方文档中IOptions描述的模式.
当我从中读取值时appsetting.json,这可以正常工作,但是如何更新值并将更改保存回来appsetting.json?
在我的例子中,我有几个字段可以从用户界面编辑(由管理员用户在应用程序中).因此,我正在寻找通过选项访问器更新这些值的理想方法.
Mat*_*tze 32
在编写此答案时,似乎没有Microsoft.Extensions.Options包提供的组件具有将配置值写回的功能appsettings.json.
在我的一个ASP.NET Core项目中,我想让用户更改一些应用程序设置 - 这些设置值应该appsettings.json更准确地存储在可选appsettings.custom.json文件中,如果存在,则会添加到配置中.
像这样...
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();
}
Run Code Online (Sandbox Code Playgroud)
我宣布IWritableOptions<T>扩展的接口IOptions<T>; 这样我就可以替代IOptions<T>由IWritableOptions<T>每当我想读写设置.
public interface IWritableOptions<out T> : IOptions<T> where T : class, new()
{
void Update(Action<T> applyChanges);
}
Run Code Online (Sandbox Code Playgroud)
此外,我想出了IOptionsWriter一个组件,旨在用于IWritableOptions<T>更新配置部分.这是我对前面提到的接口的实现......
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();
}
}
Run Code Online (Sandbox Code Playgroud)
由于编写器不知道文件结构,我决定将段处理为JObject对象.访问者尝试查找请求的部分并将其反序列化为实例T,使用当前值(如果未找到),或者仅创建新实例(T如果当前值为)null.然后将此持有者对象传递给调用者,调用者将对其应用更改.比更改的对象转换回JToken将要替换该部分的实例...
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);
});
}
}
Run Code Online (Sandbox Code Playgroud)
最后,我实现了一个扩展方法,IServicesCollection允许我轻松配置可写选项访问器...
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);
});
}
}
Run Code Online (Sandbox Code Playgroud)
哪个可以用在ConfigureServices......
services.ConfigureWritable<CustomizableOptions>(this.Configuration,
"MySection", "appsettings.custom.json");
Run Code Online (Sandbox Code Playgroud)
在我的Controller课程中,我可以要求一个IWritableOptions<CustomizableOptions>具有相同特征的实例IOptions<T>,但也允许更改和存储配置值.
private IWritableOptions<CustomizableOptions> options;
...
this.options.Update((opt) => {
opt.SampleOption = "...";
});
Run Code Online (Sandbox Code Playgroud)
cef*_*ari 30
简化版Matze的答案:
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);
});
}
}
Run Code Online (Sandbox Code Playgroud)
用法:
services.ConfigureWritable<MyOptions>(Configuration.GetSection("MySection"));
Run Code Online (Sandbox Code Playgroud)
然后:
private readonly IWritableOptions<MyOptions> _options;
public MyClass(IWritableOptions<MyOptions> options)
{
_options = options;
}
Run Code Online (Sandbox Code Playgroud)
要将更改保存到文件:
_options.Update(opt => {
opt.Field1 = "value1";
opt.Field2 = "value2";
});
Run Code Online (Sandbox Code Playgroud)
并且您可以将自定义json文件作为可选参数传递(默认情况下它将使用appsettings.json):
services.ConfigureWritable<MyOptions>(Configuration.GetSection("MySection"), "appsettings.custom.json");
Run Code Online (Sandbox Code Playgroud)
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);
}
Run Code Online (Sandbox Code Playgroud)
我看到很多答案使用Newtonsoft.Json包来更新 appsettings。我将提供一些使用System.Text.Json包的解决方案(内置于 .Net Core 3 及更高版本)。
选项1
在开始appsettings.json动态更新文件之前,先问自己一个问题,appsettings.json 中需要更新的部分有多复杂。如果需要更新的部分不是很复杂,您可以只对需要更新的部分使用appsettings 转换功能。这是一个示例:假设我的appsettings.json文件如下所示:
{
"Username": "Bro300",
"Job": {
"Title": "Programmer",
"Type": "IT"
}
}
Run Code Online (Sandbox Code Playgroud)
假设我只需要更新Job部分。appsettings.json我可以创建一个较小的文件appsettings.MyOverrides.json,而不是直接更新,如下所示:
{
"Job": {
"Title": "Farmer",
"Type": "Agriculture"
}
}
Run Code Online (Sandbox Code Playgroud)
然后确保将这个新文件添加到我的 .Net Core 应用程序中,并且 .Net Core 将弄清楚如何加载新的更新设置。现在下一步是创建一个包装类,它将保存这样的值appsettings.MyOverrides.json:
public class OverridableSettings
{
public JobSettings Job { get; set; }
}
public class JobSettings
{
public string Title { get; set; }
public string Type { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
然后我可以创建我的更新程序类,它看起来像这样(注意它接收OverridableSettings并完全覆盖appsettings.MyOverrides.json文件:
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);
}
}
Run Code Online (Sandbox Code Playgroud)
最后,这是演示如何使用它的代码:
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>();
}
}
Run Code Online (Sandbox Code Playgroud)
选项 2
如果 appsetting 转换不适合您的情况,并且您只需要更新一层深的值,您可以使用这个简单的实现:
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);
}
Run Code Online (Sandbox Code Playgroud)
选项 3
最后,如果您有一些复杂的情况并且您需要更新多级深度的 appsettings,这里是另一种实现,它扩展了前一个选项,并使用递归来更新任何级别的设置:
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;
}
}
Run Code Online (Sandbox Code Playgroud)
您可以像这样使用:
var settingsUpdater = new AppSettingsUpdater();
settingsUpdater.UpdateAppSetting("OuterProperty:NestedProperty:PropertyToUpdate", "new value");
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
13294 次 |
| 最近记录: |