【问题标题】:How to update values into appsetting.json?如何将值更新到 appsettings.json?
【发布时间】:2016-12-05 09:26:59
【问题描述】:

我正在使用IOptions 模式,如in the official documentation 所述。

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

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

【问题讨论】:

  • 该框架提供了一个通用的基础设施来读取配置值,而不是修改它们。在修改时,您必须使用特定的配置提供程序来访问和修改底层配置源。
  • “特定配置提供者访问和修改底层配置源”?你能给我一些参考吗?
  • 您要修改的配置源是什么?
  • 就像我在帖子中所说的 - appsetting.json,在此我存储了一些应用程序范围的设置,我打算从 UI 进行修改。
  • 是的...它是 ASP.NET Core MVC 应用程序。

标签: c# asp.net-core asp.net-core-mvc


【解决方案1】:

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

我声明了扩展IOptions<T>IWritableOptions<T> 接口;所以只要我想读写设置,我就可以用IWritableOptions<T>替换IOptions<T>

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

另外,我想出了IOptionsWriter,这是一个供IWritableOptions&lt;T&gt; 用来更新配置部分的组件。这是我对前面提到的接口的实现...

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 的实例,使用当前值(如果未找到),或者如果当前值为null,则仅创建T 的新实例。然后将此持有者对象传递给调用者,调用者将对其应用更改。然后更改的对象将转换回 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);
        });
    }
}

可以用ConfigureServiceslike...

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

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

private IWritableOptions<CustomizableOptions> options;

...

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

【讨论】:

  • 非常感谢。我有一个关于IConfigurationRoot 的后续问题,也许你知道答案:)
  • 一年后这仍然是唯一的方法吗?
  • @JavierGarcíaManzano 我想是的; GitHub 上有一个问题(2018 年 3 月提交),但已关闭:github.com/aspnet/Home/issues/2973
  • 所以在我走这条路之前,我只是想检查一下......差不多 4 年后,我们仍然需要做这样的事情吗?现在没有东西烤了吗? 2021 年人们使用什么来写入自定义 JSON 选项/配置文件?
  • 谢谢,@Matze。我不介意创建一个新类来处理我的所有设置。我通常会不遗余力地做这样的事情……直到后来才发现有一种更简单的方法可以完成相同的任务。尝试改进我的编码实践并首先寻找已经构建的东西。我在这个问题 (stackoverflow.com/a/45986656/1039753) 的 cmets 中查看了 Derrell 的 NuGet 包。似乎在他投入了所有工作之后,他最终决定,我猜用 JSON 做这件事太不稳定了。所以它让我停下来问这个问题。
【解决方案2】:

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");

【讨论】:

  • 我对“MyOptions”有点不确定,但阅读这篇文章帮助我弄清楚了。 codingblast.com/…
  • 已经创建了一个 github 存储库并作为 nuget 包推送 - 进行了一些更改以提高灵活性:github.com/dazinator/Dazinator.Extensions.WritableOptions
  • 选择解决方案。但是在使用它时,我的代码由于以下行而遇到错误:JObject.Parse(JsonConvert.SerializeObject(sectionObject))。如果 sectionObject 是一个数组,此行将失败。可以替换为:JToken.Parse(JsonConvert.SerializeObject(sectionObject))
  • 这个解决方案很棒,但我有一个问题。重定向后未注入我更新的文件(注入旧配置)。我创建了带有解释的问题stackoverflow.com/q/59677486/9684060
【解决方案3】:
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);
}

【讨论】:

  • appsettings.json 不必是有效的 json。例如,在 appsettings.json 中允许使用 cmets。动态也非常慢。
【解决方案4】:

我看到很多答案使用Newtonsoft.Json 包来更新appsettings。我将提供一些使用System.Text.Json 包的解决方案(内置在.Net Core 3 及更高版本上)。

选项 1

在开始动态更新appsettings.json 文件之前,问自己一个问题,appsettings.json 中需要更新的部分有多复杂。如果需要更新的部分不是很复杂,您可以将appsettings transformation functionality 仅用于需要更新的部分。这是一个例子: 假设我的appsettings.json 文件如下所示:

{
    "Username": "Bro300",
    "Job": {
        "Title": "Programmer",
        "Type": "IT"
    }
}

假设我只需要更新Job 部分。我可以创建一个较小的文件appsettings.MyOverrides.json,而不是直接更新appsettings.json,它看起来像这样:

{
  "Job": {
    "Title": "Farmer",
    "Type": "Agriculture"
  }
}

然后确保将这个新文件添加到我的 .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; }
}

然后我可以创建如下所示的更新程序类(注意它接受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);
    }
}

最后,这是演示如何使用它的代码:

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>();
    }
}

选项 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);
}

选项 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;
    }
}

你可以这样使用:

var settingsUpdater = new AppSettingsUpdater();
settingsUpdater.UpdateAppSetting("OuterProperty:NestedProperty:PropertyToUpdate", "new value");

【讨论】:

    【解决方案5】:

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

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

    这是用于来自 appsettings.json 的 JSON 的模型

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

    对于 statup.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

    【讨论】:

      【解决方案6】:

      我解决了类似的问题 - 我需要像这样覆盖 appSettings:

      对于“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);
          }
      

      【讨论】:

        【解决方案7】:

        通过此代码更新值 它只是运行控制台应用程序,它读取应用程序设置、添加新设置并更新现有设置。并在更新后刷新服务器上的应用程序而不关闭应用程序。

        欲了解更多信息: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);
                }
            }
        

        【讨论】:

          【解决方案8】:

          虽然仍然没有办法通过 Options 访问器,但我想预设一个 .NET 6 类,这样可以很容易地写回文件。您可以在System.Text.Json.Nodes 类中使用JsonNode 类。在从 appsettings.json 读取纯文本后,我正在使用它写回加密的连接字符串。

          有一些使用Newtonsoft.Json.JsonConvert.DeserializeObject 并反序列化为@Alper 建议的动态类型的示例 - 但System.Text.Json 无法做到这一点。好吧,现在你可以:)(虽然不是动态类型)。

          在下面的示例中,我试图做到简约和简单。我使用JsonNode 来检索值而不是依赖注入IConfiguration。在真正的 Web 应用程序中,我会使用 DI 方法。您如何检索设置实际上并不重要,将其写回仍然意味着重建 Json 并更新磁盘上的文件。

          JsonNode 的 MS 链接:https://docs.microsoft.com/en-us/dotnet/api/system.text.json.nodes.jsonnode?view=net-6.0

          我的 appsettings.json 示例:

          {
            "sampleSection": {
              "someStringSetting": "Value One",
              "deeperValues": {
                "someIntSetting": 23,
                "someBooleanSetting": true
              }
            }
          }
          

          C# .NET 6 控制台应用程序:

          using System.Text.Json;
          using System.Text.Json.Nodes;
          
          const string AppSettingsPath = @"<PathToYourAppSettings.JsonFile>>\appsettings.json";
          string appSettingsJson = File.ReadAllText(AppSettingsPath);
          var node = JsonNode.Parse(appSettingsJson);
          
          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
          // Note: Names are case sensitive!
          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));
          

          【讨论】:

            猜你喜欢
            • 2022-11-24
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-08-10
            • 2019-10-18
            • 2017-10-01
            • 1970-01-01
            相关资源
            最近更新 更多