Hoe waarden bijwerken naar appsetting.json?

Ik gebruik het patroon IOptionszoals beschreven in de officiële documentatie.

Dit werkt prima als ik waarden van appsetting.jsonlees, maar hoe werk ik waarden bij en sla ik wijzigingen weer op in appsetting.json?

In mijn geval heb ik een paar velden die kunnen worden bewerkt vanuit de gebruikersinterface (door admin-gebruiker in de toepassing). Daarom ben ik op zoek naar de ideale aanpak om deze waarden bij te werken via de optie-accessor.


Antwoord 1, autoriteit 100%

Op het moment van schrijven van dit antwoord leek het erop dat er geen component wordt geleverd door het pakket Microsoft.Extensions.Optionsmet functionaliteit om configuratiewaarden terug te schrijven naar appsettings.json.

In een van mijn ASP.NET Core-projecten wilde ik de gebruiker in staat stellen enkele applicatie-instellingen te wijzigen – en die instellingswaarden moeten worden opgeslagen in appsettings.json, nauwkeuriger in een optioneel bestand appsettings.custom.json, dat indien aanwezig aan de configuratie wordt toegevoegd.

Zoals dit…

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

Ik heb de IWritableOptions<T>-interface aangegeven die IOptions<T>; dus ik kan gewoon IOptions<T>vervangen door IWritableOptions<T>wanneer ik instellingen wil lezen en schrijven.

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

Ook bedacht ik IOptionsWriter, een onderdeel dat bedoeld is om door IWritableOptions<T>te worden gebruikt om een ​​configuratiegedeelte bij te werken. Dit is mijn implementatie voor de bovengenoemde 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();
    }
}

Omdat de schrijver niet op de hoogte is van de bestandsstructuur, heb ik besloten om secties te behandelen als JObject-objecten. De accessor probeert de gevraagde sectie te vinden en deserialiseert deze naar een instantie van T, gebruikt de huidige waarde (indien niet gevonden), of maakt gewoon een nieuwe instantie van T, als de huidige waarde nullis. Dit houderobject wordt dan doorgegeven aan de beller, die de wijzigingen daarop zal toepassen. Dan wordt het gewijzigde object terug geconverteerd naar een JTokeninstantie die de sectie gaat vervangen…

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

Ten slotte heb ik een uitbreidingsmethode voor IServicesCollectiongeïmplementeerd, waardoor ik gemakkelijk een beschrijfbare optie-accessor kan configureren…

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

Die kan worden gebruikt in ConfigureServiceszoals…

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

In mijn Controller-klasse kan ik gewoon een IWritableOptions<CustomizableOptions>-instantie eisen, die dezelfde kenmerken heeft als IOptions<T>, maar maakt het ook mogelijk om configuratiewaarden te wijzigen en op te slaan.

private IWritableOptions<CustomizableOptions> options;
...
this.options.Update((opt) => {
    opt.SampleOption = "...";
});

Antwoord 2, autoriteit 88%

Vereenvoudigde versie van Matze’s antwoord:

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

Gebruik:

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

Dan:

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

Om de wijzigingen in het bestand op te slaan:

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

En u kunt een aangepast json-bestand doorgeven als optionele parameter (het gebruikt standaard appsettings.json):

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

Antwoord 3, autoriteit 12%

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

Antwoord 4

Ik hoop dat mijn scenario uw bedoeling dekt. ​​Ik wilde de appsettings.json-waarden overschrijven als er omgevingsvariabelen worden doorgegeven aan de app bij het opstarten.

Ik heb gebruik gemaakt van de ConfigureOptions-methode die beschikbaar is in dotnet core 2.1.

Hier is het model dat wordt gebruikt voor de JSON van appsettings.json

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

Voor de diensten in de statup.cs:

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

Hier is de implementatie:

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

dus als er geen waarde is ingesteld, valt deze terug naar appsettings.json


Antwoord 5

Ik heb een soortgelijk probleem opgelost – ik moest appSettings als volgt overschrijven:

Voor ‘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);
    }

Other episodes