如何将值更新到appsetting.json?

问题描述 投票:21回答:3

我正在使用IOptions描述的in the official documentation模式。

当我从appsetting.json读取值时,这工作正常,但如何更新值并将更改保存回appsetting.json

在我的例子中,我有几个字段可以从用户界面编辑(由管理员用户在应用程序中)。因此,我正在寻找通过选项访问器更新这些值的理想方法。

asp.net-core asp.net-core-mvc
3个回答
27
投票

在撰写此答案时,似乎没有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();
}

我宣布扩展IWritableOptions<T>IOptions<T>接口;因此,只要我想读写设置,我就可以用IOptions<T>替换IWritableOptions<T>

public interface IWritableOptions<out T> : IOptions<T> where T : class, new()
{
    void Update(Action<T> applyChanges);
}

另外,我想出了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();
    }
}

由于编写器不知道文件结构,我决定将部分处理为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);
        });
    }
}

最后,我实现了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);
        });
    }
}

哪个可以在ConfigureServices中使用...

services.ConfigureWritable<CustomizableOptions>(this.Configuration, 
    "MySection", "appsettings.custom.json");

在我的Controller类中,我可以要求一个IWritableOptions<CustomizableOptions>实例,它具有与IOptions<T>相同的特性,但也允许更改和存储配置值。

private IWritableOptions<CustomizableOptions> options;

...

this.options.Update((opt) => {
    opt.SampleOption = "...";
});

24
投票

简化版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);
        });
    }
}

用法:

services.ConfigureWritable<MyOptions>(Configuration.GetSection("MySection"));

然后:

private readonly IWritableOptions<MyOptions> _options;

public MyClass(IWritableOptions<MyOptions> options)
{
    _options = options;
}

要将更改保存到文件:

_options.Update(opt => {
    opt.Field1 = "value1";
    opt.Field2 = "value2";
});

并且您可以将自定义json文件作为可选参数传递(默认情况下它将使用appsettings.json):

services.ConfigureWritable<MyOptions>(Configuration.GetSection("MySection"), "appsettings.custom.json");

1
投票

我希望我的场景涵盖您的意图,如果在启动时传递给应用程序的环境变量,我想覆盖appsettings.json值。

我使用了dotnet core 2.1中提供的ConfigureOptions方法。

以下是用于appsettings.json的JSON的Model

public class Integration
{
 public string FOO_API {get;set;}
}

对于startup.cs中的服务:

var section = Configuration.GetSection ("integration");
            services.Configure<Integration> (section);
            services.ConfigureOptions<ConfigureIntegrationSettings>();

这是实施:

public class ConfigureIntegrationSettings : IConfigureOptions<Integration>
    {
        public void Configure(Integration options)
        {
            if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FOO")))
                options.FOO_API = Environment.GetEnvironmentVariable("FOO_API");

        }
    }

因此,如果没有设置值,则会回退到appsettings.json

© www.soinside.com 2019 - 2024. All rights reserved.