web-dev-qa-db-fra.com

Comment mettre à jour les valeurs dans appsetting.json?

J'utilise le modèle IOptions tel que décrit dans la documentation officielle .

Cela fonctionne bien lorsque je lis les valeurs de appsetting.json, mais comment puis-je mettre à jour des valeurs et enregistrer les modifications dans appsetting.json?

Dans mon cas, j'ai quelques champs qui peuvent être édités à partir de l'interface utilisateur (par l'utilisateur admin dans l'application). C'est pourquoi je recherche l'approche idéale pour mettre à jour ces valeurs via l'option accessor.

18
439

Au moment de la rédaction de cette réponse, il semblait qu'aucun composant fourni par le package Microsoft.Extensions.Options ne contienne la fonctionnalité permettant d'écrire les valeurs de configuration dans appsettings.json.

Dans l'un de mes projets ASP.NET Core, je voulais permettre à l'utilisateur de modifier certains paramètres de l'application. Ces valeurs doivent être stockées dans appsettings.json, plus précisément dans un fichier optionnel appsettings.custom.json, qui est ajouté à la configuration, le cas échéant.

Comme ça...

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

J'ai déclaré l'interface IWritableOptions<T> qui étend IOptions<T>; je peux donc simplement remplacer IOptions<T> par IWritableOptions<T> chaque fois que je veux lire et écrire les paramètres.

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

De plus, j'ai proposé IOptionsWriter, un composant destiné à être utilisé par IWritableOptions<T> pour mettre à jour une section de configuration. Ceci est mon implémentation pour les interfaces précitées ...

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

Étant donné que l'auteur ne connaît pas la structure du fichier, j'ai décidé de traiter les sections sous forme d'objets JObject. L'accesseur essaie de trouver la section demandée et la désérialise en une instance de T, utilise la valeur actuelle (si introuvable) ou crée simplement une nouvelle instance de T, si la valeur actuelle est null. Cet objet est ensuite transmis à l'appelant, qui lui appliquera les modifications. Que l'objet modifié soit reconverti en une instance JToken qui remplacera la 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);
        });
    }
}

Enfin, j'ai implémenté une méthode d'extension pour IServicesCollection me permettant de configurer facilement un accesseur d'options en écriture ...

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

Qui peut être utilisé dans ConfigureServices comme ...

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

Dans ma classe Controller, je peux simplement demander une instance IWritableOptions<CustomizableOptions>, qui présente les mêmes caractéristiques que IOptions<T>, mais permet également de modifier et de stocker les valeurs de configuration.

private IWritableOptions<CustomizableOptions> options;

...

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

Version simplifiée de la réponse de 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);
        });
    }
}

Usage:

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

Ensuite:

private readonly IWritableOptions<MyOptions> _options;

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

Pour enregistrer les modifications dans le fichier:

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

Et vous pouvez passer un fichier json personnalisé comme paramètre facultatif (il utilisera par défaut appsettings.json):

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

J'espère que mon scénario couvre votre intention. Je voulais remplacer les valeurs de appsettings.json s'il existe des variables d'environnement transmises à l'application au démarrage.

Je me suis servi de la méthode ConfigureOptions disponible dans dotnet core 2.1.

Voici le modèle utilisé pour le JSON à partir de appsettings.json

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

Pour les services dans le statup.cs:

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

Voici l'implémentation:

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

        }
    }

donc, s'il n'y a pas de valeur définie, il revient à la appsettings.json

0
leeroya